Spring Security JWT -实现分布式架构的认证

一 理论优先

(一) 重大角色认识

第一要知道这么几个事情,实战中会使用到哦~

  • SecurityContext: Spring Security 的上下文对象, Authentication 认证对象会放在里面,若用户未认证,则 Authentication 。对象的内容为 null
  • SecurityContextHolder:用于拿到上下文对象的静态工具类,使用方法 SecurityContextHolder.getContext() 就可以获取到 SecurityContext 对象,
  • Authentication:认证接口,定义了认证对象的数据形式,列如用户名密码
  • AuthenticationManager:认证管理器。这个接口规范了 Spring Security 的过滤器要如何执行身份认证,并在身份认证成功后返回一个经过认证的 Authentication 对象

(二)Spring Security的过滤器链

之前我们介绍 Spring Security 的时候,曾经提到过 Spring Security 采用的是责任链的设计模式,它有一条很长的过滤器链来实现各个环节的控制逻辑

Spring Security JWT -实现分布式架构的认证

preview

如上图,每个 WEB 请求,会经过这么一条过滤器链,在经过过滤器的过程中会完成认证与授权。如果中间发现这个请求未认证或者未授权,就会抛出异常

Spring Security 的过滤器链有十几个,我们本篇主要讲的是 Spring Security 采用 JWT 实现认证方案,所以只关注以下几个:

  • SecurityContextPersistenceFilter ,在新版本中已经被废弃,推荐使用 SecurityContextHolderFilter 。在运行应用程序的其余部分之前, SecurityContextHolderFilterSecurityContextRepository 加载 SecurityContext 并将其设置在 SecurityContextHolder
  • UsernamePasswordAuthenticationFilter 是针对使用用户名和密码进行身份 认证 而定制化的一个过滤器,如果将原本默认的认证方案变更为 JWT ,那么就需要定义一个过滤器,并配置在其前面 将认证信息存储至上下文 之后,再进行认证
  • FilterSecurityInterceptor 会根据 SecurityContextHolder 中存储的用户信息来决定其是否有权限,从而决定是否允许访问

二 实战

既然 JWT 只是不同于 Session 的另一种认证方案,那么,我们的实战,也将在 Spring Security demo 的基础上,将原本的 Session 机制,更换为 JWT 而已

(一)Spring Security配置类

Spring Security 配置类 – SecurityConfig 如下

Spring Security JWT -实现分布式架构的认证

Spring Security demo 的基础上,为了将认证方案更改为 JWT ,做了两个地方的改动

1 认证过滤器配置

@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry
                authorizeRequests = http.csrf().disable().authorizeRequests();
//         1.查询到所有的权限
        List<Permission> allPermission = permissionMapper.findAllPermission();
//         2.分别添加权限规则
        allPermission.forEach((p -> {
            authorizeRequests.antMatchers(p.getUrl()).hasAnyAuthority(p.getName()) ;
        }));

        authorizeRequests.and()
                // 配置为 Spring Security 不创建使用 session
               .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
               .and()
               .authorizeRequests()
                .antMatchers("/**").fullyAuthenticated()
                //不用验证登录接口
               .antMatchers("/authenticate").permitAll()
               .anyRequest().authenticated();
        //配置认证过滤器 jwtAuthenticationFilter
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

复制代码

这里实则在之前 Spring Security 项目的基础上,增加了以下两项的配置

1)配置为 Spring Security 不创建使用 session

2)配置认证过滤器 jwtAuthenticationFilter

可以顺便复习一下:

SecurityFilterChain Bean 实际上是在新版本( 2.7.0 版本) Spring Security 中,用来定义各个资源能够被拥有哪些权限的用户所访问的规则

如资源 /user/common 可以分别被具有 admincommon 权限的用户所访问,而 /user/admin 则只可以被具有 admin 权限的用户所访问

2 配置认证接口的放行规则

/**
 * 资源放行配置
 * @return
 */
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
    return web -> {
        web.ignoring().antMatchers("/hello");
        web.ignoring().antMatchers("/login");
        //登录接口放行
        web.ignoring().antMatchers("/authenticate");
        web.ignoring().antMatchers("/css/**", "/js/**");
    };
}

复制代码

登录接口 /authenticate 是用来获取 token 的,它本身当然无法携带认证信息,所以这里配置我们后续所定义的登录接口 /authenticate 不用被校验

(二)JWT 认证过滤器

用于每次请求的拦截处理。 验证 token ,并将验证之后的 token 对应的用户信息 存储至上下文

