SpringSecurity Oauth2 - 11 只要请求访问后台接口就延长访问令牌过期时间

赖浩荡
2023-12-01

最近有一些历史遗留bug需要我去解决,其中有一个bug是这样的:

我们项目中有一个控制台配置可以配置页面的退出登录时长,可填写范围为10分钟~720分钟。当填写720分钟时系统页面应该在720分钟之后退出登录,且当有请求访问后台时就会自动将页面的退出登录时长自动续期到720分钟,也就是说只要不是白名单放行的心跳检测轮询的请求,都会给页面退出登录时长续期,简单看一下怎么实现这个操作的。

1. 认证登录时生成控制页面退出登录时长的Key存入Redis中

/**
 * 登陆认证
 */
@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);
    }
    
    // 省略... 
}

2. 控制台配置页面自动退出登录的时长

从请求的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"));
}

3. 当有请求访问后台接口时延长页面退出登录时长

系统使用的是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;
    }
    
    // ....
}

4. bug问题定位

但是现在出现了一个bug,当用户设置720分钟之后,页面并没有在720分钟之后退出登录,而是在60分钟后就退出登录了,查看redis中控制页面退出登录时长的key和存储用户信息的key,他们的过期时间并没有到期,还有11个小时才能过期,那为什么提前退出登录了呢?再次使用 access_token访问后台接口报错:登录凭证已过期,请重新登录。

于是开始跟踪源码进行debug,访问后台的任何一个接口进入系统。

1. 进入 OAuth2AuthenticationProcessingFilter#doFilter

请求被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方法。

2. 进入OAuth2AuthenticationManager#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;

3. 进入 DefaultTokenServices#loadAuthentication

@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。

4. 进入RedisTokenStore#readAccessToken

@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分钟,页面就提前退出登录了。

5. bug问题解决

那如何解决呢?

首先我们系统登录成功后,页面退出登录的默认过期时长就是720分钟,如果用户没有在控制台配置页面的过期时长,那么系统就应该在720分钟之后退出登录,不能提前退出登录,所以不能让SpringSecurity Oauth2认证时生成的access_token提前过期,应该让两者的过期时长一致都为720分钟。

1. 在授权服务器配置类中配置access_token的过期时长

在授权服务器配置类中配置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);
    }
    
     // ...
}

2. 控制台配置页面退出登录时长时同时配置access_token的过期时长

当在控制台修改了页面的退出登录时长,那么但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"));
}

3. 页面退出登录时长续期时同时给access_token的过期时长续期

我们的页面退出登录时长是动态延长的,只要用户请求后台系统就会自动延长为控制台用户设置的过期时长,假如用户设置的过期时长为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);
        }
    }
}
 类似资料: