这段时间在学习搭建基于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注销方案。

spring oauth2+JWT后端自动刷新access_token的更多相关文章

  1. Spring Security Jwt Token 自动刷新

    token的自动刷新 一.功能需求 二.功能分析 1.token 的生成 2.token 的自动延长 3.系统资源的保护 4.用户如何传递 token 三.实现思路 1.生成 token 和 refr ...

  2. spring cloud - config 属性自动刷新

    启动config-server,启动成功后就不需要在管了; 在config-client做些修改: 在使用的controller或service的类上加上一个注解@RefreshScope 在pom中 ...

  3. nodejs里的express自动刷新高级篇【转载】

    搬运自[简书:http://www.jianshu.com/p/2f923c8782c8]亲测可用哦! 最近在使用express框架及mongodb,由于前端和后端代码修改后都需要实现自动刷新功能,刚 ...

  4. node express4 + 前端自动刷新

    官网快速生成:http://www.expressjs.com.cn/starter/generator.html 1.安装  express 1.应用生成器工具 express-generator  ...

  5. SpringSecurity+Oauth2+Jwt实现toekn认证和刷新token

    简单描述:最近在处理鉴权这一块的东西,需求就是用户登录需要获取token,然后携带token访问接口,token认证成功接口才能返回正确的数据,如果访问接口时候token过期,就采用刷新token刷新 ...

  6. 【Spring Cloud & Alibaba 实战 | 总结篇】Spring Cloud Gateway + Spring Security OAuth2 + JWT 实现微服务统一认证授权和鉴权

    一. 前言 hi,大家好~ 好久没更文了,期间主要致力于项目的功能升级和问题修复中,经过一年时间的打磨,[有来]终于迎来v2.0版本,相较于v1.x版本主要完善了OAuth2认证授权.鉴权的逻辑,结合 ...

  7. Spring Security + OAuth2 + JWT 基本使用

    Spring Security + OAuth2 + JWT 基本使用 前面学习了 Spring Security 入门,现在搭配 oauth2 + JWT 进行测试. 1.什么是 OAuth2 OA ...

  8. Spring Oauth2 with JWT Sample

    https://www.javacodegeeks.com/2016/04/spring-oauth2-jwt-sample.html ******************************** ...

  9. spring cloud 使用spring cloud bus自动刷新配置

    Spring Cloud Bus提供了批量刷新配置的机制,它使用轻量级的消息代理(例如RabbitMQ.Kafka等)连接分布式系统的节点,这样就可以通过Spring Cloud Bus广播配置的变化 ...

随机推荐

  1. VulnHub CengBox2靶机渗透

    ​本文首发于微信公众号:VulnHub CengBox2靶机渗透,未经授权,禁止转载. 难度评级:☆☆☆☆官网地址:https://download.vulnhub.com/cengbox/CengB ...

  2. Codeforces Round #651 (Div. 2)

    感觉自己无可救药了. A题:找到小于等于n的两个不同的数的gcd最大是多少,显然是floort(n/2).设这两数是a * gcd, b * gcd然后gcd(a,b) = 1,那么gcd要尽量大,不 ...

  3. 如何通过Elasticsearch Scroll快速取出数据,构造pandas dataframe — Python多进程实现

    首先,python 多线程不能充分利用多核CPU的计算资源(只能共用一个CPU),所以得用多进程.笔者从3.7亿数据的索引,取200多万的数据,从取数据到构造pandas dataframe总共大概用 ...

  4. JavaScript基础初始时期分支(018)

    Init-Time Branching初始时期分支是一种用做优化的模式.如果某些条件在程序启动后就不再改变,那么我们就只需要在初始时期检查一次就可以了,而不是在每次 需要用到这些条件的时候都检查一次. ...

  5. Flutter —快速开发的IDE快捷方式

    老孟导读:这是老孟翻译的精品文章,文章所有权归原作者所有. 欢迎加入老孟Flutter交流群,每周翻译2-3篇付费文章,精彩不容错过. 原文地址:https://medium.com/flutter- ...

  6. 要想数组用的 6,怎能不懂 java.util.Arrays

    java.util.Arrays 类就是为数组而生的专用工具类,基本上常见的对数组的操作,Arrays 类都考虑到了,这让我由衷地觉得,是时候给该类的作者 Josh Bloch.Neal Gafter ...

  7. CSS粘性定位

    粘性定位(position:sticky) 1.定义 粘性定位可以被认为是相对定位和固定定位的混合.元素在跨越特定阈值前为相对定位,之后为固定定位.(MDN传送门) 这个特定阈值指的是 top, ri ...

  8. 每日一题 - 剑指 Offer 50. 第一个只出现一次的字符

    题目信息 时间: 2019-07-03 题目链接:Leetcode tag:哈希表 难易程度:简单 题目描述: 在字符串 s 中找出第一个只出现一次的字符.如果没有,返回一个单空格. s 只包含小写字 ...

  9. HDU 5963 朋友 (找规律,思维)

    HDU 5963 朋友 题目大意 B君在围观一群男生和一群女生玩游戏,具体来说游戏是这样的: 给出一棵n个节点的树,这棵树的每条边有一个权值,这个权值只可能是0或1. 在一局游戏开始时,会确定一个节点 ...

  10. Sightseeing,题解

    题目: 题意: 找到从s到t与最短路长度相差少于1的路径总数. 分析: 首先,搞明白题意之后,我们来考虑一下怎么处理这个1,怎样找相差为1的路径呢?我们这样想,如果有相差为1的路径,那么它将会是严格的 ...