/**
 * JWT 认证过滤器
 */
@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Resource
    private JwtService jwtService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            //1-获取 token
            String token = request.getHeader("Authorization");
            if (StrUtil.isBlank(token)) {
                //放行,会自动执行后面的过滤器
                logger.info("请求头不含 JWT token 或者 token 的值为空,调用下个过滤器");
                filterChain.doFilter(request,response);
                return;
            }
            //2-获取认证信息
            Authentication authentication = jwtService.getAuthentication(token);
            //3-设置用户验证对象至上下文
            SecurityContextHolder.getContext().setAuthentication(authentication);
        } catch (MalformedJwtException | ExpiredJwtException | UnsupportedJwtException e) {
            SecurityContextHolder.getContext().setAuthentication(null);
        } finally{
            filterChain.doFilter(request, response);
        }
    }

}

复制代码

这里通过继承 OncePerRequestFilter 类,对于客户端的请求进行拦截处理,并确保在一次请求中只通过一次过滤,避免重复执行

过滤器中的逻辑为:

1 获取 token。 第一从客户端的请求头中 获取 token 的值 ,并做 非空校验

2 获取认证对象。 调用JwtService的getAuthentication()方法 得到Authentication对象

进入 JwtServicegetAuthentication() 方法

/**
     * 获取认证过的token对应的信息,包括用户以及用户对应的权限
     * @param token
     * @return
     */
    public Authentication getAuthentication(String token) {
        // 1-根据 token 和秘钥,解析出 JWT 的 claims 对象
        Claims claims =
                Jwts.parser()
                        .setSigningKey(KEY)
                        .parseClaimsJws(token)
                        .getBody();
        // 2-获取权限信息,并转换为集合类型
        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());
        //3-得到用户对象 principal
        User principal = new User(claims.getSubject(), "", authorities);
         //4-得到认证 token 对象(UsernamePasswordAuthenticationToken 实现了 Authentication 接口,表明是通过用户名密码认证过的 token 认证信息)
        return new UsernamePasswordAuthenticationToken(principal, token, authorities);

    }

复制代码

1)得到 Claims 对象

根据 token 和秘钥,解析出 Claims 对象,这个里面就存储了我们的 JWTpayload 中各个参数值。 debug 可以看到,这个里面实际上是我们在创建 token 时,存进去的数据:用户的用户名 sub 、权限列表 auth 以及过期时间 exp

Spring Security JWT -实现分布式架构的认证

2)得到权限列表 authorities 。 获取权限信息,并转换为集合类型(生成 token 时,是以字符串的形式存储的)

3)将认证过后的这个用户对象配置到 Security 上下文:SecurityContextHolder

后续的 FilterSecurityInterceptor 会根据 SecurityContextHolder 中存储的用户信息来决定其是否有权限,从而决定是否允许访问,请求结束之后会清除掉该用户信息

一般利用 ThreadLocal 机制来保存每个使用者的 SecurityContext ,就避免了多线程并发导致信息错乱等问题了

(三)登录接口-生成token

@RestController
@CrossOrigin
public class JwtAuthenticationController {

    @Resource
    private UserService<User> userService;

    @Resource
    private JwtService jwtService;

    /**
     * 登录接口 - 用于生成 token
     * @param username 用户名
     * @param password 密码
     * @return
     * @throws Exception
     */
    @PostMapping("/authenticate")
    public ResponseResult login(String username, String password) throws Exception {
        //1-校验用户名密码是否为空
        if(StrUtil.isBlank(username) || StrUtil.isBlank(password)){
            throw new Exception("用户名或密码不能为空!");
        }

        // 2-根据用户查询用户是否存在
        User user = userService.findByUsername(username);
        if (user == null){
            throw new Exception("用户名或密码有误!");
        }
        //3-验证用户名密码
        password = MD5Util.md5slat(password);
        if (!password.equalsIgnoreCase(user.getPassword())){
            throw new Exception("用户名或密码有误!");
        }

        UserVo userVo =  UserVo.builder().build();
        userVo.setId(user.getId());
        userVo.setUsername(username);
        userVo.setPassword(password);
        //4- 生成 token
        String token = jwtService.createToken(userVo);
        userVo.setToken(token);
        userVo.setRefreshToken(UUID.randomUUID().toString());
        return new ResponseResult(userVo);
    }
}

复制代码

这里,就与上一篇中的登录接口的逻辑一样了,也是以下四个步骤:

1 校验用户名密码是否为空

2 根据用户名查询用户是否存在

