SpringSecurity - WebFlux环境下整合JWT使用 Token 认证授权

Posted 小毕超

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringSecurity - WebFlux环境下整合JWT使用 Token 认证授权相关的知识,希望对你有一定的参考价值。

一、SpringSecurity - WebFlux

上篇文章我们讲解了SpringSecurityWebFlux环境下的动态角色权限的控制,本篇文章我们一起讲解下SpringSecurityWebFlux环境下整合JWT使用 Token 认证授权。

上篇文章地址:https://blog.csdn.net/qq_43692950/article/details/122511037

二、整合JWT使用 Token 认证授权

在关于SpringSecurityWebFlux环境下的使用,在前面的几篇文章中已经讲解了。在看本篇文章之前,最好已经看过本专栏的前面几篇关于SpringSecurity 的文章了,一些重复性的代码就不再写出来了。下面直接进入主题。

在开始之前我们先清楚一个问题,对于登录认证SpringSecurity 已经帮我们实现了,我们可以指定登录的路径,默认是x-www-form-urlencoded方式。所以我们不用编写登录的逻辑,但是有些情况下可能SpringSecurity所提供的不能满足我们的需求,比如我们是自定义的加密数据传输的情况,此时我们可以自己写一个登录接口,在该接口中颁发Token令牌出来,并设置ServerHttpSecurity对象的formLogin().disable(),下面的演示中是采用SpringSecurity 所提供的认证来进行演示。

编写JWT工具类

这里我将权限也放在了JWT中,如果需要动态变更用户权限的可以考虑放在Redis或其他NoSql数据库中,本文主要演示Jwt的使用:

@Data
@Component
public class JwtTool 
    private String key = "com.bxc";
    private long overtime = 1000 * 60 * 60;

    public String CreateToken(String userid, String username, List<String> roles) 
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        JwtBuilder builder = Jwts.builder()
                .setId(userid)
                .setSubject(username)
                .setIssuedAt(now)
                .signWith(SignatureAlgorithm.HS256, key)
                .claim("roles", roles);
        if (overtime > 0) 
            builder.setExpiration(new Date(nowMillis + overtime));
        
        return builder.compact();
    

    public boolean VerityToken(String token) 
        try 
            Claims claims = Jwts.parser()
                    .setSigningKey(key)
                    .parseClaimsJws(token)
                    .getBody();
            if (claims != null) 
                return true;
            
         catch (Exception e) 
            e.printStackTrace();
        
        return false;
    

    public String getUserid(String token) 
        try 
            Claims claims = Jwts.parser()
                    .setSigningKey(key)
                    .parseClaimsJws(token)
                    .getBody();
            if (claims != null) 
                return claims.getId();
            
         catch (Exception e) 
            e.printStackTrace();
        
        return null;
    

    public String getUserName(String token) 
        try 
            Claims claims = Jwts.parser()
                    .setSigningKey(key)
                    .parseClaimsJws(token)
                    .getBody();
            if (claims != null) 
                return claims.getSubject();
            
         catch (Exception e) 
            e.printStackTrace();
        
        return null;
    

    public List<String> getUserRoles(String token) 
        try 
            Claims claims = Jwts.parser()
                    .setSigningKey(key)
                    .parseClaimsJws(token)
                    .getBody();
            if (claims != null) 
                return (List<String>) claims.get("roles");
            
         catch (Exception e) 
            e.printStackTrace();
        
        return null;
    

    public String getClaims(String token, String param) 
        try 
            Claims claims = Jwts.parser()
                    .setSigningKey(key)
                    .parseClaimsJws(token)
                    .getBody();
            if (claims != null) 
                return claims.get(param).toString();
            
         catch (Exception e) 
            e.printStackTrace();
        
        return null;
    

编写登录成功的Handler

我们可以在这里做办法Token令牌的逻辑:

@Component
public class LoginSuccessHandler implements ServerAuthenticationSuccessHandler 

    @Autowired
    JwtTool jwtTool;

    @Override
    public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) 
        UserEntity user = (UserEntity) authentication.getPrincipal();
        String username = user.getUsername();
        List<GrantedAuthority> authorities = (List<GrantedAuthority>) user.getAuthorities();
        List<String> roles = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
        String token = jwtTool.CreateToken(String.valueOf(user.getId()), username, roles);
        JSONObject params = new JSONObject();
        params.put("code", 200);
        params.put("msg", "登陆成功!");
        params.put("username", username);
        params.put("role", roles);
        params.put("token", token);

        ServerWebExchange exchange = webFilterExchange.getExchange();
        ServerHttpResponse response = exchange.getResponse();
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        Mono<Void> ret = null;
        try 
            ret = response.writeAndFlushWith(Flux.just(ByteBufFlux.just(response.bufferFactory().wrap(params.toJSONString().getBytes("UTF-8")))));
         catch (UnsupportedEncodingException e) 
            e.printStackTrace();
        
        return ret;
    

编写登录失败的Handler

