项目使用InMemoryTokenStore时,token有效期设置与强制清除某客户端持有的token

本文详细介绍OAuth2中Token的有效期设置及管理方法,包括如何通过客户端详情服务自定义Token的有效时间,以及如何在客户端被删除时清理其关联的所有Token。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 设置token有效期

    在使用InMemoryTokenStore(token存储在内存)token生成策略时,系统默认的token的有效时间是12小时。

从oauth源码的默认token生成方法中,可以看出

public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices,
		ConsumerTokenServices, InitializingBean {

	private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.

	private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.
.............................
        private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
             DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
             int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
             if (validitySeconds > 0) {
                  token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
             }
             token.setRefreshToken(refreshToken);
             token.setScope(authentication.getOAuth2Request().getScope());

             return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
        }
      
       /**
        * The access token validity period in seconds
        * @param clientAuth the current authorization request
        * @return the access token validity period in seconds
        */
       protected int getAccessTokenValiditySeconds(OAuth2Request clientAuth) {
         if (clientDetailsService != null) {
             ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId());
             Integer validity = client.getAccessTokenValiditySeconds();
             if (validity != null) {
                 return validity;
             }
         }
         return accessTokenValiditySeconds;
      }
 ......................

}
从上面官方源码我们可以了解到2个事情

1. 在token对象里面包含了token的过期时间

2. ClientDetails 中的AccessTokenValiditySeconds字段可以指定token的有效期

目前我已经有一个自定义的客户端验证服务类。那么可以在验证客户端对象时,直接调用AccessTokenValiditySeconds的set方法,设置token有效期,这个时间的单位是秒

我的代码:

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;

import bap.core.config.util.spring.SpringContextHolder;
import bap.core.dao.BaseDao;
import bap.pp.core.config.item.domain.ConfigItem;
import bap.pp.strongbox.client.domain.ClientDetail;

/**
 * 客户端身份验证
 * @author Amanda.Z
 *
 */
public class ClientDetailConfig implements ClientDetailsService{

	private BaseDao baseDao;
	
	public ClientDetailConfig() {
		baseDao=SpringContextHolder.getBean(BaseDao.class);
	}
	
	/* 
	 * 根据客户端clientid检查客户端有效性,如果有效,则封装为oauth2客户端对象
	 * @see org.springframework.security.oauth2.provider.ClientDetailsService#loadClientByClientId(java.lang.String)
	 */
	@Override
	public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
		BaseClientDetails details=new BaseClientDetails();
		ClientDetail client=(ClientDetail) this.baseDao.getUniqueResultByHql("from ClientDetail where clientName=? and deleted=0",clientId);
		if(client!=null){
			//设置客户端id
			details.setClientId(client.getClientName());
			//客户端私钥
			details.setClientSecret(client.getClientSecret());
			//受保护资源id
			List<String> resourceList=new ArrayList<>();
			resourceList.add(client.getResource().getResourceKey());
			details.setResourceIds(resourceList);
			//oauth2保护模式,本项目默认全部是客户端认证模式
			List<String> authorizedGrantTypes=new ArrayList<>();
			authorizedGrantTypes.add("client_credentials");
			details.setAuthorizedGrantTypes(authorizedGrantTypes);
			//客户端传入的参数
			List<String> scopList=new ArrayList<>();
			scopList.add("view");
			scopList.add(client.getScope());
			details.setScope(scopList);
			//设置此客户端持有的用户组
			Collection<GrantedAuthority> auths = new ArrayList<GrantedAuthority>();
			if(bap.util.StringUtil.isNotEmpty(client.getAuthoritites())){
				String [] authArray=client.getAuthoritites().split(",");
				for (String auth : authArray) {
					auths.add(new SimpleGrantedAuthority(auth));
				}
			}
			details.setAuthorities(auths);
			//设置token有效时间,单位是秒,(如果不设置,框架内部默认是12小时,本平台设置默认2小时)
			details.setAccessTokenValiditySeconds(Integer.parseInt(ConfigItem.TokenValiditySeconds.getVal()));
		}
		return details;
	}

}

注意:这里通过ClientDail设置进去的token有效时间,只要有效时间没有过,不管这个时候,客户端是否被删除,都不会影响这个token的访问。

      这个时候 ,产生了一个明显的问题,认证端管理员创建了某个客户端账户,并分配了权限,这个客户端获取了一个token,如果这个时候管理员希望把这个客户端删除,理论上应该连带这个客户端产生的token也删除。

      接下来,介绍如何解决上面的问题,在自己的服务类中,删除某个客户端账户产生的token数据

      首先还是先看源码,我使用的是InMemoryTokenStore,其中已经具备了删除token的方法:

