最近有一些历史遗留bug需要我去解决,其中有一个bug是这样的:
我们项目中有一个控制台配置可以配置页面的退出登录时长,可填写范围为10分钟~720分钟。当填写720分钟时系统页面应该在720分钟之后退出登录,且当有请求访问后台时就会自动将页面的退出登录时长自动续期到720分钟,也就是说只要不是白名单放行的心跳检测轮询的请求,都会给页面退出登录时长续期,简单看一下怎么实现这个操作的。
/**
* 登陆认证
*/
@Slf4j
@Service("loginService")
public class LoginServiceImpl implements LoginService {
// 省略...
/**
* 处理redis中的认证数据
*/
private void processRedisAuthInfo(AuthenticationInfo authenticationInfo) {
AuthToken authToken = authenticationInfo.getAuthToken();
String accessToken = authToken.getAccessToken();
String refreshToken = authToken.getRefreshToken();
UserEntity userInfo = authenticationInfo.getUserInfo();
// 页面控制时间(分钟)
// 页面自动退出登录时长为默认720分钟
int overTime = this.readOrWriteDefaultOnConsoleConfig().getOverTime();
long pageOverTimeLong = (long) overTime * 1000 * 60;
long nowMilliseconds = LocalDateUtils.getNowMilliseconds();
String overMillTimeStr = String.valueOf(nowMilliseconds + pageOverTimeLong);
String pageOverTimeWithTokenKey = RedisKeyUtil.getPageOverTimeWithTokenKey(accessToken);
redisTemplate.opsForValue().set(pageOverTimeWithTokenKey, overMillTimeStr, pageOverTimeLong, TimeUnit.MILLISECONDS);
// 全局的过期时长配置 用于延长redis存储的token有效时长,不配置过期时间,永不过期
String globalPageOverTimeKey = RedisKeyUtil.getGlobalPageOverTimeKey();
redisTemplate.opsForValue().set(globalPageOverTimeKey, String.valueOf(pageOverTimeLong));
// 用户信息(包括角色和策略)存储到redis 过期时间为页面过期时长
String authAccessTokenKey = RedisKeyUtil.getAuthAccessTokenKey(accessToken);
redisTemplate.opsForValue().set(authAccessTokenKey, JSON.toJSONString(authenticationInfo), pageOverTimeLong, TimeUnit.MILLISECONDS);
// 用户认证信息(用于刷新token使用) 过期时间JWT过期两倍的时长
String userId = userInfo.getId();
Integer expiresIn = authToken.getExpiresIn();
int refreshExpiresIn = expiresIn * 2;
String refreshUserAuthenticationKey = RedisKeyUtil.getUserAuthenticationKey(userId);
redisTemplate.opsForValue().set(refreshUserAuthenticationKey, JSON.toJSONString(authenticationInfo), refreshExpiresIn, TimeUnit.SECONDS);
String refreshTokenKey = RedisKeyUtil.getAuthRefreshTokenKey(refreshToken);
redisTemplate.opsForValue().set(refreshTokenKey, JSON.toJSONString(authenticationInfo), refreshExpiresIn, TimeUnit.SECONDS);
// 存储用户的登陆token
String userLoginTokenKey = RedisKeyUtil.getUserLoginTokenKey(userId);
redisTemplate.opsForSet().add(userLoginTokenKey, accessToken);
redisTemplate.expire(userLoginTokenKey, expiresIn, TimeUnit.SECONDS);
recordLoginState(userInfo.getName(), expiresIn);
}
// 省略...
}
从请求的Cookie中获取 token,通过 token 获取redis中账号退出登录时长的key,并修改该key的过期时长为用户设置的过期时长,同时修改全局配置的页面过期时长key的value值为用户设置的过期时长。
@ApiOperation("更新控制台配置")
@PostMapping("/consoleConfig")
public ApiResponse updateConsoleConfig(@RequestBody ConsoleConfig consoleConfig,HttpServletRequest request) throws IOException, BizException {
consoleSettingService.setConsoleConfig(consoleConfig);
int newOverTimeMilliSeconds = consoleConfig.getOverTime() * 60 * 1000;
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if (cookie.getName().equalsIgnoreCase(OAuth2AccessToken.ACCESS_TOKEN)) {
String accessToken = cookie.getValue();
// 修改控制页面自动退出登录key的过期时间
long nowMillSecondes = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
String pageMillTimeOutStr = String.valueOf(nowMillSecondes + newOverTimeMilliSeconds);
String pageOverTimeWithTokenKey = String.format(Constants.PageOverTime.PAGE_OVER_TIME_WITH_TOKEN, accessToken);
redisTemplate.opsForValue().set(pageOverTimeWithTokenKey, pageMillTimeOutStr, newOverTimeMilliSeconds, TimeUnit.MILLISECONDS);
// 修改用户token过期时间为页面过期时长
String authAccessTokenKey = String.format(Constants.NgsocAuth.ACCESS_TOKEN, accessToken);
redisTemplate.expire(authAccessTokenKey, newOverTimeMilliSeconds, TimeUnit.MILLISECONDS);
}
}
// 修改全局配置的页面超时时间
String globalPageOverTimeKey = Constants.PageOverTime.GLOBAL_PAGE_OVER_TIME;
redisTemplate.opsForValue().set(globalPageOverTimeKey, String.valueOf(newOverTimeMilliSeconds));
return ApiResponse.newInstance(ApiResponse.CODE_OK, I18nUtils.i18n("save.success.response"));
}
系统使用的是SpringSecurity Oauth2框架做认证授权中心,当请求访问系统受限资源时都会判断请求携带的token是否正确,所以可以自定义CustomTokenExtractor继承BearerTokenExtractor,在提取到token后,通过token获取redis中存储的控制页面退出登录时长的key和用于延长token过期时长的key,将控制页面过期时长的key的过期时间重新设置为延长token过期时长的key的value值。(value值就是用户填写的系统过期时长,如果没有经过控制台操作默认就是720分钟)。
@Slf4j
public class CustomTokenExtractor extends BearerTokenExtractor {
private RequestMatcher requestMatcher;
@Autowired
private RedisTemplate<String, String> redisTemplate;
private Long defualtPageTimeout = 10L;
public void setWhiteUrls(String... urls) {
List<RequestMatcher> requestMatcherList = new ArrayList<>(urls.length);
for (String url : urls) {
requestMatcherList.add(new AntPathRequestMatcher(url));
}
this.requestMatcher = new OrRequestMatcher(requestMatcherList);
}
@Override
protected String extractToken(HttpServletRequest request) {
String token = null;
// 从cookie获取 确保tokenCookie.setHttpOnly(true)
Cookie[] cookies = request.getCookies();
if (Objects.nonNull(cookies)) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(OAuth2AccessToken.ACCESS_TOKEN)) {
token = cookie.getValue();
break;
}
}
}
// 从header获取
if (StringUtils.isEmpty(token)) {
log.debug("Token not found in cookies. Trying request header.");
token = extractHeaderToken(request);
}
// 从parameters获取
if (StringUtils.isEmpty(token)) {
log.debug("Token not found in headers. Trying request parameters.");
token = request.getParameter(OAuth2AccessToken.ACCESS_TOKEN);
}
if (StringUtils.isEmpty(token)) {
log.debug("Token not found in headers and request parameters and cookie. Not an OAuth2 request.");
return null;
}
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, OAuth2AccessToken.BEARER_TYPE);
return returnToken(request, token);
}
private String returnToken(HttpServletRequest request, String token) {
// 白名单url,直接返回null,绕过OAuth2AuthenticationProcessingFilter的校验
if (requestMatcher != null && requestMatcher.matches(request)) {
return null;
}
return freshTimeAndGetRedisToken(request, token);
}
private String freshTimeAndGetRedisToken(HttpServletRequest request, String accessToken) {
// 获取页面超时时长
String pageOverTimeWithTokenKey = String.format(Constants.PageOverTime.PAGE_OVER_TIME_WITH_TOKEN, accessToken);
String expTimeMilliSecondsStr = redisTemplate.opsForValue().get(pageOverTimeWithTokenKey);
if (StringUtils.isBlank(expTimeMilliSecondsStr)) {
return null;
}
String globalPageOverTimeKey = Constants.PageOverTime.GLOBAL_PAGE_OVER_TIME;
String pageTimeout = redisTemplate.opsForValue().get(globalPageOverTimeKey);
// 配置默认的页面超时时间
if (StringUtils.isBlank(pageTimeout)) {
String defualtPageTimeoutStr = String.valueOf(defualtPageTimeout * 60 * 1000);
redisTemplate.opsForValue().set(globalPageOverTimeKey, defualtPageTimeoutStr);
pageTimeout = redisTemplate.opsForValue().get(globalPageOverTimeKey);
}
long pageTimeoutMilliSeconds = Long.parseLong(pageTimeout);
long expTimeMilliSeconds = Long.parseLong(expTimeMilliSecondsStr);
long nowMillSecondes = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
String authAccessTokenKey = String.format(Constants.NgsocAuth.ACCESS_TOKEN, accessToken);
if (expTimeMilliSeconds - nowMillSecondes > 0) {
String uri = request.getRequestURI().trim();
// 匹配自动刷新接口,不重置 页面过期时间
if (!Constants.CommonAutoUrl.COMMON_AUTO_URL.contains(uri.toUpperCase())) {
String pageMillTimeOutStr = String.valueOf(nowMillSecondes + pageTimeoutMilliSeconds);
redisTemplate.opsForValue().set(pageOverTimeWithTokenKey, pageMillTimeOutStr, pageTimeoutMilliSeconds, TimeUnit.MILLISECONDS);
// 修改用户token页面过期时长
redisTemplate.expire(authAccessTokenKey, pageTimeoutMilliSeconds, TimeUnit.MILLISECONDS);
}
return accessToken;
} else {
redisTemplate.delete(authAccessTokenKey);
removeUserLoginState(request);
}
return null;
}
// ....
}
但是现在出现了一个bug,当用户设置720分钟之后,页面并没有在720分钟之后退出登录,而是在60分钟后就退出登录了,查看redis中控制页面退出登录时长的key和存储用户信息的key,他们的过期时间并没有到期,还有11个小时才能过期,那为什么提前退出登录了呢?再次使用 access_token访问后台接口报错:登录凭证已过期,请重新登录。
于是开始跟踪源码进行debug,访问后台的任何一个接口进入系统。
请求被OAuth2AuthenticationProcessingFilter过滤器拦截执行该过滤器的doFilter方法:
@Deprecated
public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean {
// ...
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
boolean debug = logger.isDebugEnabled();
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
try {
// 1、提取请求中的token
Authentication authentication = this.tokenExtractor.extract(request);
if (authentication == null) {
if (this.stateless && this.isAuthenticated()) {
if (debug) {
logger.debug("Clearing security context.");
}
SecurityContextHolder.clearContext();
}
if (debug) {
logger.debug("No token in request, will continue chain.");
}
} else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken)authentication;
needsDetails.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
// 调用认证管理器的authenticate方法执行认证
Authentication authResult = this.authenticationManager.authenticate(authentication);
if (debug) {
logger.debug("Authentication success: " + authResult);
}
this.eventPublisher.publishAuthenticationSuccess(authResult);
SecurityContextHolder.getContext().setAuthentication(authResult);
}
} catch (OAuth2Exception var9) {
SecurityContextHolder.clearContext();
if (debug) {
logger.debug("Authentication request failed: " + var9);
}
this.eventPublisher.publishAuthenticationFailure(new BadCredentialsException(var9.getMessage(), var9), new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
// 3、异常处理
this.authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(var9.getMessage(), var9));
return;
}
chain.doFilter(request, response);
}
// ...
}
在该方法中:
第一步:从请求中提取Token,会调用自定义CustomTokenExtractor类的提取token方法;
第二步:调用认证管理器AuthenticationManager的authenticate方法对token进行认证;
发现在执行第二步时抛出了异常,进入第三步,所以继续debug进入查看AuthenticationManager的authenticate方法。
@Deprecated
public class OAuth2AuthenticationManager implements AuthenticationManager, InitializingBean {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
} else {
// 1、获取认证主体acces_token
String token = (String)authentication.getPrincipal();
// 2、通过acces_token加载认证类OAuth2Authentication
OAuth2Authentication auth = this.tokenServices.loadAuthentication(token);
if (auth == null) {
throw new InvalidTokenException("Invalid token: " + token);
} else {
Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
if (this.resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(this.resourceId)) {
throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + this.resourceId + ")");
} else {
this.checkClientDetails(auth);
if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)authentication.getDetails();
if (!details.equals(auth.getDetails())) {
details.setDecodedDetails(auth.getDetails());
}
}
auth.setDetails(authentication.getDetails());
auth.setAuthenticated(true);
return auth;
}
}
}
}
}
在该方法中:
第一步:获取认证主体acces_token;
第二步:调用 TokenService 的 loadAuthentication(token) 方法通过acces_token 加载认证类 OAuth2Authentication;
@Deprecated
public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices, ConsumerTokenServices, InitializingBean {
public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException, InvalidTokenException {
// 1、通过accessTokenValue获取OAuth2AccessToken类
OAuth2AccessToken accessToken = this.tokenStore.readAccessToken(accessTokenValue);
if (accessToken == null) {
// 2、抛出异常
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
} else if (accessToken.isExpired()) {
this.tokenStore.removeAccessToken(accessToken);
throw new InvalidTokenException("Access token expired: " + accessTokenValue);
} else {
OAuth2Authentication result = this.tokenStore.readAuthentication(accessToken);
if (result == null) {
throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
} else {
if (this.clientDetailsService != null) {
String clientId = result.getOAuth2Request().getClientId();
try {
this.clientDetailsService.loadClientByClientId(clientId);
} catch (ClientRegistrationException var6) {
throw new InvalidTokenException("Client not valid: " + clientId, var6);
}
}
return result;
}
}
}
}
debug到这儿,让我找到了bug的所在原因,通过accessTokenValue获取OAuth2AccessToken类时结果为null,为什么会使null呢?于是继续向下debug。
@Deprecated
public class RedisTokenStore implements TokenStore {
private static final String ACCESS = "access:";
private static final String AUTH_TO_ACCESS = "auth_to_access:";
private static final String AUTH = "auth:";
private static final String REFRESH_AUTH = "refresh_auth:";
private static final String ACCESS_TO_REFRESH = "access_to_refresh:";
private static final String REFRESH = "refresh:";
private static final String REFRESH_TO_ACCESS = "refresh_to_access:";
private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:";
private static final String UNAME_TO_ACCESS = "uname_to_access:";
private static final boolean springDataRedis_2_0 = ClassUtils.isPresent("org.springframework.data.redis.connection.RedisStandaloneConfiguration", RedisTokenStore.class.getClassLoader());
private final RedisConnectionFactory connectionFactory;
private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();
private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy();
private String prefix = "";
private Method redisConnectionSet_2_0;
public OAuth2AccessToken readAccessToken(String tokenValue) {
byte[] key = this.serializeKey("access:" + tokenValue);
byte[] bytes = null;
RedisConnection conn = this.getConnection();
byte[] bytes;
try {
bytes = conn.get(key);
} finally {
conn.close();
}
OAuth2AccessToken var5 = this.deserializeAccessToken(bytes);
return var5;
}
public OAuth2Authentication readAuthentication(String token) {
byte[] bytes = null;
RedisConnection conn = this.getConnection();
byte[] bytes;
try {
bytes = conn.get(this.serializeKey("auth:" + token));
} finally {
conn.close();
}
OAuth2Authentication var4 = this.deserializeAuthentication(bytes);
return var4;
}
}
可以看到在该方法中会去 redis 中读取 access:tokenValue 这个key,这个是SpringSecurity Oauth2框架中在请求oauth/token进行登录认证时生成的access_token访问令牌,该令牌会存入redis中,默认过期时间为1小时。
所以问题就找到了,虽然控制页面退出登录时长的key还没有过期,但是SpringSecurity Oauth2 在登录认证时生成的access_token 的过期时长我60分钟,因此即使还没到720分钟,页面就提前退出登录了。
那如何解决呢?
首先我们系统登录成功后,页面退出登录的默认过期时长就是720分钟,如果用户没有在控制台配置页面的过期时长,那么系统就应该在720分钟之后退出登录,不能提前退出登录,所以不能让SpringSecurity Oauth2认证时生成的access_token提前过期,应该让两者的过期时长一致都为720分钟。
在授权服务器配置类中配置SpringSecurity Oauth2认证时生成的access_token的过期时长:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
// ...
/**
* 使用redis存储token
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(tokenStore)
.authenticationManager(authenticationManager)
// 设置userDetailsService刷新token时候会用到
.userDetailsService(refreshTokenUserDetailService);
// 修改默认令牌生成服务
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
// 是否支持刷新令牌
tokenServices.setSupportRefreshToken(true);
// 是否重复使用刷新令牌(直到过期)
tokenServices.setReuseRefreshToken(true);
// 访问令牌的默认有效期,720分钟
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(12));
// 刷新令牌的有效性,720分钟
tokenServices.setRefreshTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(12));
// 使用配置令牌服务
endpoints.tokenServices(tokenServices);
}
// ...
}
当在控制台修改了页面的退出登录时长,那么但SpringSecurity Oauth2认证时生成的access_token的过期时长也要修改,这个过期时长要和页面自动退出登录的时长一致。
在控制台配置页面退出登录的过期时长时,同时修改SpringSecurity Oauth2认证时生成的access_token的过期时长为配置的时长:
@ApiOperation("更新控制台配置")
@PostMapping("/consoleConfig")
public ApiResponse updateConsoleConfig(@RequestBody ConsoleConfig consoleConfig,HttpServletRequest request) throws IOException, BizException {
consoleSettingService.setConsoleConfig(consoleConfig);
int newOverTimeMilliSeconds = consoleConfig.getOverTime() * 60 * 1000;
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if (cookie.getName().equalsIgnoreCase(OAuth2AccessToken.ACCESS_TOKEN)) {
String accessToken = cookie.getValue();
// 修改控制页面自动退出登录key的过期时间
long nowMillSecondes = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
String pageMillTimeOutStr = String.valueOf(nowMillSecondes + newOverTimeMilliSeconds);
String pageOverTimeWithTokenKey = String.format(Constants.PageOverTime.PAGE_OVER_TIME_WITH_TOKEN, accessToken);
redisTemplate.opsForValue().set(pageOverTimeWithTokenKey, pageMillTimeOutStr, newOverTimeMilliSeconds, TimeUnit.MILLISECONDS);
// 修改用户token过期时间为页面过期时长
String authAccessTokenKey = String.format(Constants.NgsocAuth.ACCESS_TOKEN, accessToken);
// 修改SpringSecurity Oauth2认证时生成的auth:access_token和acces:access_token的key的过期时长
redisTemplate.expire(authAccessTokenKey, newOverTimeMilliSeconds, TimeUnit.MILLISECONDS);
redisTemplate.expire("auth:"+accessToken,newOverTimeMilliSeconds, TimeUnit.MILLISECONDS);
redisTemplate.expire("access:"+accessToken,newOverTimeMilliSeconds, TimeUnit.MILLISECONDS);
}
}
// 修改全局配置的页面超时时间
String globalPageOverTimeKey = Constants.PageOverTime.GLOBAL_PAGE_OVER_TIME;
redisTemplate.opsForValue().set(globalPageOverTimeKey, String.valueOf(newOverTimeMilliSeconds));
return ApiResponse.newInstance(ApiResponse.CODE_OK, I18nUtils.i18n("save.success.response"));
}
我们的页面退出登录时长是动态延长的,只要用户请求后台系统就会自动延长为控制台用户设置的过期时长,假如用户设置的过期时长为720分钟,在还有10分钟系统就要退出登录的时候,用户访问了后台的系统,那么页面的退出登录时长就会又延长到720分钟后退出。但SpringSecurity Oauth2认证时生成的access_token的过期时长为720分钟是不变的,不会自动延长。因此我们也需要设置成自动续期的。这个续期时长要和页面自动退出登录的时长一致。
@Slf4j
public class CustomTokenExtractor extends BearerTokenExtractor {
private RequestMatcher requestMatcher;
@Autowired
private RedisTemplate<String, String> redisTemplate;
private Long defualtPageTimeout = 10L;
public void setWhiteUrls(String... urls) {
List<RequestMatcher> requestMatcherList = new ArrayList<>(urls.length);
for (String url : urls) {
requestMatcherList.add(new AntPathRequestMatcher(url));
}
this.requestMatcher = new OrRequestMatcher(requestMatcherList);
}
@Override
protected String extractToken(HttpServletRequest request) {
String token = null;
// 从cookie获取 确保tokenCookie.setHttpOnly(true)
Cookie[] cookies = request.getCookies();
if (Objects.nonNull(cookies)) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(OAuth2AccessToken.ACCESS_TOKEN)) {
token = cookie.getValue();
break;
}
}
}
// 从header获取
if (StringUtils.isEmpty(token)) {
log.debug("Token not found in cookies. Trying request header.");
token = extractHeaderToken(request);
}
// 从parameters获取
if (StringUtils.isEmpty(token)) {
log.debug("Token not found in headers. Trying request parameters.");
token = request.getParameter(OAuth2AccessToken.ACCESS_TOKEN);
}
if (StringUtils.isEmpty(token)) {
log.debug("Token not found in headers and request parameters and cookie. Not an OAuth2 request.");
return null;
}
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, OAuth2AccessToken.BEARER_TYPE);
return returnToken(request, token);
}
private String returnToken(HttpServletRequest request, String token) {
// 白名单url,直接返回null,绕过OAuth2AuthenticationProcessingFilter的校验
if (requestMatcher != null && requestMatcher.matches(request)) {
return null;
}
return freshTimeAndGetRedisToken(request, token);
}
private String freshTimeAndGetRedisToken(HttpServletRequest request, String accessToken) {
// 获取页面超时时长
String pageOverTimeWithTokenKey = String.format(Constants.PageOverTime.PAGE_OVER_TIME_WITH_TOKEN, accessToken);
String expTimeMilliSecondsStr = redisTemplate.opsForValue().get(pageOverTimeWithTokenKey);
if (StringUtils.isBlank(expTimeMilliSecondsStr)) {
return null;
}
String globalPageOverTimeKey = Constants.PageOverTime.GLOBAL_PAGE_OVER_TIME;
String pageTimeout = redisTemplate.opsForValue().get(globalPageOverTimeKey);
// 配置默认的页面超时时间
if (StringUtils.isBlank(pageTimeout)) {
String defualtPageTimeoutStr = String.valueOf(defualtPageTimeout * 60 * 1000);
redisTemplate.opsForValue().set(globalPageOverTimeKey, defualtPageTimeoutStr);
pageTimeout = redisTemplate.opsForValue().get(globalPageOverTimeKey);
}
long pageTimeoutMilliSeconds = Long.parseLong(pageTimeout);
long expTimeMilliSeconds = Long.parseLong(expTimeMilliSecondsStr);
long nowMillSecondes = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
String authAccessTokenKey = String.format(Constants.NgsocAuth.ACCESS_TOKEN, accessToken);
if (expTimeMilliSeconds - nowMillSecondes > 0) {
String uri = request.getRequestURI().trim();
// 匹配自动刷新接口,不重置 页面过期时间
if (!Constants.CommonAutoUrl.COMMON_AUTO_URL.contains(uri.toUpperCase())) {
String pageMillTimeOutStr = String.valueOf(nowMillSecondes + pageTimeoutMilliSeconds);
redisTemplate.opsForValue().set(pageOverTimeWithTokenKey, pageMillTimeOutStr, pageTimeoutMilliSeconds, TimeUnit.MILLISECONDS);
// 修改用户token页面过期时长
redisTemplate.expire(authAccessTokenKey, pageTimeoutMilliSeconds, TimeUnit.MILLISECONDS);
redisTemplate.expire("auth:"+accessToken,pageTimeoutMilliSeconds, TimeUnit.MILLISECONDS);
redisTemplate.expire("access:"+accessToken,pageTimeoutMilliSeconds, TimeUnit.MILLISECONDS);
freshUserLoginState(request, pageTimeoutMilliSeconds);
}
return accessToken;
} else {
redisTemplate.delete(authAccessTokenKey);
removeUserLoginState(request);
}
return null;
}
/**
* 刷新当前用户的登陆状态
*/
private void freshUserLoginState(HttpServletRequest request, Long pageTimeout) {
String account = ClientUtils.getClientAccount(request);
if (StringUtils.isNotBlank(account)) {
String loginStateKey = Constants.USER_LOGIN_STATUS_PREFIX + account;
redisTemplate.opsForValue().set(loginStateKey, Constants.USER_LOGIN_ONLINE, pageTimeout, TimeUnit.MILLISECONDS);
}
}
/**
* 清除用户的登陆状态
*/
private void removeUserLoginState(HttpServletRequest request) {
String account = ClientUtils.getClientAccount(request);
if (StringUtils.isNotBlank(account)) {
String loginStateKey = Constants.USER_LOGIN_STATUS_PREFIX + account;
redisTemplate.delete(loginStateKey);
}
}
}