返回客户端一个友好的提示,这里我直接返回了登录失败,大家可以根据AuthenticationException这个类进行具体判断,返回具体的错误信息:

@Component
public class LoginFailedHandler implements ServerAuthenticationFailureHandler 
    @Override
    public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException e) 
        JSONObject params = new JSONObject();
        params.put("code", 400);
        params.put("msg", "登录失败!");

        ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        Mono<Void> ret = null;
        try 
            ret = response.writeAndFlushWith(Flux.just(ByteBufFlux.just(response.bufferFactory().wrap(params.toJSONString().getBytes("UTF-8")))));
         catch (UnsupportedEncodingException e0) 
            e0.printStackTrace();
        
        return ret;
    

编写JWT的过滤器

既然上面已经颁发了JWTToken,那么请求来的第一步就要进行JWT的过滤和校验,如果OK在交给SpringSecurity将JWT的内容解析出来,所以这个过滤器只是一个教研JWT是否有效的作用,并没有对当前请求授权:

@Slf4j
@Component
public class JwtWebFilter implements WebFilter 

    @Autowired
    JwtTool jwtTool;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) 
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        HttpHeaders header = response.getHeaders();
        header.add("Content-Type", "application/json; charset=UTF-8");
        String path = request.getPath().value();
        if (path.contains("/auth/login"))
            return chain.filter(exchange);
        
        String token = exchange.getRequest().getHeaders().getFirst("token");
        if (StringUtils.isBlank(token)) 
            JSONObject jsonObject = setResultErrorMsg(401,"登录失效");
            DataBuffer buffer = response.bufferFactory().wrap(jsonObject.toJSONString().getBytes());
            return response.writeWith(Mono.just(buffer));
        
        boolean isold = jwtTool.VerityToken(token);
        if (!isold) 
            JSONObject jsonObject = setResultErrorMsg(401,"登录失效");
            DataBuffer buffer = response.bufferFactory().wrap(jsonObject.toJSONString().getBytes());
            return response.writeWith(Mono.just(buffer));
        
        String username = jwtTool.getUserName(token);
        if (com.alibaba.druid.util.StringUtils.isEmpty(username)) 
            JSONObject jsonObject = setResultErrorMsg(401,"登录失效");
            DataBuffer buffer = response.bufferFactory().wrap(jsonObject.toJSONString().getBytes());
            return response.writeWith(Mono.just(buffer));
        
        return chain.filter(exchange);
    

    private JSONObject setResultErrorMsg(Integer code,String msg) 
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code", code);
        jsonObject.put("message", msg);
        return jsonObject;
    

解析JWT中用户信息,并授予角色权限信息

上面只是做了JWT的一个初步过滤,到这就要解析JWT中的信息,组建一个UsernamePasswordAuthenticationToken进行用户的授权,这里我又做了一遍JWT的校验,其实这里可以不做JWT的校验了,前面的过滤器已经校验过了,直接取内容即可,

@Slf4j
@Component
public class JwtSecurityContextRepository implements ServerSecurityContextRepository 

    @Autowired
    JwtTool jwtTool;

    @Override
    public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) 
        return Mono.empty();
    

    @Override
    public Mono<SecurityContext> load(ServerWebExchange exchange) 
        String path = exchange.getRequest().getPath().toString();
        // 过滤路径
        if ("/auth/login".equals(path)) 
            return Mono.empty();
        
        String token = exchange.getRequest().getHeaders().getFirst("token");
        if (StringUtils.isBlank(token)) 
            throw new DisabledException("登录失效!");
        
        boolean isold = jwtTool.VerityToken(token);
        if (!isold) 
            throw new AccessDeniedException("登录失效!");
        
        String username = jwtTool.getUserName(token);
        if (com.alibaba.druid.util.StringUtils.isEmpty(username)) 
            throw new AccessDeniedException("登录失效!");
        
        Authentication newAuthentication = new UsernamePasswordAuthenticationToken(username, username);
        return new ReactiveAuthenticationManager() 
            @Override
            public Mono<Authentication> authenticate(Authentication authentication) 
                return Mono.fromCallable(() -> 
                    List<String> roles = jwtTool.getUserRoles(token);
                    List<GrantedAuthority> authorities = roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
                    UserEntity principal = new UserEntity();
                    principal.setUsername(username);
                    return new UsernamePasswordAuthenticationToken(principal, null, authorities);
                );
            
        .authenticate(newAuthentication).map(SecurityContextImpl::new);
    

判断用户是否有权访问该接口

上面只是获取到了用户所以拥有的角色权限信息,下面还要判断访问的该接口所需的角色用户是否拥有,这个地方的逻辑在上篇文章中进行了讲解,可以参考下上篇文章:

