spring-oauth集群负载的cas单点登出问题

本文介绍了解决CAS环境下单点登出故障的方法。通过监听广播机制,确保所有OAuth服务器同步登出状态,避免因集群负载导致的用户残留登录信息问题。

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

原文及更多文章请见个人博客:http://heartlifes.com

背景:

前端有N台由spring-oauth,spring-cas搭建的提供oauth2服务的服务器,后端有单台cas搭建的sso单点登录服务器,通过nginx的iphash保证用户在同一会话工程中始终登录在固定的一台oauth2服务器上。

现象:

cas3.5默认不支持集群环境下的单点登出,导致当用户使用oauth服务时,出现单点故障,具体表现为:
用户A在浏览器上完成整个oauth流程后,此时
1.用户A在单点登录服务器上点击登出按钮
2.系统提示用户登出成功
3.用户B在同一个浏览器上访问oauth服务器,此时没有要求用户B登录,还是用户A的登录信息,并且后续oauth流程报错

原因:

假设oauth服务部署在A,B两台机器上,提供负载访问。SSO单点服务部署在C机器上。
1.用户在C机上登出时,C机器上的SSO服务删除C服务器中的session,并且清空存在用户浏览器中的cookies
2.C服务器中的sso服务通知A,B中部署的oauth服务,用户已经退出,请求oauth服务清空自己的session缓存。
3.此时,由于A,B是负载设置,CAS通知的oauth登出服务,其实只是通知到了A或B中的一台。
4.假设通知到的是A服务器,此时A服务器删除oauth中的session缓存,而B服务器中的oauth session缓存依旧存在
5.用户再次使用oauth服务,此时,由于集群原因,用户可能正好使用到的是B服务器上的oauth服务,由于B服务器中session依旧存在,结果出现单点登出故障。

解决方案:

翻遍了google中所有的讨论,结果毫无进展。
尝试了使用jedis来存储session,结果发现session中存储的AuthorizationRequest类,没有实现序列化接口,无法实体化到redis中,无奈之下,使用了一种监听广播的方式

1.重写SingleSignOutFilter类中的doFilter方法

if (handler.isTokenRequest(request)) {
    handler.recordSession(request);
} else if (handler.isLogoutRequest(request)) {
    String from = request.getParameter("from");
    if (StringUtils.isEmpty(from)) {
            multiCastToDestroy(request);
    }
    handler.destroySession(request);
    // Do not continue up filter chain
    return;
} else {
    log.trace("Ignoring URI " + request.getRequestURI());
}

增加multiCastToDestroy方法

private void multiCastToDestroy(HttpServletRequest request) {
    String logoutMessage = CommonUtils.safeGetParameter(request,"logoutRequest");
    ExecutorService executors = Executors.newFixedThreadPool(100);
    String[] tmps = multicastUrls.split(",");
    for (String tmp : tmps) {
        String[] urls = tmp.split("=");
        String key = urls[0];
        String url = urls[1];
        executors.submit(new MessageSender(url, logoutMessage, ownKey,5000, 5000, true));
    }
}

增加MessageSender内部类

private static final class MessageSender implements Callable<Boolean> {
    private String url;
    private String message;
    private String from;
    private int readTimeout;
    private int connectionTimeout;
    private boolean followRedirects;

    public MessageSender(final String url, final String message,final String from, final int readTimeout,final int connectionTimeout, final boolean followRedirects) {
        this.url = url;
        this.message = message;
        this.from = from;
        this.readTimeout = readTimeout;
        this.connectionTimeout = connectionTimeout;
        this.followRedirects = followRedirects;
    }