3 验证用户名密码是否正确。(需要提前通过 MD5Util.md5slat(password ) 方法得到加密之后的密码,写入表 User 中的密码)

列如,我的密码是 123456 ,第一采用 MD5Util.md5slat(123456) 生成一个秘钥

Spring Security JWT -实现分布式架构的认证

运行 main 方法,输出的秘钥为:
53b432e61314e00bcc99287af9537dc4

填写在 user 表中

Spring Security JWT -实现分布式架构的认证

**4 生成 token 。**调用 jwtService.createToken() 方法生成 token ,并返回 UserVo 对象

那么, token 是怎样生成的呢?

进入 jwtService.createToken() 方法

/**
 * 创建 token
 * @param userVo 用户对象
 * @return token
 */
public String createToken(UserVo userVo) {
    String authorities = userVo.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.joining(","));

    long now = (new Date()).getTime();

    return Jwts.builder()
            .setSubject(userVo.getUsername())
            .claim(AUTHORITIES_KEY, authorities)
            .setExpiration(new Date(now + TOKEN_EXSPIRE_TIME))
            .signWith(key, SignatureAlgorithm.HS256)
            .compact();
}

复制代码

采用 Jwts.builder() 创建一个 JwtBuilder 构建器,然后分别配置用户的用户名 sub 、权限列表 auth 以及过期时间 exp ,指定加密算法,调用 compact() 方法得到一个字符串,即生成了一个 token 字符串

(四)资源接口

@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("/common")
    public String common() {
        return "hello~ common";
    }

    @GetMapping("/admin")
    public String admin() {
        return "hello~ admin";
    }

}

复制代码

分别定义 /user/common/user/admin 两个 API ,作为用户的权限,用于测试不同用户的权限

这里只列出了该项目的主要类,此外,还分别需要创建用于密码加密的 MD5Util 工具类, UserUserVoResponseResult 等实体类, 用于项目本身的功能完善

三 测试

需要说明一下,本次的项目是在 Spring Security 文章里面的 demo(** github.com/helemile/Sp… Session 变更为 JWT 认证方案而已,原先的实现逻辑完全没有变化

所以,RBAC 权限控制表: userroleuser_rolepermissionrole_permission 表中的信息完全一样(除了密码采用了 MD5 加盐的方式加密以外)

即:共存在两个用户: adminuser ,他们所对应的权限列表分别为:

user 用户: common 权限,可以访问的资源列表: /user/common

admin 用户: admin 权限,可以访问的资源列表: /user/admin/user/common

(一)user 用户

1 调用登录接口,获取token(user用户)

使用 user 账号进行登录,该账号只有 common 权限

Spring Security JWT -实现分布式架构的认证

登录之后,成功返回当前用户的信息,包括用户名,密码, token 以及权限列表 authorities

2 不携带token,直接访问资源/user/common

Spring Security JWT -实现分布式架构的认证

接口返回 403 -禁止访问

3 携带token,访问资源/user/common

Spring Security JWT -实现分布式架构的认证

访问成功

4 携带token,访问资源/user/admin

当然,由于 user 用户只有 common 权限,所以如果访问 /user/admin 资源,也会返回 403

Spring Security JWT -实现分布式架构的认证

若换成 admin 用户登录呢?

(二)admin 用户

1 调用登录接口,获取token(admin用户)

Spring Security JWT -实现分布式架构的认证

2 访问/user/admin资源

Spring Security JWT -实现分布式架构的认证

成功访问了

也就是说,我们的目的就达到了:成功将 Spring Security 的认证方案替换成了 JWT ,实现了 Sprinfg SecurityJWT 的结合

四 总结

今天我们讲了:

1 Spring Security 的几个重大类或接口: SecurityContextSecurityContextHolder“Authentication 以及 AuthenticationManager

2 Spring Security 的过滤器链说明

3实战实现 Spring SecurityJWT 认证方案的集成,共同实现了分布式系统的认证功能

关于用户的权限列表 authorities 如何获取这个问题,网上的许多实战例子是 在每次过滤器解析时,根据用户名去数据库查询一次,得到权限列表 。这样的效果实则跟 Session 差不多了,由于同样每次请求都要请求一次持久层呢~

我们这里实战 demo 的思路是将 用户以及用户的权限列表 authorities 作为生成 token 的其中一个数据项,从而在过滤器中解析完 token 即可获得,从而存储至认证对象: Authentication 中,而不用专门从数据库中获取一次,这种设计还是相对比较巧妙的

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 共1条

请登录后发表评论

    暂无评论内容