You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 06:37:43