public class InMemoryTokenStore implements TokenStore {

	private static final int DEFAULT_FLUSH_INTERVAL = 1000;

	private final ConcurrentHashMap<String, OAuth2AccessToken> accessTokenStore = new ConcurrentHashMap<String, OAuth2AccessToken>();

	private final ConcurrentHashMap<String, OAuth2AccessToken> authenticationToAccessTokenStore = new ConcurrentHashMap<String, OAuth2AccessToken>();

	private final ConcurrentHashMap<String, Collection<OAuth2AccessToken>> userNameToAccessTokenStore = new ConcurrentHashMap<String, Collection<OAuth2AccessToken>>();

	private final ConcurrentHashMap<String, Collection<OAuth2AccessToken>> clientIdToAccessTokenStore = new ConcurrentHashMap<String, Collection<OAuth2AccessToken>>();

	private final ConcurrentHashMap<String, OAuth2RefreshToken> refreshTokenStore = new ConcurrentHashMap<String, OAuth2RefreshToken>();

	private final ConcurrentHashMap<String, String> accessTokenToRefreshTokenStore = new ConcurrentHashMap<String, String>();

	private final ConcurrentHashMap<String, OAuth2Authentication> authenticationStore = new ConcurrentHashMap<String, OAuth2Authentication>();

	private final ConcurrentHashMap<String, OAuth2Authentication> refreshTokenAuthenticationStore = new ConcurrentHashMap<String, OAuth2Authentication>();

	private final ConcurrentHashMap<String, String> refreshTokenToAccessTokenStore = new ConcurrentHashMap<String, String>();

	private final DelayQueue<TokenExpiry> expiryQueue = new DelayQueue<TokenExpiry>();

	private final ConcurrentHashMap<String, TokenExpiry> expiryMap = new ConcurrentHashMap<String, TokenExpiry>();

	private int flushInterval = DEFAULT_FLUSH_INTERVAL;

	private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();

	private AtomicInteger flushCounter = new AtomicInteger(0);
        .............................
        //从内存中清除token记录
        public void removeAccessToken(OAuth2AccessToken accessToken) {
		removeAccessToken(accessToken.getValue());
	}

        //根据clientid获取其所有token
	public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) {
               Collection<OAuth2AccessToken> result = clientIdToAccessTokenStore.get(clientId);
                return result != null ? Collections.<OAuth2AccessToken> unmodifiableCollection(result) : Collections
                .<OAuth2AccessToken> emptySet();
        }
        //根据token值,在内存中移除这条token的记录
	public void removeAccessToken(String tokenValue) {
		OAuth2AccessToken removed = this.accessTokenStore.remove(tokenValue);
		this.accessTokenToRefreshTokenStore.remove(tokenValue);
		// Don't remove the refresh token - it's up to the caller to do that
		OAuth2Authentication authentication = this.authenticationStore.remove(tokenValue);
		if (authentication != null) {
			this.authenticationToAccessTokenStore.remove(authenticationKeyGenerator.extractKey(authentication));
			Collection<OAuth2AccessToken> tokens;
			String clientId = authentication.getOAuth2Request().getClientId();
			tokens = this.userNameToAccessTokenStore.get(getApprovalKey(clientId, authentication.getName()));
			if (tokens != null) {
				tokens.remove(removed);
			}
			tokens = this.clientIdToAccessTokenStore.get(clientId);
			if (tokens != null) {
				tokens.remove(removed);
			}
			this.authenticationToAccessTokenStore.remove(authenticationKeyGenerator.extractKey(authentication));
		}
	}
        ...............
}

      我们看到这些remove方法,入参要求传入token的,显然我并不知道这个客户端的token的值,我只有这个客户端的对象本身,那么,再看另外的方法findTokensByClientId通过这个方法,可以由clientid获取到这个客户端所有token值,也就是说,接下来,只需要调用这个方法,遍历结果集,逐个调用remove方法移除即可。

     事实上,真正坑爹的是,你得不到InMemoryTokensStrore这个类的实例对象,从Spring容器中,竟然获取不了,我试着用@autowride获取它,直接异常。这是,求助官网文档,但是连个P都没说。所以继续扣源码!!

     持续向上查找所有调用InMemoryTokenStore这个类中方法,并且能够提供对tokenStore的get方法的类,还要求能够从spring中获取得到这个类对象,终于!终于!在public final class AuthorizationServerEndpointsConfigurer 这个类里面我找到了tokenStore的get方法。


