feat:实现异步缓存支持
This commit is contained in:
parent
c127e275c6
commit
097c6210ed
|
@ -1,13 +1,11 @@
|
||||||
package com.flyfish.framework.configuration.jwt;
|
package com.flyfish.framework.configuration.jwt;
|
||||||
|
|
||||||
import com.flyfish.framework.service.MongoUserDetailsService;
|
import com.flyfish.framework.service.MongoUserDetailsService;
|
||||||
import com.flyfish.framework.utils.RedisOperations;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContext;
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@ -21,8 +19,6 @@ public class JwtSecurityContextRepository implements ServerSecurityContextReposi
|
||||||
private MongoUserDetailsService userDetailsService;
|
private MongoUserDetailsService userDetailsService;
|
||||||
@Resource
|
@Resource
|
||||||
private TokenProvider tokenProvider;
|
private TokenProvider tokenProvider;
|
||||||
@Resource
|
|
||||||
private RedisOperations redisOperations;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<SecurityContext> load(ServerWebExchange serverWebExchange) {
|
public Mono<SecurityContext> load(ServerWebExchange serverWebExchange) {
|
||||||
|
@ -30,9 +26,9 @@ public class JwtSecurityContextRepository implements ServerSecurityContextReposi
|
||||||
String jwt = tokenProvider.retrieveToken(serverWebExchange).orElse(null);
|
String jwt = tokenProvider.retrieveToken(serverWebExchange).orElse(null);
|
||||||
URI requestURI = request.getURI();
|
URI requestURI = request.getURI();
|
||||||
// 存在jwt时,校验jwt。redis也需要存在
|
// 存在jwt时,校验jwt。redis也需要存在
|
||||||
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt) && redisOperations.hasKey(jwt)) {
|
return tokenProvider.validateToken(jwt).flatMap(authorized -> {
|
||||||
|
if (authorized) {
|
||||||
// token即将过期,续租
|
// token即将过期,续租
|
||||||
|
|
||||||
Authentication authentication = tokenProvider.getAuthentication(jwt);
|
Authentication authentication = tokenProvider.getAuthentication(jwt);
|
||||||
log.debug("set Authentication to security context for '{}', uri: {}", authentication.getName(), requestURI);
|
log.debug("set Authentication to security context for '{}', uri: {}", authentication.getName(), requestURI);
|
||||||
return userDetailsService.findByUsername(authentication.getName())
|
return userDetailsService.findByUsername(authentication.getName())
|
||||||
|
@ -41,6 +37,7 @@ public class JwtSecurityContextRepository implements ServerSecurityContextReposi
|
||||||
log.debug("no valid JWT token found, uri: {}", requestURI);
|
log.debug("no valid JWT token found, uri: {}", requestURI);
|
||||||
return Mono.empty();
|
return Mono.empty();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package com.flyfish.framework.configuration.jwt;
|
package com.flyfish.framework.configuration.jwt;
|
||||||
|
|
||||||
import com.flyfish.framework.domain.base.IUser;
|
import com.flyfish.framework.utils.ReactiveRedisOperations;
|
||||||
import com.flyfish.framework.utils.RedisOperations;
|
import com.flyfish.framework.utils.UUIDUtils;
|
||||||
import io.jsonwebtoken.*;
|
import io.jsonwebtoken.*;
|
||||||
import io.jsonwebtoken.io.Decoders;
|
import io.jsonwebtoken.io.Decoders;
|
||||||
import io.jsonwebtoken.io.DecodingException;
|
import io.jsonwebtoken.io.DecodingException;
|
||||||
|
@ -18,9 +18,11 @@ import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.userdetails.User;
|
import org.springframework.security.core.userdetails.User;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
@ -37,12 +39,13 @@ public class TokenProvider implements InitializingBean {
|
||||||
|
|
||||||
public static final String AUTHORIZATION_HEADER = "Authorization";
|
public static final String AUTHORIZATION_HEADER = "Authorization";
|
||||||
private static final String AUTHORITIES_KEY = "auth";
|
private static final String AUTHORITIES_KEY = "auth";
|
||||||
|
private static final String TOKEN_BLOCK_PREFIX = "tk_blk_";
|
||||||
private final Boolean remember;
|
private final Boolean remember;
|
||||||
private final long tokenValidityInMilliseconds;
|
private final long tokenValidityInMilliseconds;
|
||||||
private final long tokenValidityInMillisecondsForRememberMe;
|
private final long tokenValidityInMillisecondsForRememberMe;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private RedisOperations redisOperations;
|
private ReactiveRedisOperations reactiveRedisOperations;
|
||||||
@Resource
|
@Resource
|
||||||
private JwtProperties jwtProperties;
|
private JwtProperties jwtProperties;
|
||||||
|
|
||||||
|
@ -89,39 +92,50 @@ public class TokenProvider implements InitializingBean {
|
||||||
* @param authentication 认证信息
|
* @param authentication 认证信息
|
||||||
*/
|
*/
|
||||||
public void addToken(ServerWebExchange exchange, Authentication authentication) {
|
public void addToken(ServerWebExchange exchange, Authentication authentication) {
|
||||||
IUser user = (IUser) authentication.getPrincipal();
|
|
||||||
String token = createToken(authentication, remember);
|
String token = createToken(authentication, remember);
|
||||||
HttpHeaders headers = exchange.getResponse().getHeaders();
|
HttpHeaders headers = exchange.getResponse().getHeaders();
|
||||||
// app用户从头部返回,方便获取
|
// app用户从头部返回,方便获取
|
||||||
headers.add("Token", token);
|
headers.add("Token", token);
|
||||||
headers.add("Token-Valid-Time", String.valueOf(remember ? tokenValidityInMillisecondsForRememberMe :
|
headers.add("Token-Valid-Time", String.valueOf(remember ? tokenValidityInMillisecondsForRememberMe :
|
||||||
tokenValidityInMilliseconds));
|
tokenValidityInMilliseconds));
|
||||||
// token在web端的时间较短,不允许记住,所以使用短期
|
|
||||||
// exchange.getResponse().addCookie(ResponseCookie.from(AUTHORIZATION_HEADER, "Bearer-" + token).
|
|
||||||
// httpOnly(true).maxAge(tokenValidityInMilliseconds).build());
|
|
||||||
// redis存储时间长
|
|
||||||
redisOperations.set(token, true, tokenValidityInMillisecondsForRememberMe);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeToken(ServerWebExchange exchange) {
|
/**
|
||||||
retrieveToken(exchange).ifPresent(token -> redisOperations.del(token));
|
* 移除token
|
||||||
|
*
|
||||||
|
* @param exchange 交换报文
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
public Mono<Void> removeToken(ServerWebExchange exchange) {
|
||||||
|
return Mono.justOrEmpty(retrieveToken(exchange))
|
||||||
|
.map(this::parseToken)
|
||||||
|
.flatMap(token -> {
|
||||||
|
long distance = token.getExpiration().getTime() - System.currentTimeMillis();
|
||||||
|
String key = getCacheKey(token.getId());
|
||||||
|
return reactiveRedisOperations.set(key, true, Duration.ofMillis(distance)).then();
|
||||||
|
})
|
||||||
|
.onErrorResume(e -> Mono.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建token
|
||||||
|
*
|
||||||
|
* @param authentication 认证信息
|
||||||
|
* @param rememberMe 记住我标记
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
public String createToken(Authentication authentication, boolean rememberMe) {
|
public String createToken(Authentication authentication, boolean rememberMe) {
|
||||||
String authorities = authentication.getAuthorities().stream()
|
String authorities = authentication.getAuthorities().stream()
|
||||||
.map(GrantedAuthority::getAuthority)
|
.map(GrantedAuthority::getAuthority)
|
||||||
.collect(Collectors.joining(","));
|
.collect(Collectors.joining(","));
|
||||||
|
|
||||||
long now = (new Date()).getTime();
|
long now = (new Date()).getTime();
|
||||||
Date validity;
|
long duration = rememberMe ? this.tokenValidityInMillisecondsForRememberMe : this.tokenValidityInMilliseconds;
|
||||||
if (rememberMe) {
|
Date validity = new Date(now + duration);
|
||||||
validity = new Date(now + this.tokenValidityInMillisecondsForRememberMe);
|
|
||||||
} else {
|
|
||||||
validity = new Date(now + this.tokenValidityInMilliseconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Jwts.builder()
|
return Jwts.builder()
|
||||||
.setSubject(authentication.getName())
|
.setSubject(authentication.getName())
|
||||||
|
.setId(UUIDUtils.generateShortUuid())
|
||||||
.claim(AUTHORITIES_KEY, authorities)
|
.claim(AUTHORITIES_KEY, authorities)
|
||||||
.signWith(key, SignatureAlgorithm.HS512)
|
.signWith(key, SignatureAlgorithm.HS512)
|
||||||
.setExpiration(validity)
|
.setExpiration(validity)
|
||||||
|
@ -135,11 +149,7 @@ public class TokenProvider implements InitializingBean {
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
public Authentication getAuthentication(String token) {
|
public Authentication getAuthentication(String token) {
|
||||||
Claims claims = Jwts.parserBuilder()
|
Claims claims = parseToken(token);
|
||||||
.setSigningKey(key)
|
|
||||||
.build()
|
|
||||||
.parseClaimsJws(token)
|
|
||||||
.getBody();
|
|
||||||
|
|
||||||
Collection<? extends GrantedAuthority> authorities =
|
Collection<? extends GrantedAuthority> authorities =
|
||||||
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
|
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
|
||||||
|
@ -151,10 +161,31 @@ public class TokenProvider implements InitializingBean {
|
||||||
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
|
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean validateToken(String authToken) {
|
/**
|
||||||
|
* 解析token
|
||||||
|
*
|
||||||
|
* @param token 用户token、
|
||||||
|
* @return 结果
|
||||||
|
*/
|
||||||
|
public Claims parseToken(String token) {
|
||||||
|
return Jwts.parserBuilder()
|
||||||
|
.setSigningKey(key)
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getCacheKey(String jti) {
|
||||||
|
return TOKEN_BLOCK_PREFIX + jti;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Boolean> validateToken(String authToken) {
|
||||||
|
if (StringUtils.isBlank(authToken)) {
|
||||||
|
return Mono.just(false);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(authToken);
|
Claims claims = parseToken(authToken);
|
||||||
return true;
|
return reactiveRedisOperations.hasKey(getCacheKey(claims.getId())).map(exists -> !exists);
|
||||||
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
|
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
|
||||||
log.info("Invalid JWT signature.");
|
log.info("Invalid JWT signature.");
|
||||||
log.trace("Invalid JWT signature trace: {}", e, e);
|
log.trace("Invalid JWT signature trace: {}", e, e);
|
||||||
|
@ -171,6 +202,6 @@ public class TokenProvider implements InitializingBean {
|
||||||
log.info("Token解析失败!");
|
log.info("Token解析失败!");
|
||||||
log.trace("Token解析失败: {}", e, e);
|
log.trace("Token解析失败: {}", e, e);
|
||||||
}
|
}
|
||||||
return false;
|
return Mono.just(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,13 +30,13 @@ public class JsonLogoutSuccessHandler implements ServerLogoutSuccessHandler {
|
||||||
public Mono<Void> onLogoutSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
|
public Mono<Void> onLogoutSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
|
||||||
ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
|
ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
|
||||||
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
|
||||||
tokenProvider.removeToken(webFilterExchange.getExchange());
|
Mono<Void> removed = tokenProvider.removeToken(webFilterExchange.getExchange());
|
||||||
Object principal = authentication.getPrincipal();
|
Object principal = authentication.getPrincipal();
|
||||||
if ("anonymous".equals(principal)) {
|
if ("anonymous".equals(principal)) {
|
||||||
return response.writeWith(authenticationAuditor.transform(Result.ok()));
|
return removed.then(response.writeWith(authenticationAuditor.transform(Result.ok())));
|
||||||
}
|
}
|
||||||
// 记录日志
|
// 记录日志
|
||||||
return authenticationAuditor.logout((UserDetails) authentication.getPrincipal())
|
return removed.then(authenticationAuditor.logout((UserDetails) authentication.getPrincipal())
|
||||||
.then(response.writeWith(authenticationAuditor.transform(Result.ok())));
|
.then(response.writeWith(authenticationAuditor.transform(Result.ok()))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,7 +139,24 @@ public class ReactiveRedisOperations {
|
||||||
*/
|
*/
|
||||||
public Mono<Boolean> set(String key, Object value, long time) {
|
public Mono<Boolean> set(String key, Object value, long time) {
|
||||||
if (time > 0) {
|
if (time > 0) {
|
||||||
return redisTemplate.opsForValue().set(key, value, Duration.of(time, ChronoUnit.SECONDS))
|
return redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(time))
|
||||||
|
.doOnError(Throwable::printStackTrace).onErrorReturn(false);
|
||||||
|
} else {
|
||||||
|
return set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 普通缓存放入并设置时间
|
||||||
|
*
|
||||||
|
* @param key 键
|
||||||
|
* @param value 值
|
||||||
|
* @param duration 时间(秒) time要大于0 如果time小于等于0 将设置无限期
|
||||||
|
* @return true成功 false 失败
|
||||||
|
*/
|
||||||
|
public Mono<Boolean> set(String key, Object value, Duration duration) {
|
||||||
|
if (duration.getSeconds() > 0) {
|
||||||
|
return redisTemplate.opsForValue().set(key, value, duration)
|
||||||
.doOnError(Throwable::printStackTrace).onErrorReturn(false);
|
.doOnError(Throwable::printStackTrace).onErrorReturn(false);
|
||||||
} else {
|
} else {
|
||||||
return set(key, value);
|
return set(key, value);
|
||||||
|
|
Loading…
Reference in New Issue