这段时间在学习搭建基于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注销方案。
gitee地址: https://gitee.com/syher/gravel ,核心代码都在gravel-kernel模块。
来源:oschina
链接:https://my.oschina.net/u/4368960/blog/4503648