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;
    }
}
编写登录失效的Hander
@Component
public class LoginLoseHandler extends HttpBasicServerAuthenticationEntryPoint {

    @Override
    public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
        JSONObject params = new JSONObject();
        params.put("code", 401);
        params.put("msg", "登录失效!");

        ServerHttpResponse response = exchange.getResponse();

        response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
        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
在这里插入图片描述

评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小毕超

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值