public final class AuthorizationServerEndpointsConfigurer {

	private AuthorizationServerTokenServices tokenServices;

	private ConsumerTokenServices consumerTokenServices;

	private AuthorizationCodeServices authorizationCodeServices;

	private ResourceServerTokenServices resourceTokenServices;

	private TokenStore tokenStore;

	private TokenEnhancer tokenEnhancer;

	private AccessTokenConverter accessTokenConverter;

	private ApprovalStore approvalStore;

	private TokenGranter tokenGranter;

	private OAuth2RequestFactory requestFactory;

	private OAuth2RequestValidator requestValidator;

	private UserApprovalHandler userApprovalHandler;

	private AuthenticationManager authenticationManager;

	private ClientDetailsService clientDetailsService;

	private String prefix;

	private Map<String, String> patternMap = new HashMap<String, String>();

	private Set<HttpMethod> allowedTokenEndpointRequestMethods = new HashSet<HttpMethod>();

	private FrameworkEndpointHandlerMapping frameworkEndpointHandlerMapping;

	private boolean approvalStoreDisabled;

	private List<Object> interceptors = new ArrayList<Object>();

	private DefaultTokenServices defaultTokenServices;

	private UserDetailsService userDetailsService;

	private boolean tokenServicesOverride = false;

	private boolean userDetailsServiceOverride = false;

	private boolean reuseRefreshToken = true;

	private WebResponseExceptionTranslator exceptionTranslator;

        ......................
        //记住这里先
        public boolean isUserDetailsServiceOverride() {
		return userDetailsServiceOverride;
	}
        .............
        public AuthorizationServerEndpointsConfigurer userDetailsService(UserDetailsService userDetailsService) {
		if (userDetailsService != null) {
			this.userDetailsService = userDetailsService;
			this.userDetailsServiceOverride = true;
		}
		return this;
	}
        .................

       //tokenStore是InMemoryTokenStore的父类,这里是关键,我们亲爱的get方法
        public TokenStore getTokenStore() {
		return tokenStore();
	}


那么如何获取AuthorizationServerEndpointsConfigurer,我尝试用@autowride注入我的服务类,然并卵。那么继续挖,在oauth2验证端的原始配置类中,找到了如下语句:

@Configuration
@Order(0)
@Import({ ClientDetailsServiceConfiguration.class, AuthorizationServerEndpointsConfiguration.class })
public class AuthorizationServerSecurityConfiguration extends WebSecurityConfigurerAdapter {

	@Autowired
	private List<AuthorizationServerConfigurer> configurers = Collections.emptyList();

	@Autowired
	private ClientDetailsService clientDetailsService;

	@Autowired
	private AuthorizationServerEndpointsConfiguration endpoints;

	@Autowired
	public void configure(ClientDetailsServiceConfigurer clientDetails) throws Exception {
		for (AuthorizationServerConfigurer configurer : configurers) {
			configurer.configure(clientDetails);
		}
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
		FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping();
		http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping);
		configure(configurer);
		http.apply(configurer);
		String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token");
		String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key");
		String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token");
		if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) {
			UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class);
                        //注意看这行代码
                        endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);
		}

      endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);看到这一行,你会明白,用这个endpoints.getEndpointsConfigurer(),你可以获取到AuthorizationServerEndpointsConfigurrer对象。那么这个endpoints这货,是怎么拿到的,是@autowride来的,他在spring容器!妥了这回!

      我的客户端全部都是自定义的,有自己的服务类,所以,现在我可以在我自己的服务类中的删除客户端方法中这样写:

@Autowired
private AuthorizationServerEndpointsConfiguration endpoints;

@Transactional
	public boolean delete(String[] ck_ids) {
		//执行逻辑删除操作
		for (String id : ck_ids) {
			ClientDetail detail=this.baseDao.get(ClientDetail.class, id);
			//移除这个客户端创建的所有token
			InMemoryTokenStore tokenStore=(InMemoryTokenStore) endpoints.getEndpointsConfigurer().getTokenStore();
			Collection<OAuth2AccessToken> tokens=tokenStore.findTokensByClientId(detail.getClientName());
			if(tokens!=null&&tokens.size()>0){
				for(OAuth2AccessToken accessToken:tokens){
					tokenStore.removeAccessToken(accessToken);
				}
			}
			//对客户端进行逻辑删除
			detail.setDeleted(1);	
			this.baseDao.update(detail);
		}
		return true;
	}


