九、Spring Reactive Security使用JWT

发布时间 2023-06-10 15:58:05作者: shigp1

JWT之前说过了,可以参考 https://www.cnblogs.com/shigongp/p/17454635.html

 

使用jwt的思路:AuthenticationWebFilter认证成功后生成TOKEN,并通过响应头写回到客户端。新增一个WebFilter校验TOKEN。
 

添加依赖:

<dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.0</version>
    </dependency>

    <dependency>
        <groupId>javax.xml.bind</groupId>
        <artifactId>jaxb-api</artifactId>
        <version>2.3.1</version>
    </dependency>

application.properties添加配置:

token.expire=3600000
token.key=123456HJKsdsf,';dfs

 

添加TokenManager:

public interface TokenManager {
    public String createToken(String username);

    public String getUserFromToken(String token);
}


@Service
@Slf4j
public class JwtTokenManager implements TokenManager {

    @Value("${token.expire}")
    private long tokenExpiration = 3600;
    @Value("${token.key}")
    private String tokenSignKey;

    @Override
    public String createToken(String username) {
        String token = Jwts.builder().setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                .signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
        log.info("用户:{}生成token:{}", username, token);
        return token;
    }


    @Override
    public String getUserFromToken(String token) {
        String user = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
        log.info("从token:{}解析的用户名:{}", token, user);
        return user;
    }
}

修改AuthenticationWebFilter:

@Bean
public AuthenticationWebFilter authenticationManager() {
    AuthenticationWebFilter authenticationFilter = new AuthenticationWebFilter(userDetailsRepositoryReactiveAuthenticationManager());
    authenticationFilter.setRequiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, LOGIN_PAGE));
    authenticationFilter.setAuthenticationFailureHandler((webFilterExchange, exception) -> {
        log.info("认证异常",exception);
        ServerWebExchange exchange = webFilterExchange.getExchange();

        exchange.getResponse().getHeaders().add("Content-type", MediaType.APPLICATION_JSON_UTF8_VALUE);

        Flux<DataBuffer> dataBufferFlux = DataBufferUtils.read(new ByteArrayResource("认证失败".getBytes(StandardCharsets.UTF_8)), exchange.getResponse().bufferFactory(), 1024 * 8);

        return exchange.getResponse().writeAndFlushWith(t -> {
            t.onSubscribe(new Subscription() {
                @Override
                public void request(long l) {

                }

                @Override
                public void cancel() {

                }
            });
            t.onNext(dataBufferFlux);
            t.onComplete();
        });
    });
    authenticationFilter.setAuthenticationConverter(new JsonServerAuthenticationConverter());
    authenticationFilter.setAuthenticationSuccessHandler((webFilterExchange, authentication) -> {
        log.info("认证成功:{}",authentication);
        ServerWebExchange exchange = webFilterExchange.getExchange();

        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        String token = tokenManager.createToken(userDetails.getUsername());

        exchange.getResponse().getHeaders().add("MY_TOKEN", token);
        exchange.getResponse().getHeaders().add("Content-type", MediaType.APPLICATION_JSON_UTF8_VALUE);

        Flux<DataBuffer> dataBufferFlux = DataBufferUtils.read(new ByteArrayResource("认证成功".getBytes(StandardCharsets.UTF_8)), exchange.getResponse().bufferFactory(), 1024 * 8);

        return exchange.getResponse().writeAndFlushWith(t -> {
            t.onSubscribe(new Subscription() {
                @Override
                public void request(long l) {

                }

                @Override
                public void cancel() {

                }
            });
            t.onNext(dataBufferFlux);
            t.onComplete();
        });
    });
    authenticationFilter.setSecurityContextRepository(NoOpServerSecurityContextRepository.getInstance());
    return authenticationFilter;
}

将SecurityContextRepository设置成NoOpServerSecurityContextRepository,替换默认的WebSessionServerSecurityContextRepository。不需要将认证信息保存到WebSession中了。

 

SecurityWebFilterChain添加配置(只粘部分代码):

.exceptionHandling()
.authenticationEntryPoint(((exchange, ex) -> {

  exchange.getResponse().getHeaders().add("Content-type", MediaType.APPLICATION_JSON_UTF8_VALUE);

  Flux<DataBuffer> dataBufferFlux = DataBufferUtils.read(new ByteArrayResource("请先登录".getBytes(StandardCharsets.UTF_8)), exchange.getResponse().bufferFactory(), 1024 * 8);

  return exchange.getResponse().writeAndFlushWith(t -> {
      t.onSubscribe(new Subscription() {
          @Override
            public void request(long l) {

            }

            @Override
            public void cancel() {

            }
        });
        t.onNext(dataBufferFlux);
        t.onComplete();
    });
}))

用于处理未登录的情形。

 

添加token校验WebFilter:

@Slf4j
public class TokenFilter implements WebFilter {
    private TokenManager tokenManager;

    private ReactiveUserDetailsService reactiveUserDetailsService;



    public TokenFilter(TokenManager tokenManager, ReactiveUserDetailsService reactiveUserDetailsService){
        this.tokenManager = tokenManager;
        this.reactiveUserDetailsService = reactiveUserDetailsService;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {

        log.info("123");

        List<String> myToken = exchange.getRequest().getHeaders().get("MY_TOKEN");

        log.info("my_token:{}",myToken);

        return Mono.justOrEmpty(myToken)
                .map(s -> {
                    if (Objects.nonNull(s)) {
                       return s.get(0);
                    } else {
                      return null;
                    }
                })
                .filter(s -> s!=null)
                .map(s -> {
                    String username = tokenManager.getUserFromToken(s);
                    if (username == null || username.trim().equals("")) {
                        throw new RuntimeException("非法TOKEN");
                    }
                    return username;
                })
                .flatMap(t -> {
                    Mono<UserDetails> userDetailsMono = reactiveUserDetailsService.findByUsername(t);
                    userDetailsMono.filter(tt -> Objects.isNull(t)).map(tt -> {
                        throw new RuntimeException("非法TOKEN");
                    }).subscribe();
                    return userDetailsMono;
                })
                .map(t -> {
                    SecurityContextImpl securityContext = new SecurityContextImpl();
                    securityContext.setAuthentication(UsernamePasswordAuthenticationToken.authenticated(t, t.getPassword(),t.getAuthorities()));
                    log.info("userDetail:{}",securityContext);
                    return Mono.just(securityContext);
                })
                .switchIfEmpty(Mono.defer(() ->  chain.filter(exchange)).then(Mono.empty()))
                .flatMap(t ->  chain.filter(exchange).subscriberContext(ReactiveSecurityContextHolder.withSecurityContext(t)));
    }

}

从TOKEN取出用户名并加载UserDetails到ReactiveSecurityContextHolder中。注意:switchIfEmpty(Mono.defer(() -> chain.filter(exchange)).then(Mono.empty()))是必须的,相当于始终都会调用chain.filter(exchange)。

 

将TokenFilter添加到SecurityWebFilterChain中:

 @Bean
public TokenFilter tokenFilter() {
    int order = SecurityWebFiltersOrder.FORM_LOGIN.getOrder();
    TokenFilter tokenFilter = new TokenFilter(tokenManager, reactiveUserDetailsService);
    return tokenFilter;
}

修改SecurityWebFilterChain:

 http.addFilterAt(authenticationManager(), SecurityWebFiltersOrder.FORM_LOGIN);
 http.addFilterAt(tokenFilter(), SecurityWebFiltersOrder.FORM_LOGIN);

 

用Postman获取验证码后在调用登录接口获取TOKEN,在使用TOKEN去访问controller。试下删除TOKEN能不能访问。