关于java:实战Spring-Boot-SecurityJWT前后端分离架构登录认证

9次阅读

共计 7948 个字符,预计需要花费 20 分钟才能阅读完成。

大家好,我是不才陈某~

认证、受权是实战我的项目中必不可少的局部,而 Spring Security 则将作为首选平安组件,因而陈某新开了《Spring Security 进阶》这个专栏,写一写从单体架构到 OAuth2 分布式架构的认证受权。

Spring security 这里就不再过多介绍了,置信大家都用过,也都恐怖过,相比 Shiro 而言,Spring Security 更加重量级,之前的 SSM 我的项目更多企业都是用的 Shiro,然而 Spring Boot 进去之后,整合 Spring Security 更加不便了,用的企业也就多了。

明天陈某就来介绍一下在前后端拆散的我的项目中如何应用 Spring Security 进行登录认证。文章的目录如下:

前后端拆散认证的思路

前后端拆散不同于传统的 web 服务,无奈应用 session,因而咱们采纳 JWT 这种无状态机制来生成 token,大抵的思路如下:

  1. 客户端调用服务端登录接口,输出用户名、明码登录,登录胜利返回两个token,如下:

    1. accessToken:客户端携带这个 token 拜访服务端的资源
    2. refreshToken:刷新令牌,一旦 accessToken 过期了,客户端须要应用 refreshToken 从新获取一个 accessToken。因而 refreshToken 的过期工夫个别大于 accessToken。
  2. 客户申请头中携带 accessToken 拜访服务端的资源,服务端对 accessToken 进行鉴定(验签、是否生效 ….),如果这个 accessToken 没有问题则放行。
  3. accessToken一旦过期须要客户端携带 refreshToken 调用刷新令牌的接口从新获取一个新的accessToken

我的项目搭建

陈某应用的是 Spring Boot 框架,演示我的项目新建了两个模块,别离是common-basesecurity-authentication-jwt

1、common-base 模块

这是一个形象进去的公共模块,这个模块次要放一些专用的类,目录如下:

2、security-authentication-jwt 模块

一些须要定制的类,比方 security 的全局配置类、Jwt 登录过滤器的配置类,目录如下:

3、五张表

权限设计依据业务的需要往往有不同的设计,陈某用的 RBAC 标准,次要波及到五张表,别离是 用户表 角色表 权限表 用户 <-> 角色表 角色 <-> 权限表,如下图:

上述几张表的 SQL 会放在案例源码中(这几张表字段为了省事,设计的并不全,本人依据业务逐渐拓展即可)

登录认证过滤器

登录接口的逻辑写法有很多种,明天陈某介绍一种应用过滤器的定义的登录接口。

Spring Security 默认的表单登录认证的过滤器是UsernamePasswordAuthenticationFilter,这个过滤器并不适用于前后端拆散的架构,因而咱们须要自定义一个过滤器。

逻辑很简略,参照 UsernamePasswordAuthenticationFilter 这个过滤器革新一下,代码如下:

认证胜利处理器 AuthenticationSuccessHandler

上述的过滤器接口一旦认证胜利,则会调用 AuthenticationSuccessHandler 进行解决,因而咱们能够自定义一个认证胜利处理器进行本人的业务解决,代码如下:

陈某仅仅返回了accessTokenrefreshToken,其余的业务逻辑解决本人欠缺。

认证失败处理器 AuthenticationFailureHandler

同样的,一旦登录失败,比方用户名或者明码谬误等等,则会调用 AuthenticationFailureHandler 进行解决,因而咱们须要自定义一个认证失败的处理器,其中依据异样信息返回特定的 JSON 数据给客户端,代码如下:

逻辑很简略,AuthenticationException有不同的实现类,依据异样的类型返回特定的提示信息即可。

AuthenticationEntryPoint 配置

AuthenticationEntryPoint这个接口当 用户未通过认证拜访受爱护的资源 时,将会调用其中的 commence() 办法进行解决,比方客户端携带的 token 被篡改,因而咱们须要自定义一个 AuthenticationEntryPoint 返回特定的提示信息,代码如下:

AccessDeniedHandler 配置

AccessDeniedHandler这处理器当认证胜利的用户拜访受爱护的资源,然而 权限不够,则会进入这个处理器进行解决,咱们能够实现这个处理器返回特定的提示信息给客户端,代码如下:

UserDetailsService 配置

UserDetailsService这个类是用来加载用户信息,包含 用户名 明码 权限 角色 汇合 …. 其中有一个办法如下:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

在认证逻辑中 Spring Security 会调用这个办法依据客户端传入的 username 加载该用户的详细信息,这个办法须要实现的逻辑如下:

  • 明码匹配
  • 加载权限、角色汇合

咱们须要实现这个接口,从 数据库 加载用户信息,代码如下:

