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注销方案。

  • 相关阅读:
    分布式配置中心选型
    springboot中后端返回数据给前端的设计模式
    java中泛型在静态方法中的使用
    java在开发中DO、DTO、BO、AO、VO、POJO定义
    linux中find命令的使用详解(转载)
    Centos7配置ssh免密登录群发
    关于oracle PL/SQL存储过程 PLS-00905 object is invalid,statement ignored问题的解决
    Django-ModelFrom中修改save后的字段值
    MongoDB的集群模式--Sharding(分片)
    MongoDB用户和密码登录
  • 原文地址:https://www.cnblogs.com/braska/p/13368284.html
Copyright © 2011-2022 走看看