九、Spring Reactive Security使用JWT
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能不能访问。