背景
项目中使用security + OAuth2.0 作为用户登录鉴权,存在的问题就是token不能自动续期,如果我们自己不配置,token的默认有效期是12小时,一般情况我们会设置token的有效期,假如:设置token的有效期为1小时,用户在操作系统的时候,token到期后,在操作系统时提示token失效,用户体验很差;toekn有效时间设置过长,又不太安全,所以需要在用户操作系统时进行token续期操作
一、自定义tokenServices
- 通过请求打到服务上的调用链路的源码分析
FilterChainProxy类中,doFilter方法中的doFilterInternal方法,获取过所有滤器
这里有三组过滤器,通过请求匹配,匹配到合适的过滤器链,这里是匹配到第二个过滤器链,如果想看具体情况,自己可以debug进行源码分析
执行过滤器链上的过滤器,此过滤器链上有11个过滤器,分别执行,查看具体执行情况,这里就不一一演示,有兴趣的可以自己debug执行。
这里执行到第五个过滤器OAuth2AuthenticationProcessingFilter,我们发现这里对token进行了验证,authenticationManager.authenticate(authentication),在这个方法中获取用户的认证信息,这里是OAuth2AuthenticationManager这个类,认证管理器,这个类中有一个属性ResourceServerTokenServices,程序启动时创建OAuth2AuthenticationManager对象后,对ResourceServerTokenServices这个私有属性进行赋值,这里默认创建的对象为DefaultTokenServices
这里调用tokenServices的loadAuthentication()方法,获取用户认证信息,
经过这个链路的分析,我们可以判定,我们可以在这里进行token的续期操作,然而,我们要怎么做,才可以实现呢,首先重写这个loadAuthentication()方法才可以,我们通过这个方法,找到父类,我们自定义一个类:CustomTokenServices实现ResourceServerTokenServices这个接口,重写loadAuthentication()方法,我们重写这个方法,里面要怎么实现呢?经过代码分析,这里token信息是通过tokenStore(这里tokenStore的实现类我用的是redis)获取的,OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue),分析DefaultTokenServices这个类,这个类里有创建token的方法,createAccessToken()方法,这个方法里创建token后进行封装并进行存储,发现TokenStore这个接口中,提供了一个token存储的方法,见图1;思考一下,我们是不是封装好这个接口的上入参就可以通过调用这个接口进行重新设置token,对原来的token进行覆盖。我们又该如何封装这个两个参数呢?通过分析TokenStore发现,这里提供了两个方法readAccessToken()、readAuthentication(),见图2,可以通过token分别获取OAuth2AccessToken、OAuth2Authentication这两个对象,我们就可以通过请求中传进来的token获取到这两个对象,再调用图1中的方法,重新设置token的所有相关信息。我们分析图1的具体实现,这里是redis实现,可以发现,这里是设置了token相关的过期时间以及一些其他信息
图1
图2
token存储具体实现
- token续期的具体实现,自定义实现类CustomTokenServices并实现ResourceServerTokenServices接口
import com.anneng.financemember.commons.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.util.ObjectUtils;
import java.util.Date;
/**
1. @Description: 自定义用户信息token服务
**/
public class CustomTokenServices implements ResourceServerTokenServices {
protected final Logger log = LoggerFactory.getLogger(CustomTokenServices.class);
/**
* token存储
*/
private final TokenStore tokenStore;
/**
* token有效期 默认5小时
*/
private int accessTokenValiditySeconds;
private final int timeLimit = 60 * 60;
public CustomTokenServices(TokenStore tokenStore, int accessTokenValiditySeconds) {
this.tokenStore = tokenStore;
this.accessTokenValiditySeconds = accessTokenValiditySeconds;
}
@Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
//获取认证信息
OAuth2Authentication authentication = tokenStore.readAuthentication(accessToken);
if (ObjectUtils.isEmpty(authentication)) {
throw new InvalidTokenException("Invalid access token: " + accessToken);
}
//判断是否为账户密码token
String grantType = authentication.getOAuth2Request().getGrantType();
if (!Constants.GRANT_TYPE_PWD.equals(grantType)) {
return authentication;
}
//获取token信息
DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) tokenStore.readAccessToken(accessToken);
if (ObjectUtils.isEmpty(token) || token.isExpired()) {
throw new InvalidTokenException("Invalid access token: " + accessToken);
}
//为了防止每次操作都进行续期,判断在小于一定时间时续期,小于一个小时时续期
if (token.getExpiresIn() > timeLimit) {
return authentication;
}
//重置token时长
resetTokenTime(authentication, token);
return authentication;
}
private void resetTokenTime(OAuth2Authentication authentication, DefaultOAuth2AccessToken token) {
//延长token时长
token.setExpiration(new Date(token.getExpiration().getTime() + (accessTokenValiditySeconds - token.getExpiresIn()) * 1000L));
//刷新token时间
tokenStore.storeAccessToken(token, authentication);
}
@Override
public OAuth2AccessToken readAccessToken(String accessToken) {
throw new UnsupportedOperationException("Not supported: read access token");
}
}
这里我们就把整个链路分析及核心的代码实现就完成了。
二、资源服务配置
- 上面我们虽然实现了核心代码,但是如何让代码生效呢?那就要把请求链路中的默认创建的DefaultTokenServices对象,替换为我们自定义的类CustomTokenServices,改如何替换呢?
通过源码分析,找到ResourceServerConfiguration配置类,发现图3中对ResourceServerTokenServices进行了处理,进入到方法中发现:这里会获取spring容器中注入的ResourceServerTokenServices实现类,如果有则设置进去,如果没有则不设置;我们继续debug,发现后面对ResourceServerTokenServices进行了设置,我们debug到ResourceServerSecurityConfigurer这个类的configure()方法,如图4,在继续debug,直到图5我们发现,这里对resourceTokenServices进行了赋值,如果resourceTokenServices,则默认创建DefaultTokenServices,看到这里就全都明白了,所以需要我们把我们自定义的类注入的Spring容器中,让项目启动时在能获取到ResourceServerTokenServices的实现类,那就不会走创建DefaultTokenServices这一步,那么后续拿到的ResourceServerTokenServices的实现类就是我们自定义的。
图3
图4
图5
这里可以发现图5中resourceTokenServices为null,这里我们对我们自定义的类进行Spring容器管理
- 对资源服务配置修改
原来的实现
package com.anneng.financemember.config;
import com.anneng.financemember.oauth.CustomTokenServices;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
/**
* 白名单
*/
@Value("${token.white-list}")
private String[] whiteList;
/**
* token过期时间
*/
@Value("${token.validity-seconds:18000}")
private int validitySeconds;
/**
* 安全策略配置
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers(whiteList).permitAll()
.anyRequest().authenticated()
.and()
.headers().addHeaderWriter((request, response) -> {
response.addHeader("Access-Control-Allow-Origin", "*");//允许跨域
if (request.getMethod().equals("OPTIONS")) {//如果是跨域的预检请求,则原封不动向下传达请求头信息
response.setHeader("Access-Control-Allow-Methods", request.getHeader("Access-Control-Request-Method"));
response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
}
});
}
/**
* 资源服务器安全策略配置
* @param resources configurer for the resource server
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.tokenStore(tokenStore).resourceId("product_api");
}
}
修改后
package com.anneng.financemember.config;
import com.anneng.financemember.oauth.CustomTokenServices;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
/**
*
**/
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
/**
* 白名单
*/
@Value("${token.white-list}")
private String[] whiteList;
/**
* token过期时间
*/
@Value("${token.validity-seconds:18000}")
private int validitySeconds;
/**
* 安全策略配置
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers(whiteList).permitAll()
.anyRequest().authenticated()
.and()
.headers().addHeaderWriter((request, response) -> {
response.addHeader("Access-Control-Allow-Origin", "*");//允许跨域
if (request.getMethod().equals("OPTIONS")) {//如果是跨域的预检请求,则原封不动向下传达请求头信息
response.setHeader("Access-Control-Allow-Methods", request.getHeader("Access-Control-Request-Method"));
response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
}
});
}
/**
* 资源服务器安全策略配置
* @param resources configurer for the resource server
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.tokenStore(tokenStore).resourceId("product_api");
}
@Bean
public ResourceServerTokenServices ResourceServerTokenServices() {
return new CustomTokenServices(tokenStore, validitySeconds);
}
}
我们启动项目debug到图5的位置
这里我们发现已经把我们自定义的CustomTokenServices 已经注入到Spring容器中,好了到这里结束了。代码很简单,但是整个链路分析起来就很费时费力,有什么问题可以评论留言。