前言

之前写的 涂涂影院管理系统 这个 demo 是基于 shiro 来鉴权的,项目前后端分离后,显然集成 Spring Security 更加方便一些,毕竟,都用 Spring 了,权限管理当然 Spring Security.

花了半天时间整理的笔记,希望能对你有所帮助。

Spring Security 一句话概述:一组 filter 过滤器链组成的权限认证。

一、加入依赖

环境:项目采用 Spring Initializr 快速构建 Spring Boot ,版本交由 spring-boot-starter-parent 管理。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

在仅仅添加完依赖的情况下,启动项目看看:

1.1 控制台打印

控制台打印了一串密码,如下图所示:

访问一下项目中的某个方法:

http://localhost:7777/tmax/videoCategory/getAll

奇怪,怎么自己跳到 /login 路径下了,而且还让登陆?

1.2 账号登录

在登陆 from 表单里输入如下:

  • 用户名:user
  • 密码:0839a4ba-c8a3-4aee-8a6e-cd19c1d0b0c1(控制台打印的)

点击 Sign in 然后跳转到了目标地址:

添加 Spring Security 依赖后,实际触发了两件事,一时将系统中所有的连接服务都保护起来, 再就是会有默认配置 form 表单认证。

二、基本原理

Spring Security的整个工作流程如下所示:

绿色认证方式可以配置, 橘黄色和蓝色的位置不可更改。

Security 有两种认证方式:

  • httpbasic
  • formLogin 默认的,如上边那种方式

同样,Security 也提供两种过滤器类:

  • UsernamePasswordAuthenticationFilter 表示表单登陆过滤器
  • BasicAuthenticationFilter 表示 httpbaic 方式登陆过滤器

图中橙色的 FilterSecurityInterceptor 是最终的过滤器,它会决定当前的请求可不可以访问Controller,判断规则放在这个里面。

当不通过时会把异常抛给在这个过滤器的前面的 ExceptionTranslationFilter 过滤器。

ExceptionTranslationFilter 接收到异常信息时,将跳转页面引导用户进行认证,如上方所示的用户登陆界面。

三、自定义认证逻辑

实际开发中是不可能使用上方 Spring Security 默认的这种方式的,如何去覆盖掉 Spring Security 默认的配置呢?

我们以:将默认的 form 认证方式改为 httpbasic 方式为例。

创建SpringSecurity自定义配置类:WebSecurityConfig.java

@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {     @Override
    protected void configure(HttpSecurity http) throws Exception {         ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
                .authorizeRequests();         registry.and()
            表单登录方式
            .formLogin()
            .permitAll()
            .and()
            .logout()
            .permitAll()
            .and()
            .authorizeRequests()
            任何请求
            .anyRequest()
            需要身份认证
            .authenticated()
            .and()
            关闭跨站请求防护
            .csrf().disable()
            前后端分离采用JWT 不需要session
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    }
}

重新启动项目,已经看到修改后的 httpbasic 方式认证了。

在这里我们依然采用的默认提供的用户名 user,以及每次服务器启动自动生成的 password,那么可不可以自定义认证逻辑呢?比如采用数据库中的用户登陆?

答案是肯定的。

自定义用户认证逻辑需要了解三步:
  1. 处理用户信息获取逻辑
  2. 处理用户校验逻辑
  3. 处理密码加密解密

接下来我们来看一下这三步,然后实现自定义登陆:

3.1 处理用户信息获取逻辑

Spring Security 中用户信息获取逻辑的获取逻辑是封装在一个接口里的:UserDetailService,代码如下:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

这个接口中只有一个方法,loadUserByUsername(), 该接收一个 String 类型的 username 参数,然后返回一个 UserDetails 的对象。

那么这个方法到底是干啥的呢?

通过前台用户输入的用户名,然后去数据库存储中获取对应的用户信息,然后封装在 UserDetail 实现类里面。

