Spring Boot OAuth2 REST服务:用Session ID跨节点共享ID Token
解决方案:基于WebSphere分布式Session实现OAuth2 ID Token跨节点共享
针对你在WebSphere部署Spring Boot OAuth2服务时遇到的Token跨节点丢失问题,我们可以利用WebSphere原生的分布式Session机制,将ID Token与用户Session绑定,实现无粘性负载均衡下的Token共享。以下是具体实现步骤:
1. 先配置WebSphere的分布式Session
首先要确保WebSphere启用了分布式会话管理,这样用户Session能在集群节点间同步:
- 登录WebSphere管理控制台,进入应用服务器 > [你的服务器名称] > 会话管理
- 勾选启用分布式会话,根据集群架构选择合适的会话复制方式(生产环境推荐数据库持久化,稳定性更高;测试环境可选用内存到内存复制)
- 保持会话跟踪方式为Cookie(默认配置即可,需确保负载均衡器会转发Cookie)
- 保存配置并重启应用服务器集群
2. 自定义Session-backed TokenStore实现
替换原来的InMemoryTokenStore,实现一个基于HttpSession的TokenStore,让Token存储在WebSphere的分布式Session中:
import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2RefreshToken; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpSession; import java.util.Collection; import java.util.HashSet; import java.util.Map; import java.util.Set; public class SessionTokenStore implements TokenStore { // Session中存储Token的键前缀 private static final String TOKEN_SESSION_KEY_PREFIX = "OAUTH2_TOKEN_"; private static final String AUTHENTICATION_SESSION_KEY_PREFIX = "OAUTH2_AUTH_"; @Override public OAuth2AccessToken readAccessToken(String tokenValue) { HttpSession session = getCurrentSession(); if (session == null) { return null; } return (OAuth2AccessToken) session.getAttribute(TOKEN_SESSION_KEY_PREFIX + tokenValue); } @Override public OAuth2Authentication readAuthentication(OAuth2AccessToken token) { return readAuthentication(token.getValue()); } @Override public OAuth2Authentication readAuthentication(String tokenValue) { HttpSession session = getCurrentSession(); if (session == null) { return null; } return (OAuth2Authentication) session.getAttribute(AUTHENTICATION_SESSION_KEY_PREFIX + tokenValue); } @Override public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) { HttpSession session = getCurrentSession(); if (session != null) { session.setAttribute(TOKEN_SESSION_KEY_PREFIX + token.getValue(), token); session.setAttribute(AUTHENTICATION_SESSION_KEY_PREFIX + token.getValue(), authentication); } } @Override public void removeAccessToken(OAuth2AccessToken token) { removeAccessToken(token.getValue()); } @Override public void removeAccessToken(String tokenValue) { HttpSession session = getCurrentSession(); if (session != null) { session.removeAttribute(TOKEN_SESSION_KEY_PREFIX + tokenValue); session.removeAttribute(AUTHENTICATION_SESSION_KEY_PREFIX + tokenValue); } } @Override public OAuth2RefreshToken readRefreshToken(String tokenValue) { // 若业务需要支持刷新Token,可参照AccessToken逻辑实现存储读取 return null; } @Override public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) { return null; } @Override public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) { // 若业务需要支持刷新Token,可参照AccessToken逻辑实现存储 } @Override public void removeRefreshToken(OAuth2RefreshToken token) { // 若业务需要支持刷新Token,可参照AccessToken逻辑实现移除 } @Override public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) { // 若业务需要支持刷新Token,可参照AccessToken逻辑实现关联移除 } @Override public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) { // 集群环境下该方法需结合数据库或全局存储实现,若业务无需求可返回空集合 return new HashSet<>(); } @Override public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) { // 同上,根据业务需求实现,无需求则返回空集合 return new HashSet<>(); } private HttpSession getCurrentSession() { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes == null) { return null; } return attributes.getRequest().getSession(false); // 不主动创建新Session } }
3. 修改Spring Boot OAuth2配置
在你的ResourceServerConfig中,替换原来的InMemoryTokenStore为自定义的SessionTokenStore:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; 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.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; @Configuration public class ResourceServerConfig { private static final String SERVER_RESOURCE_ID = "oauth2-server"; private static final Logger LOG = LoggerFactory.getLogger(ResourceServerConfig.class); // 替换为自定义的SessionTokenStore private static SessionTokenStore tokenStore = new SessionTokenStore(); @Autowired private UserDetailsService userDetailsService; @Autowired private AuthenticationManager authenticationManager; @Configuration @EnableResourceServer @Order(2) protected class ResourceServer extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { LOG.info("ResourceServerSecurityConfigurer"); resources.tokenStore(tokenStore).resourceId(SERVER_RESOURCE_ID); } @Override public void configure(HttpSecurity http) throws Exception { LOG.info("HttpSecurity"); http.anonymous().disable() .requestMatchers().antMatchers("/v1/**") .and().authorizeRequests().antMatchers("/v1/**").authenticated(); } } @Configuration @EnableAuthorizationServer @Order(1) protected class AuthConfig extends AuthorizationServerConfigurerAdapter { @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { LOG.info("AuthorizationServerEndpointsConfigurer"); endpoints.userDetailsService(userDetailsService) .authenticationManager(authenticationManager) .tokenStore(tokenStore) // 使用自定义TokenStore .approvalStoreDisabled(); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { LOG.info("ClientDetailsServiceConfigurer: {}", clients); clients.inMemory() .withClient("client") .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit", "client_credentials") .authorities("ROLE_CLIENT") .scopes("read") .resourceIds(SERVER_RESOURCE_ID) .secret("secret"); } } }
4. 关键注意事项
- 会话超时匹配:确保WebSphere的Session超时时间与OAuth2 Token的有效期保持一致,避免Session过期但Token仍有效导致认证失败
- 刷新Token支持:如果业务需要刷新Token,需在自定义
SessionTokenStore中完善刷新Token的存储/读取逻辑,同样绑定到Session - 负载均衡器配置:确保负载均衡器会转发所有Cookie(尤其是
JSESSIONID),否则WebSphere无法识别用户Session - 生产环境优化:集群节点较多时,优先选择数据库持久化的分布式Session,避免内存复制带来的性能开销
内容的提问来源于stack exchange,提问作者Frank huaylinos velasquez
相关产品推荐
相关产品推荐