@Component
public class AuthManagerHandler implements ReactiveAuthorizationManager<AuthorizationContext> 

    @Autowired
    MeunMapper meunMapper;

    @Autowired
    RoleMapper roleMapper;
    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext object) 
        ServerHttpRequest request = object.getExchange().getRequest();
        String requestUrl = request.getPath().pathWithinApplication().value();
        List<MeunEntity> list = meunMapper.selectList(null);
        List<String> roles = new ArrayList<>();
        list.forEach(m -> 
            if (antPathMatcher.match(m.getPattern(), requestUrl)) 
                List<String> allRoleByMenuId = roleMapper.getAllRoleByMenuId(m.getId())
                        .stream()
                        .map(r -> r.getRole())
                        .collect(Collectors.toList());
                roles.addAll(allRoleByMenuId);
            
        );
        if (roles.isEmpty()) 
            return Mono.just(new AuthorizationDecision(false));
        
        return authentication
                .filter(a -> a.isAuthenticated())
                .flatMapIterable(a -> a.getAuthorities())
                .map(g -> g.getAuthority())
                .any(c -> 
                    if (roles.contains(String.valueOf(c))) 
                        return true;
                    
                    return false;
                )
                .map(hasAuthority -> new AuthorizationDecision(hasAuthority))
                .defaultIfEmpty(new AuthorizationDecision(false));
    

    @Override
    public Mono<Void> verify(Mono<Authentication> authentication, AuthorizationContext object) 
        return null;
    

编写无权访问的提示Handler

@Component
public class AccessDeniedHandler implements ServerAccessDeniedHandler 
    @Override
    public Mono<Void> handle(ServerWebExchange serverWebExchange, AccessDeniedException e) 
        JSONObject params = new JSONObject();
        params.put("code", 403);
        params.put("msg", "权限不足!");

        ServerHttpResponse response = serverWebExchange.getResponse();

        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        Mono<Void> ret = null;
        try 
            ret = response.writeAndFlushWith(Flux.just(ByteBufFlux.just(response.bufferFactory().wrap(params.toJSONString().getBytes("UTF-8")))));
         catch (UnsupportedEncodingException e0) 
            e0.printStackTrace();
        
        return ret;
    

修改SecurityConfig配制

将上面所写的配制到SpringSecurity 中:

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfig 

    @Autowired
    UserDetailService userDetailService;

    @Autowired
    AuthManagerHandler authManagerHandler;

    @Autowired
    AccessDeniedHandler accessDeniedHandler;

    @Autowired
    LoginSuccessHandler loginSuccessHandler;

    @Autowired
    LoginFailedHandler loginFailedHandler;

    @Autowired
    LoginLoseHandler loginLoseHandler;

    @Autowired
    JwtSecurityContextRepository jwtSecurityContextRepository;

    @Autowired
    JwtWebFilter jwtWebFilter;

    //security的鉴权排除列表
    private static final String[] excludedAuthPages = 
            "/auth/login",
            "/auth/logout"
    ;

    @Bean
    public ReactiveAuthenticationManager authenticationManager() 
        UserDetailsRepositoryReactiveAuthenticationManager authenticationManager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailService);
        authenticationManager.setPasswordEncoder(passwordEncoder());
        return authenticationManager;
    

    @Bean
    PasswordEncoder passwordEncoder() 
        return new BCryptPasswordEncoder();
    

    @Bean
    SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception 
        http.authorizeExchange()
                .pathMatchers(excludedAuthPages).permitAll()  //无需进行权限过滤的请求路径
                .pathMatchers(HttpMethod.OPTIONS).permitAll() //o
                .pathMatchers("/**").access(authManagerHandler)
                .anyExchange().authenticated()
                .and()
                .addFilterAfter(jwtWebFilter, SecurityWebFiltersOrder.FIRST)
                .securityContextRepository(jwtSecurityContextRepository)
                .formLogin()
                .loginPage("/auth/login")
                .authenticationSuccessHandler(loginSuccessHandler)
                .authenticationFailureHandler(loginFailedHandler)
                .and().exceptionHandling().authenticationEntryPoint(loginLoseHandler)
                .and().exceptionHandling().accessDeniedHandler(accessDeniedHandler)
                .and().cors().disable().csrf().disable();
        return http.build();
    


三、效果演示

不登录,直接访问http://localhost:8080/admin/test,会提示登录失效。

登录http://localhost:8080/auth/login,获取返回的Token

下面使用返回的Token,再次测试上面的接口:

如果访问一个无权限的接口:http://localhost:8080/common/test


喜欢的小伙伴可以关注我的个人微信公众号,获取更多学习资料!

以上是关于SpringSecurity - WebFlux环境下整合JWT使用 Token 认证授权的主要内容,如果未能解决你的问题,请参考以下文章

SpringSecurity - WebFlux环境下实现用户动态认证

SpringSecurity系列4基于Spring Webflux集成SpringSecurity实现前后端分离无状态Rest API的权限控制原理分析

WebFlux 和 Spring Security 会碰出哪些火花?

WebFlux 和 Spring Security 会碰出哪些火花?

在 Spring WebFlux 中使用 Spring Security 实现身份验证的资源是啥

Spring Security WebFlux IP 白名单