    public Boolean call() throws Exception {
        HttpURLConnection connection = null;
        BufferedReader in = null;
        try {
            System.out.println("Attempting to access " + url);
            final URL logoutUrl = new URL(url);
            final String output = "from=" + from + "&logoutRequest="+ URLEncoder.encode(message, "UTF-8");
            connection = (HttpURLConnection) logoutUrl.openConnection();
            connection.setDoInput(true);
            connection.setDoOutput(true);
            connection.setRequestMethod("POST");
            connection.setReadTimeout(this.readTimeout);
            connection.setConnectTimeout(this.connectionTimeout);
            connection.setInstanceFollowRedirects(this.followRedirects);
            connection.setRequestProperty("Content-Length",Integer.toString(output.getBytes().length));
            connection.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
            final DataOutputStream printout = new DataOutputStream(connection.getOutputStream());
            printout.writeBytes(output);
            printout.flush();
            printout.close();
            in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
                while (in.readLine() != null) {
                // nothing to do
            }

            System.out.println("Finished sending message to" + url);
            return true;
        } catch (final SocketTimeoutException e) {
            e.printStackTrace();
            return false;
        } catch (final Exception e) {
            e.printStackTrace();
            return false;
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (final IOException e) {
                    // can't do anything
                }
            }
            if (connection != null) {
                connection.disconnect();
            }
        }
    }
}

修改oauth配置文件:
增加以下配置:

<bean id="singleLogoutFilter" class="com.xxx.xxx.cas.filter.SingleSignOutFilter">
    <property name="multicastUrls"      value="127.0.0.1=http://127.0.0.1/api/j_spring_cas_security_check,localhost=http://localhost/api/j_spring_cas_security_check" />
    <property name="ownKey" value="localhost" />
</bean>

这样,当CAS通知到A服务器去做登出操作时,A服务器会广播给其他几台服务器同步去做登出操作,通过广播的方式解决单点登出的故障

### 单点登录(SSO)的实现 在使用 `spring-security-oauth2` 实现单点登录时,主要涉及以下几个关键步骤: #### 1. **依赖配置** 首先需要引入相关的依赖包。以下是一个典型的 Maven 配置示例: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> </dependency> ``` 这些依赖项提供了 OAuth2 客户端和 Spring Security 的支持,确保可以与授权服务器进行交互 [^1]。 #### 2. **配置文件设置** 在 `application.yml` 文件中,需要配置客户端的相关信息以及与授权服务器的连接参数。例如: ```yaml server: port: 9501 servlet: session: cookie: name: OAUTH2-CLIENT-SESSIONID # 防止Cookie冲突 spring: application: name: oauth2-client security: oauth2: client: client-id: admin client-secret: admin123456 user-authorization-uri: http://localhost:9401/oauth/authorize access-token-uri: http://localhost:9401/oauth/token resource: jwt: key-uri: http://localhost:9401/oauth/token_key ``` 通过这些配置,应用程序能够正确地与授权服务器通信,并处理用户的身份验证请求 [^4]。 #### 3. **启用单点登录功能** 为了启用单点登录功能,只需要在主类上添加几行注解即可。例如: ```java @SpringBootApplication @EnableOAuth2Sso public class SsoClientApplication { public static void main(String[] args) { SpringApplication.run(SsoClientApplication.class, args); } } ``` 这个注解会自动配置 OAuth2 客户端并启用 SSO 功能,使得用户可以在多个服务之间共享身份验证状态 [^1]。 #### 4. **自定义 JWT 内容** 如果需要在 JWT 中添加自定义内容,可以通过继承 `TokenEnhancer` 类并重写 `enhance` 方法来实现。例如: ```java public class CustomTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { Map<String, Object> additionalInfo = new HashMap<>(); additionalInfo.put("customClaim", "value"); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); return accessToken; } } ``` 这样可以在生成的令牌中包含额外的信息,满足特定业务需求 [^3]。 #### 5. **多系统集成** 对于多个系统的集成,每个系统都需要在其各自的 `application.yml` 文件中配置相同的授权服务器地址和其他相关参数。例如: ```yaml server: port: 8082 servlet: context-path: /memberSystem security: oauth2: client: client-id: UserManagement client-secret: user123 access-token-uri: http://localhost:8080/oauth/token user-authorization-uri: http://localhost:8080/oauth/authorize resource: jwt: key-uri: http://localhost:8080/oauth/token_key ``` 这种配置允许不同的服务共享同一个授权服务器,从而实现统一的身份验证和授权机制 [^5]。 ### 总结 通过上述步骤,可以使用 `spring-security-oauth2` 实现一个完整的单点登录系统。需要注意的是,随着 Spring Security 的发展,官方已经不再推荐使用 `spring-security-oauth2`,而是建议使用新的 `spring-authorization-server` 项目来实现更现代的 OAuth2.1 协议 [^2]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值