封装到 UserDetail 实现类返回以后,Spring Srcurity 会拿着用户信息去做校验,如果校验通过了,就会把用户放在 session 里面,否则,抛出 UsernameNotFoundException 异常,Spring Security 捕获后做出相应的提示信息。

想要处理用户信息获取逻辑,那么我们就需要自己去实现 UserDetailsService

新建 UserDetailsServiceImpl.java

@Slf4j
@Component
public class UserDetailsServiceImpl implements UserDetailsService{     @Autowired
    private UserService userService;     /**
     * 从数据库中获取用户信息,返回一个 UserDetails 对象,
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {         通过用户名获取用户
        User user = userService.findByUsername(username);
        将 user 对象转化为 UserDetails 对象
        return new SecurityUserDetails(user);
    }
}

SecurityUserDetail.java

public class SecurityUserDetails extends User implements UserDetails {

    private static final long serialVersionUID = 1L;

    public SecurityUserDetails(User user) {

        if(user!=null) {
            this.setUsername(user.getUsername());
            this.setPassword(user.getPassword());
            this.setStatus(user.getStatus());
        }
    }     @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        理想型返回 admin 权限,可自已处理这块
        return AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
    }     /**
     * 账户是否过期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }     /**
     * 是否禁用
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }     /**
     * 密码是否过期
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }     /**
     * 是否启用
     * @return
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

至此,处理用户信息获取逻辑 部分完成了,主要实现 UserDetailsService 接口的 loadUserByname 方法。

为何会用到 SecurityUserDetail 类进行转换一下?

其实完全可以直接返回一个 User 对象,但是需要注意的是,如果直接返回 User 对象的话,返回的是 security 包下的 user。

至于为何这样处理,如果返回的是 security 包下的 user,这样就失去了使用本地数据库的意义,下方自定义登陆逻辑详细说明。

再来登陆试一下:

其中 niceyoo、 为数据库用户信息,如下图为成功跳转:

3.2 处理用户校验逻辑

关于用户的校验逻辑主要包含两方面:

  1. 密码是否匹配【由Sprin Security处理,只需要告诉其密码即可】
  2. 密码是否过期、或者账户是否被冻结等

前者,已经通过实现 UserDetailsService 的 loadUserByname() 方法实现了,接下来主要看看后者。

用户密码是否过期、是否被冻结等等需要实现 UserDetails 接口:

public interface UserDetails extends Serializable {

    Collection<? extends GrantedAuthority> getAuthorities();授权列表;

    String getPassword();从数据库中查询到的密码;

    String getUsername();用户输入的用户名;

    boolean isAccountNonExpired();当前账户是否过期;

    boolean isAccountNonLocked();账户是否被锁定;

    boolean isCredentialsNonExpired();账户的认证时间是否过期;

    boolean isEnabled();是账户是否有效。
}

主要看后四个方法:

1、isAccountNonExpired() 账户没有过期 返回true 表示没有过期
2、isAccountNonLocked() 账户没有锁定
3、isCredentialsNonExpired() 密码是否过期
4、isEnabled() 是否被删除

如上四个方法,皆可根据实际情况做响应处理。

3.3 处理密码加密解密

再回到 WebSecurityConfig 自定义配置类。加入:

@Autowired
private UserDetailsServiceImpl userDetailsService; @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());//加密
}

配置了这个 configure 方法以后,从前端传递过来的密码就会被加密,所以从数据库查询到的密码必须是经过加密的,而这个过程都是在用户注册的时候进行加密的。

补充:UserDetailsServiceImpl 为自定义的 UserDetailsService 实现类。

四、个性化认证流程

同样的在实际的开发中,对于用户的登录认证,不可能使用 Spring Security 自带的方式或者页面,需要自己定制适用于项目的登录流程。

Spring Security 支持用户在配置文件中配置自己的登录页面,如果用户配置了,则采用用户自己的页面,否则采用模块内置的登录页面。

WebSecurityConfig 配置类中增加 成功、失败过滤器。

@Autowired
private AuthenticationSuccessHandler successHandler; @Autowired
private AuthenticationFailHandler failHandler; @Override
protected void configure(HttpSecurity http) throws Exception {     ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
            .authorizeRequests();     registry.and()
        表单登录方式
        .formLogin()
        .permitAll()
        成功处理类
        .successHandler(successHandler)
        失败
        .failureHandler(failHandler)
        .and()
        .logout()
        .permitAll()
        .and()
        .authorizeRequests()
        任何请求
        .anyRequest()
        需要身份认证
        .authenticated()
        .and()
        关闭跨站请求防护
        .csrf().disable()
        前后端分离采用JWT 不需要session
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}

在添加 AuthenticationSuccessHandler、AuthenticationFailHandler 后会帮我们自动导包,但是,既然是个性化认证流程,自然要我们自己去实现~

那我们究竟要实现什么效果呢?

自定义登陆成功处理:

自定义登陆失败处理:

为何要采用这种返回新式?

用户登录成功后,Spring Security 的默认处理方式是跳转到原来的链接上,这也是企业级开发的常见方式,但是有时候采用的是 Ajax 方式发送的请求,往往需要返回 Json 数据,如图中:登陆成功后,会把 token 返回给前台,失败时则返回失败信息。

AuthenticationSuccessHandler:

Slf4j
@Component
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {     @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {         String username = ((UserDetails)authentication.getPrincipal()).getUsername();
        List<GrantedAuthority> authorities = (List<GrantedAuthority>) ((UserDetails)authentication.getPrincipal()).getAuthorities();
        List<String> list = new ArrayList<>();
        for(GrantedAuthority g : authorities){
            list.add(g.getAuthority());
        }
        登陆成功生成token
        String  token = UUID.randomUUID().toString().replace("-", "");
    token 需要保存至服务器一份,实现方式:redis or jwt
        输出到浏览器
        ResponseUtil.out(response, ResponseUtil.resultMap(true,200,"登录成功", token));
    }
}

SavedRequestAwareAuthenticationSuccessHandle r是 Spring Security 默认的成功处理器,默认方式是跳转。这里将认证信息作为 Json 数据进行了返回,也可以返回其他数据,这个是根据业务需求来定的,比如,上方代码在用户登陆成功后返回来 token,需要注意的是,此 token 需要在服务器备份一份,毕竟要用做下次的身份认证嘛~

AuthenticationFailHandler:

@Component
public class AuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler {     @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {         ## 默认情况下,不管你是用户名不存在,密码错误,SS 都会报出 Bad credentials 异常信息
        if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
            ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"用户名或密码错误"));
        } else if (e instanceof DisabledException) {
            ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"账户被禁用,请联系管理员"));
        } else {
            ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"登录失败,其他内部错误"));
        }
    } }

失败处理器跟成功处理此雷同。

ResponseUtil:

@Slf4j
public class ResponseUtil {     /**
     *  使用response输出JSON
     * @param response
     * @param resultMap
     */
    public static void out(HttpServletResponse response, Map<String, Object> resultMap){         ServletOutputStream out = null;
        try {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json;charset=UTF-8");
            out = response.getOutputStream();
            out.write(new Gson().toJson(resultMap).getBytes());
        } catch (Exception e) {
            log.error(e + "输出JSON出错");
        } finally{
            if(out!=null){
                try {
                    out.flush();
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

其中用到 gson 依赖:

<!-- Gson -->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.5</version>
</dependency>

最后

下一篇将集成 jwt 实现用户身份认证。

SpringSecurity 整合 JWT

项目集成Spring Security的更多相关文章

  1. SpringBoot集成Spring Security

    1.Spring Security介绍 Spring security,是一个强大的和高度可定制的身份验证和访问控制框架.它是确保基于Spring的应用程序的标准 --来自官方参考手册 Spring ...

  2. SpringMVC 3.1集成Spring Security 3.1

    这篇算是一个入门文章,昨天看见有网友提问,spring mvc集成spring security 的时候出错,揣测了一下问题木有解决.我就帮忙给搭建了一个集成框架他说可以,他告诉我这样的文章网上少.今 ...

  3. 【Spring】关于Boot应用中集成Spring Security你必须了解的那些事

    Spring Security Spring Security是Spring社区的一个顶级项目,也是Spring Boot官方推荐使用的Security框架.除了常规的Authentication和A ...

  4. SpringMVC 3.2集成Spring Security 3.2

    参考:http://www.cnblogs.com/Beyond-bit/p/springmvc_and_springsecurity.html SpringMVC 3.2集成Spring Secur ...

  5. SpringBoot 集成Spring security

    Spring security作为一种安全框架,使用简单,能够很轻松的集成到springboot项目中,下面讲一下如何在SpringBoot中集成Spring Security.使用gradle项目管 ...

  6. 关于Boot应用中集成Spring Security你必须了解的那些事

    Spring Security Spring Security是Spring社区的一个顶级项目,也是Spring Boot官方推荐使用的Security框架.除了常规的Authentication和A ...

  7. Spring Boot中集成Spring Security 专题

    check to see if spring security is applied that the appropriate resources are permitted: @Configurat ...

  8. SpringBoot集成Spring Security入门体验

    一.前言 Spring Security 和 Apache Shiro 都是安全框架,为Java应用程序提供身份认证和授权. 二者区别 Spring Security:重量级安全框架 Apache S ...

  9. SpringBoot集成Spring Security(6)——登录管理

    文章目录 一.自定义认证成功.失败处理 1.1 CustomAuthenticationSuccessHandler 1.2 CustomAuthenticationFailureHandler 1. ...

随机推荐

  1. 通过Fastdfs进行文件上传服务(文件和图片的统一处理)

    1.文件上传简单流程分析图: 2.Fastdfs介绍: Fastdfs由两个角色组成: Tracker(集群):调度(帮你找到有空闲的Storage) Storage(集群):文件存储(帮你保存文件或 ...

  2. C语言开发中常见报错的解决方案

    C语言开发中常见报错的解决方案 整理来源于网络,侵权请通知删除.*禁止转载 ---- fatal error C1003: error count exceeds number; stopping c ...

  3. SSM整合学习 一

    一:新建maven项目 File--New Project选择maven项目下的maven-archetype-webapp,输入GroupId.Artifactld,选择maven信息,新建mave ...

  4. JZOJ5833 永恒

    题目大意 给你一个树,每个节点上有有一个部落,以及部落的人数,要你求出每个节点的子树里面人数最多的部落是哪一个(人数相同部落编号最小的). 思路 全网第一篇分治题解 考虑树的dfs序,然后分治处理,每 ...

  5. 解决使用RabbitTemplate操作RabbitMQ,发生The channelMax limit is reached. Try later.问题

    使用RabbitTemplate操纵RabbitMQ,每个RabbitTemplate等于一个connection,每个connection最多支持2048个channel,当hannel达到2048 ...

  6. java之mybatis之缓存

    1.mybatis自带缓存功能.分为一级缓存,二级缓存. 2.一级缓存为 session 缓存,在一个 session中 ,一个查询的 select 语句只会执行一次,根据  <select&g ...

  7. 使用KONG网关实现接口迁移的灰度验证

    在我们对一个API站点进行微服务化的过程中,使用KONG网关可以实现以下几个效果: 1. 业务线无感知,其实内部已经被Kong转到其他站点上执行了,这对业务线特别友好. 2. 可以实现租户级/接口级灰 ...

  8. centos8安装chromium浏览器

    1/yum install epel* [root@localhost framework]# yum list epl* Last metadata expiration check: 0:57:4 ...

  9. 原油petrolaeum石油 Archaic spelling of petroleum

    petrolaeum (uncountable) Archaic spelling of petroleum petroleum See also: Petroleum Contents [hide] ...

  10. [LeetCode] 19. 删除链表的倒数第N个节点 ☆☆☆

    描述 给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点. 示例: 给定一个链表: 1->2->3->4->5, 和 n = 2. 当删除了倒数第二个节点后,链表 ...