其中的 LoginService 是依据用户名从数据库中查问出明码、角色、权限,代码如下:

UserDetails这个也是个接口,其中定义了几种办法,都是围绕着 用户名 明码 权限 + 角色汇合 这三个属性,因而咱们能够实现这个类拓展这些字段,SecurityUser代码如下:

拓展 UserDetailsService 这个类的实现个别波及到 5 张表,别离是 用户表 角色表 权限表 用户 <-> 角色对应关系表 角色 <-> 权限对应关系表 ,企业中的实现必须遵循RBAC 设计规定。这个规定陈某前面会具体介绍。

Token 校验过滤器

客户端申请头携带了 token,服务端必定是须要针对每次申请解析、校验 token,因而必须定义一个 Token 过滤器,这个过滤器的次要逻辑如下:

  • 从申请头中获取accessToken
  • accessToken 解析、验签、校验过期工夫
  • 校验胜利,将 authentication 存入 ThreadLocal 中,这样不便后续间接获取用户详细信息。

下面只是最根底的一些逻辑,理论开发中还有特定的解决,比方将用户的详细信息放入 Request 属性中、Redis 缓存中,这样可能实现 feign 的令牌中继成果。

校验过滤器的代码如下:

刷新令牌接口

accessToken一旦过期,客户端必须携带着 refreshToken 从新获取令牌,传统 web 服务是放在 cookie 中,只须要服务端实现刷新,齐全做到无感知令牌续期,然而前后端拆散架构中必须由客户端拿着 refreshToken 调接口手动刷新。

代码如下:

次要逻辑很简略,如下:

  • 校验refreshToken
  • 从新生成 accessTokenrefreshToken 返回给客户端。

留神:理论生产中 refreshToken 令牌的生成形式、加密算法能够和 accessToken 不同。

登录认证过滤器接口配置

上述定义了一个认证过滤器JwtAuthenticationLoginFilter,这个是用来登录的过滤器,然而并没有注入退出 Spring Security 的过滤器链中,须要定义配置,代码如下:

/**
 * @author 公号:码猿技术专栏
 * 登录过滤器的配置类
 */
@Configuration
public class JwtAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    /**
     * userDetailService
     */
    @Qualifier("jwtTokenUserDetailsService")
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 登录胜利处理器
     */
    @Autowired
    private LoginAuthenticationSuccessHandler loginAuthenticationSuccessHandler;

    /**
     * 登录失败处理器
     */
    @Autowired
    private LoginAuthenticationFailureHandler loginAuthenticationFailureHandler;

    /**
     * 加密
     */
    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 将登录接口的过滤器配置到过滤器链中
     * 1. 配置登录胜利、失败处理器
     * 2. 配置自定义的 userDetailService(从数据库中获取用户数据)* 3. 将自定义的过滤器配置到 spring security 的过滤器链中,配置在 UsernamePasswordAuthenticationFilter 之前
     * @param http
     */
    @Override
    public void configure(HttpSecurity http) {JwtAuthenticationLoginFilter filter = new JwtAuthenticationLoginFilter();
        filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        // 认证胜利处理器
        filter.setAuthenticationSuccessHandler(loginAuthenticationSuccessHandler);
        // 认证失败处理器
        filter.setAuthenticationFailureHandler(loginAuthenticationFailureHandler);
        // 间接应用 DaoAuthenticationProvider
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        // 设置 userDetailService
        provider.setUserDetailsService(userDetailsService);
        // 设置加密算法
        provider.setPasswordEncoder(passwordEncoder);
        http.authenticationProvider(provider);
        // 将这个过滤器增加到 UsernamePasswordAuthenticationFilter 之前执行
        http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
    }
}

所有的逻辑都在 public void configure(HttpSecurity http) 这个办法中,如下:

  • 设置认证胜利处理器loginAuthenticationSuccessHandler
  • 设置认证失败处理器loginAuthenticationFailureHandler
  • 设置 userDetailService 的实现类JwtTokenUserDetailsService
  • 设置加密算法 passwordEncoder
  • JwtAuthenticationLoginFilter 这个过滤器退出到过滤器链中,间接退出到 UsernamePasswordAuthenticationFilter 这个过滤器之前。

Spring Security 全局配置

上述仅仅配置了登录过滤器,还须要在全局配置类做一些配置,如下:

  • 利用登录过滤器的配置
  • 将登录接口、令牌刷新接口放行,不须要拦挡
  • 配置AuthenticationEntryPointAccessDeniedHandler
  • 禁用 session,前后端拆散 +JWT 形式不须要 session
  • 将 token 校验过滤器 TokenAuthenticationFilter 增加到过滤器链中,放在 UsernamePasswordAuthenticationFilter 之前。

残缺配置如下:

