zoukankan      html  css  js  c++  java
  • spring oauth2+JWT后端自动刷新access_token

    这段时间在学习搭建基于spring boot的spring oauth2 和jwt整合。

    说实话挺折腾的。使用jwt做用户鉴权,难点在于token的刷新和注销。

    当然注销的难度更大,网上的一些方案也没有很出色的。这个功能基本让我放弃了jwt(滑稽笑~)。

    所以今天我单纯的先记录jwt token的刷新。

    Token刷新

    jwt token刷新方案可以分为两种:一种是校验token前刷新,第二种是校验失败后刷新。

    我们先来说说第二种方案

    验证失效后,Oauth2框架会把异常信息发送到OAuth2AuthenticationEntryPoint类里处理。这时候我们可以在这里做jwt token刷新并跳转。

    网上大部分方案也是这种:失效后,使用refresh_token获取新的access_token。并将新的access_token设置到response.header然后跳转,前端接收并无感更新新的access_token。

    这里就不多做描述,可以参考这两篇:

    https://www.cnblogs.com/xuchao0506/p/13073913.html

    https://blog.csdn.net/m0_37834471/article/details/83213002

    接着说第一种,其实两种方案的代码我都写过,最终使用了第一种。原因是兼容其他token刷新方案。

    我在使用第二种方案并且jwt token刷新功能正常使用后,想换一种token方案做兼容。

    切换成memory token的时候,发现OAuth2AuthenticationEntryPoint里面拿不到旧的token信息导致刷新失败。

    我们翻一下源码

    DefaultTokenServices.java

    public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException,
    			InvalidTokenException {
    		OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);
    		if (accessToken == null) {
    			throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
    		}
    		else if (accessToken.isExpired()) {
    			// 失效后accessToken即被删除
    			tokenStore.removeAccessToken(accessToken);
    			throw new InvalidTokenException("Access token expired: " + accessTokenValue);
    		}
    
    		// 忽略部分代码
    		return result;
    	}
    

      

    可以看到JwtTokenStore的removeAccessToken:它是一个空方法,什么也没做。所以我们在OAuth2AuthenticationEntryPoint依然能拿到旧的token并作处理。

    但是其他的token策略在token过期后,被remove掉了。一点信息都没留下,巧妇难为无米之炊。所以,我之后选择选择了第一种方案,在token校验remove前做刷新处理。

    jwt token刷新的方案是这样的:

    客户端发送请求大部分只携带access_token,并不携带refresh_token、client_id及client_secret等信息。所以我是先把refresh_token、client_id等信息放到access_token里面。

    因为jwt并不具有续期的功能,所以在判断token过期后,立刻使用refresh_token刷新。并且在response的header里面添加标识告诉前端你的token实际上已经过期了需要更新。

    当然,其他的类似memory token、redis token可以延期的,更新策略就没这么复杂:直接延长过期时间并且不需要更新token。

    说了这么多,放token刷新相关代码:

    首先,我们需要把refresh_token、client_id、client_secret放入到access_token中,以便刷新。所以我们需要重写JwtAccessTokenConverter的enhance方法。

    OauthJwtAccessTokenConverter.java

    public class OauthJwtAccessTokenConverter extends JwtAccessTokenConverter {
        private JsonParser objectMapper = JsonParserFactory.create();
    
        public OauthJwtAccessTokenConverter(SecurityUserService userService) {
            // 使用SecurityContextHolder.getContext().getAuthentication()能获取到User信息
            super.setAccessTokenConverter(new OauthAccessTokenConverter(userService));
        }
    
        @Override
        public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
            DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
            Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
            String tokenId = result.getValue();
            if (!info.containsKey(TOKEN_ID)) {
                info.put(TOKEN_ID, tokenId);
            } else {
                tokenId = (String) info.get(TOKEN_ID);
            }
    
            // access_token 包含自动刷新过期token需要的数据(client_id/secret/refresh_token)
            Map<String, Object> details = (Map<String, Object>) authentication.getUserAuthentication().getDetails();
            if (!Objects.isNull(details) && details.size() > 0) {
                info.put(OauthConstant.OAUTH_CLIENT_ID,
                        details.getOrDefault("client_id", details.get(OauthConstant.OAUTH_CLIENT_ID)));
    
                info.put(OauthConstant.OAUTH_CLIENT_SECRET,
                        details.getOrDefault("client_secret", details.get(OauthConstant.OAUTH_CLIENT_SECRET)));
            }
    
            OAuth2RefreshToken refreshToken = result.getRefreshToken();
            if (refreshToken != null) {
                DefaultOAuth2AccessToken encodedRefreshToken = new DefaultOAuth2AccessToken(accessToken);
                encodedRefreshToken.setValue(refreshToken.getValue());
                // Refresh tokens do not expire unless explicitly of the right type
                encodedRefreshToken.setExpiration(null);
                try {
                    Map<String, Object> claims = objectMapper
                            .parseMap(JwtHelper.decode(refreshToken.getValue()).getClaims());
                    if (claims.containsKey(TOKEN_ID)) {
                        encodedRefreshToken.setValue(claims.get(TOKEN_ID).toString());
                    }
                } catch (IllegalArgumentException e) {
                }
                Map<String, Object> refreshTokenInfo = new LinkedHashMap<String, Object>(
                        accessToken.getAdditionalInformation());
                refreshTokenInfo.put(TOKEN_ID, encodedRefreshToken.getValue());
                // refresh token包含client id/secret, 自动刷新过期token时用到。
                if (!Objects.isNull(details) && details.size() > 0) {
                    refreshTokenInfo.put(OauthConstant.OAUTH_CLIENT_ID,
                            details.getOrDefault("client_id", details.get(OauthConstant.OAUTH_CLIENT_ID)));
    
                    refreshTokenInfo.put(OauthConstant.OAUTH_CLIENT_SECRET,
                            details.getOrDefault("client_secret", details.get(OauthConstant.OAUTH_CLIENT_SECRET)));
                }
                refreshTokenInfo.put(ACCESS_TOKEN_ID, tokenId);
                encodedRefreshToken.setAdditionalInformation(refreshTokenInfo);
                DefaultOAuth2RefreshToken token = new DefaultOAuth2RefreshToken(
                        encode(encodedRefreshToken, authentication));
                if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
                    Date expiration = ((ExpiringOAuth2RefreshToken) refreshToken).getExpiration();
                    encodedRefreshToken.setExpiration(expiration);
                    token = new DefaultExpiringOAuth2RefreshToken(encode(encodedRefreshToken, authentication), expiration);
                }
                result.setRefreshToken(token);
                info.put(OauthConstant.OAUTH_REFRESH_TOKEN, token.getValue());
            }
            result.setAdditionalInformation(info);
            result.setValue(encode(result, authentication));
            return result;
        }
    }
    

      

    信息准备好了,就要开始处理刷新。就是改写DefaultTokenServices的loadAuthentication方法。

    OauthTokenServices.java

    public class OauthTokenServices extends DefaultTokenServices {
        private static final Logger logger = LoggerFactory.getLogger(OauthTokenServices.class);
    
        private TokenStore tokenStore;
        // 自定义的token刷新处理器
        private TokenRefreshExecutor executor;
    
        public OauthTokenServices(TokenStore tokenStore, TokenRefreshExecutor executor) {
            super.setTokenStore(tokenStore);
            this.tokenStore = tokenStore;
            this.executor = executor;
        }
    
        @Override
        public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException, InvalidTokenException {
            OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);
            executor.setAccessToken(accessToken);
            // 是否刷新token
            if (executor.shouldRefresh()) {
                try {
                    logger.info("refresh token.");
                    String newAccessTokenValue = executor.refresh();
                    // token如果是续期不做remove操作,如果是重新生成则删除旧的token
                    if (!newAccessTokenValue.equals(accessTokenValue)) {
                        tokenStore.removeAccessToken(accessToken);
                    }
                    accessTokenValue = newAccessTokenValue;
                } catch (Exception e) {
                    logger.error("token refresh failed.", e);
                }
            }
    
            return super.loadAuthentication(accessTokenValue);
        }
    }
    

      

    类里面的TokenRefreshExecutor就是我们的重点。这个类定义了两个比较重要的接口。

    shouldRefresh:是否需要刷新

    refresh:刷新

    TokenRefreshExecutor.java

    public interface TokenRefreshExecutor {
    
        /**
         * 执行刷新
         * @return
         * @throws Exception
         */
        String refresh() throws Exception;
    
        /**
         * 是否需要刷新
         * @return
         */
        boolean shouldRefresh();
    
        void setTokenStore(TokenStore tokenStore);
    
        void setAccessToken(OAuth2AccessToken accessToken);
    
        void setClientService(ClientDetailsService clientService);
    }
    

     

    然后我们来看看jwt刷新器,

    OauthJwtTokenRefreshExecutor.java

    public class OauthJwtTokenRefreshExecutor extends AbstractTokenRefreshExecutor {
    
        private static final Logger logger = LoggerFactory.getLogger(OauthJwtTokenRefreshExecutor.class);
    
        @Override
        public boolean shouldRefresh() {
            // 旧token过期才刷新
            return getAccessToken() != null && getAccessToken().isExpired();
        }
    
        @Override
        public String refresh() throws Exception{
            HttpServletRequest request = ServletUtil.getRequest();
            HttpServletResponse response = ServletUtil.getResponse();
            MultiValueMap<String, Object> parameters = new LinkedMultiValueMap<>();
            // OauthJwtAccessTokenConverter中存入access_token中的数据,在这里使用
            parameters.add("client_id", TokenUtil.getStringInfo(getAccessToken(), OauthConstant.OAUTH_CLIENT_ID));
            parameters.add("client_secret", TokenUtil.getStringInfo(getAccessToken(), OauthConstant.OAUTH_CLIENT_SECRET));
            parameters.add("refresh_token", TokenUtil.getStringInfo(getAccessToken(), OauthConstant.OAUTH_REFRESH_TOKEN));
            parameters.add("grant_type", "refresh_token");
            // 发送刷新的http请求
            Map result = RestfulUtil.post(getOauthTokenUrl(request), parameters);
    
            if (Objects.isNull(result) || result.size() <= 0 || !result.containsKey("access_token")) {
                throw new IllegalStateException("refresh token failed.");
            }
    
            String accessToken = result.get("access_token").toString();
            OAuth2AccessToken oAuth2AccessToken = getTokenStore().readAccessToken(accessToken);
            OAuth2Authentication auth2Authentication = getTokenStore().readAuthentication(oAuth2AccessToken);
            // 保存授权信息,以便全局调用
            SecurityContextHolder.getContext().setAuthentication(auth2Authentication);
    
            // 前端收到该event事件时,更新access_token
            response.setHeader("event", "token-refreshed");
            response.setHeader("access_token", accessToken);
            // 返回新的token信息
            return accessToken;
        }
    
        private String getOauthTokenUrl(HttpServletRequest request) {
            return String.format("%s://%s:%s%s%s",
                    request.getScheme(),
                    request.getLocalAddr(),
                    request.getLocalPort(),
                    Strings.isNotBlank(request.getContextPath()) ? "/" + request.getContextPath() : "",
                    "/oauth/token");
        }
    }
    

      

    类写完了,开始使用。

    @Configuration
    public class TokenConfig {
    
        @Bean
        public TokenStore tokenStore(AccessTokenConverter converter) {
            return new JwtTokenStore((JwtAccessTokenConverter) converter);
            // return new InMemoryTokenStore();
        }
    
        @Bean
        public AccessTokenConverter accessTokenConverter(SecurityUserService userService) {
            JwtAccessTokenConverter accessTokenConverter = new OauthJwtAccessTokenConverter(userService);
            accessTokenConverter.setSigningKey("sign_key");
            return accessTokenConverter;
            /*DefaultAccessTokenConverter converter = new DefaultAccessTokenConverter();
            DefaultUserAuthenticationConverter userTokenConverter = new DefaultUserAuthenticationConverter();
            userTokenConverter.setUserDetailsService(userService);
            converter.setUserTokenConverter(userTokenConverter);
            return converter;*/
        }
        @Bean
        public TokenRefreshExecutor tokenRefreshExecutor(TokenStore tokenStore,
                                                         ClientDetailsService clientService) {
            TokenRefreshExecutor executor = new OauthJwtTokenRefreshExecutor();
            // TokenRefreshExecutor executor = new OauthTokenRefreshExecutor();
            executor.setTokenStore(tokenStore);
            executor.setClientService(clientService);
            return executor;
        }
    
        @Bean
        public AuthorizationServerTokenServices tokenServices(TokenStore tokenstore,
                                                              AccessTokenConverter accessTokenConverter,
                                                              ClientDetailsService clientService,
                                                              TokenRefreshExecutor executor) {
    
            OauthTokenServices tokenServices = new OauthTokenServices(tokenstore, executor);
            // 非jwtConverter可注释setTokenEnhancer
            tokenServices.setTokenEnhancer((TokenEnhancer) accessTokenConverter);
            tokenServices.setSupportRefreshToken(true);
            tokenServices.setClientDetailsService(clientService);
            tokenServices.setReuseRefreshToken(true);
            return tokenServices;
        }
    }
    

      

    然后是认证服务器相关代码

    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    
        @Autowired
        private AuthenticationManager manager;
        @Autowired
        private SecurityUserService userService;
        @Autowired
        private TokenStore tokenStore;
        @Autowired
        private AccessTokenConverter tokenConverter;
        @Autowired
        private AuthorizationServerTokenServices tokenServices;
    
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.tokenStore(tokenStore)
                    .authenticationManager(manager)
                    .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
                    .userDetailsService(userService)
                    .accessTokenConverter(tokenConverter)
                    .tokenServices(tokenServices);
        }
    
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            security.tokenKeyAccess("permitAll()") //url:/oauth/token_key,exposes public key for token verification if using JWT tokens
                    .checkTokenAccess("isAuthenticated()") //url:/oauth/check_token allow check token
                    .allowFormAuthenticationForClients();
        }
    
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.withClientDetails(clientDetailsService());
        }
    
        public ClientDetailsService clientDetailsService() {
            return new OauthClientService();
        }
    }
    

      

    接着是前端处理, 用的axios。

    service.interceptors.response.use(res => {
        // 缓存自动刷新生成的新token
        if (res.headers['event'] && "token-refreshed" === res.headers['event']) {
          setToken(res.headers['access_token'])
          store.commit('SET_TOKEN', res.headers['access_token'])
        }
        // 忽略部分代码
    }
    

      

    这样就做到了jwt无感刷新。

    讲完了jwt的token刷新,多嘴说说memory token的刷新。

    上面讲了,memory token刷新策略比较简单,每次请求过来直接给token延期即可。

    OauthTokenRefreshExecutor.java

    public class OauthTokenRefreshExecutor extends AbstractTokenRefreshExecutor {
        private int accessTokenValiditySeconds = 60 * 60 * 12;
    
        @Override
        public boolean shouldRefresh() {
            // 与jwt不同,因为每次请求都需要延长token失效时间,所以这里是token未过期时就需要刷新
            return getAccessToken() != null && !getAccessToken().isExpired();
        }
    
        @Override
        public String refresh() {
            int seconds;
            if (getAccessToken() instanceof DefaultOAuth2AccessToken) {
                // 获取client中的过期时间, 没有则默认12小时
                if (getClientService() != null) {
                    OAuth2Authentication auth2Authentication = getTokenStore().readAuthentication(getAccessToken());
                    String clientId = auth2Authentication.getOAuth2Request().getClientId();
                    ClientDetails client = getClientService().loadClientByClientId(clientId);
                    seconds = client.getAccessTokenValiditySeconds();
                } else {
                    seconds = accessTokenValiditySeconds;
                }
                // 只修改token失效时间
                ((DefaultOAuth2AccessToken) getAccessToken()).setExpiration(new Date(System.currentTimeMillis() + (seconds * 1000l)));
            }
            // 返回的还是旧的token
            return getAccessToken().getValue();
        }
    }
    

        

    然后修改TokenConfig相关bean注册即可。

    好了,Token刷新这块差不多就这样了。Token注销暂时没有好的思路。

    如果Token刷新有更好的方案可以告知,也欢迎分享Token注销方案。

  • 相关阅读:
    Java Output流写入包装问题
    SpringBoot项目单元测试不经过过滤器问题
    SpringSecurity集成启动报 In the composition of all global method configuration, no annotation support was actually activated 异常
    JWT jti和kid属性的说明
    Maven 排除依赖
    第五章 基因概念的发现
    第三章 孟德尔遗传的拓展
    第二章 孟德尔遗传
    第一章 引言
    GWAS全基因组关联分析
  • 原文地址:https://www.cnblogs.com/braska/p/13368284.html
Copyright © 2011-2022 走看看