打完收工

<think>好的,我现在需要帮助用户解答关于在WebSocket中生成Token设置有效期应由前端还是后端负责的问题。用户之前已经询问过WebSocket如何设置Token有效期,现在进一步明确责任归属。 首先,我需要回顾之前的回答,确保这次回答一致。之前提到生成Token后端负责设置有效期,比如使用JWT的exp字段或Redis存储。现在用户的问题更具体,需要明确前端还是后端负责。 接下来,我需要分析用户的需求。用户可能是在设计系统架构,需要了解前后端职责划分,或者遇到相关问题需要解决。他们可能担心安全问题,或者想了解最佳实践。 然后,我要考虑技术细节。Token的生成和有效期设置通常涉及安全性和控制,后端处理更安全,因为前端容易被篡改。比如JWT的exp字段由后端生成,确保不可篡改。同,后端可以通过Redis等存储机制强制过期,即使Token未过期也能撤销访问权限。 还需要引用之前的参考资料,比如引用[1]中提到客户端发送UUID,但生成Token应由服务端处理。引用[2]中的扫码登录流程,服务端协调认证,这也支持后端负责Token的观点。 此外,用户可能担心前后端协作,比如Token刷新机制,是否需要前端参。但即使需要前端传递新Token,生成和有效期设置仍由后端处理。 最后,要确保回答结构清晰,分点说明原因,给出示例代码,并总结最佳实践。同,生成相关问题,帮助用户进一步理解。</think>#### 专业结论: **Token有效期设置必须完全由后端负责**,这是保障系统安全的核心设计原则。 --- ### 一、后端负责的三大核心原因 1. **防篡改保障** - 前端代码完全暴露在浏览器中,攻击者可以轻易修改本地间或伪造`exp`字段 - 示例:JWT签名验证机制 ```java // 后端验证JWT有效性(Spring Security示例) Claims claims = Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); if(claims.getExpiration().before(new Date())) { // 强制后端验证间戳 throw new TokenExpiredException(); } ``` [^4][^5] 2. **强制过期控制** - 即使Token未过期,后端可通过Redis等存储系统强制使其失效 ```python # Django通道示例:双重验证机制 async def websocket_connect(self, event): token = self.scope['query_string'].decode().split('=')[1] if not cache.get(f"valid_token:{token}"): # 检查Redis存储状态 await self.close(code=4001) ``` [^3][^5] 3. **钟同步问题** - 不同客户端设备的系统间可能存在偏差,后端统一使用服务器间保证准确性 --- ### 二、前后端协作模式 | 阶段 | 前端职责 | 后端职责 | |--------------|------------------------------|--------------------------------------| | Token生成 | 发送认证请求 | 生成带有效期Token并签名 | | Token传递 | 通过WebSocket协议头携带Token | 解码并校验有效期 | | Token刷新 | 接收过期通知 | 主动推送新Token/触发重新认证流程 | --- ### 三、具体实现方案对比 **方案1:JWT标准有效期(推荐)** ```javascript // 后端Node.js生成示例 const token = jwt.sign( { userId: 123, exp: Math.floor(Date.now() / 1000) + 3600 // 后端设置1小有效期 }, secretKey ); ``` [^4] **方案2:服务端存储控制** ```java // Spring Boot + Redis方案 public String generateToken(String userId) { String token = UUID.randomUUID().toString(); redisTemplate.opsForValue().set( "ws_token:" + userId, token, 1, TimeUnit.HOURS // 设置存储有效期 ); return token; } ``` [^5] --- ### 四、错误实现案例警示 ❌ **危险的前端做法**(绝对禁止): ```javascript // 前端伪造过期间(可被绕过) const fakeExp = Date.now() + 3600000; const fakeToken = { data: 'user123', exp: fakeExp }; localStorage.setItem('token', JSON.stringify(fakeToken)); ``` --- ### 五、最佳实践总结 1. **敏感操作隔离**:有效期计算、签名生成等操作必须运行在服务端环境 2. **双重验证机制**:同使用JWT过期间和服务端存储状态检查 3. **动态刷新策略**:在Token过期前通过WebSocket推送更新指令[^2] ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值