/**
 * @author 公众号:码猿技术专栏
 * @EnableGlobalMethodSecurity 开启权限校验的注解
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig;
    @Autowired
    private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;
    @Autowired
    private RequestAccessDeniedHandler requestAccessDeniedHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {http.formLogin()
                // 禁用表单登录,前后端拆散用不上
                .disable()
                // 利用登录过滤器的配置,配置拆散
                .apply(jwtAuthenticationSecurityConfig)

                .and()
                // 设置 URL 的受权
                .authorizeRequests()
                // 这里须要将登录页面放行,permitAll()示意不再拦挡,/login 登录的 url,/refreshToken 刷新 token 的 url
                //TODO 此处失常我的项目中放行的 url 还有很多,比方 swagger 相干的 url,druid 的后盾 url,一些动态资源
                .antMatchers("/login","/refreshToken")
                .permitAll()
                //hasRole()示意须要指定的角色能力拜访资源
                .antMatchers("/hello").hasRole("ADMIN")
                // anyRequest() 所有申请   authenticated() 必须被认证
                .anyRequest()
                .authenticated()

                // 解决异常情况:认证失败和权限有余
                .and()
                .exceptionHandling()
                // 认证未通过,不容许拜访异样处理器
                .authenticationEntryPoint(entryPointUnauthorizedHandler)
                // 认证通过,然而没权限处理器
                .accessDeniedHandler(requestAccessDeniedHandler)

                .and()
                // 禁用 session,JWT 校验不须要 session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                // 将 TOKEN 校验过滤器配置到过滤器链中,否则不失效,放到 UsernamePasswordAuthenticationFilter 之前
                .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class)
                // 敞开 csrf
                .csrf().disable();
    }

    // 自定义的 Jwt Token 校验过滤器
    @Bean
    public TokenAuthenticationFilter authenticationTokenFilterBean()  {return new TokenAuthenticationFilter();
    }

    /**
     * 加密算法
     * @return
     */
    @Bean
    public PasswordEncoder getPasswordEncoder(){return new BCryptPasswordEncoder();
    }
}

正文的很具体了,有不了解的认真看一下。

案例源码曾经上传 GitHub,关注公众号:码猿技术专栏,回复关键词:9529 获取!

测试

1、首先测试登录接口,postman 拜访 http://localhost:2001/securit…,如下:

能够看到,胜利返回了两个 token。

2、申请头不携带 token,间接申请 http://localhost:2001/securit…,如下:

能够看到,间接进入了 EntryPointUnauthorizedHandler 这个处理器。

3、携带 token 拜访 http://localhost:2001/securit…,如下:

胜利拜访,token 是无效的。

4、刷新令牌接口测试,携带一个过期的令牌拜访如下:

5、刷新令牌接口测试,携带未过期的令牌测试,如下:

能够看到,胜利返回了两个新的令牌。

源码追踪

以上一系列的配置齐全是参照 UsernamePasswordAuthenticationFilter 这个过滤器,这个是 web 服务表单登录的形式。

Spring Security 的原理就是一系列的过滤器组成,登录流程也是一样,起初在 org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#doFilter() 办法,进行认证匹配,如下:

attemptAuthentication()这个办法次要作用就是获取客户端传递的 username、password,封装成 UsernamePasswordAuthenticationToken 交给 ProviderManager 的进行认证,源码如下:

ProviderManager 次要流程是调用抽象类 AbstractUserDetailsAuthenticationProvider#authenticate() 办法,如下图:

retrieveUser()办法就是调用 userDetailService 查问用户信息。而后认证,一旦认证胜利或者失败,则会调用对应的失败、胜利处理器进行解决。

总结

Spring Security 尽管比拟重,然而真的好用,尤其是实现 Oauth2.0 标准,非常简单不便。

案例源码曾经上传 GitHub,关注公众号:码猿技术专栏,回复关键词:9529 获取!

最初说一句(别白嫖,求关注)

陈某每一篇文章都是精心输入,曾经写了 3 个专栏,整顿成PDF,获取形式如下:

  1. 《Spring Cloud 进阶》PDF:关注公号:【码猿技术专栏】回复关键词 Spring Cloud 进阶 获取!
  2. 《Spring Boot 进阶》PDF:关注公号:【码猿技术专栏】回复关键词 Spring Boot 进阶 获取!
  3. 《Mybatis 进阶》PDF:关注公号:【码猿技术专栏】回复关键词 Mybatis 进阶 获取!

如果这篇文章对你有所帮忙,或者有所启发的话,帮忙 点赞 在看 转发 珍藏,你的反对就是我坚持下去的最大能源!

关注公号:【码猿技术专栏】,公众号内有超赞的粉丝福利,回复:加群,能够退出技术探讨群,和大家一起探讨技术,吹牛逼!

正文完
 0