# lcxm-cas-sso **Repository Path**: basic-support/lcxm-cas-sso ## Basic Information - **Project Name**: lcxm-cas-sso - **Description**: 基于cas的sso编码实现 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-05-23 - **Last Updated**: 2025-08-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: basic-support ## README ## lcxm-cas-sso > 基于cas的sso单点登录实现,包含以下三个模块 1. lcxm-cas-sso-common:基础模块,包含一些公共的类,如:工具类,常量类,枚举类等 2. lcxm-cas-sso-client:客户端模块,包含客户端的配置,主要提供登录登出过滤器 3. lcxm-cas-sso-server:服务端模块,包含服务端的配置,是一个独立的的springboot项目 - start at 2024-05-23 ### 版本更新说明 - 参见:[CHANGELOG](CHANGELOG.md) **maven坐标** ``` cn.xuqiudong.basic lcxm-cas-sso-{moduleName} ${version} ``` ### 背景 > 同[lcxm-common](https://gitee.com/basic-support/lcxm-common/) > 原来的所有的模块都放在[boot-support](https://gitee.com/xuqiudong/boot-support), 显得项目很臃肿,现在将其拆分,方便管理。 本项目是原来的[module-cas-sso](https://gitee.com/xuqiudong/boot-support/tree/master/module-cas-sso),现在独立拆分为一个独立项目。 ### 打包: > 打包和发布的时候跳过验证 1. 打包:mvn clean package '-Dskip.spotbugs=true' 2. install: mvn clean install '-Dskip.spotbugs=true' 3. 发布: mvn clean deploy '-Dskip.spotbugs=true' -Pdeploy - lcxm-cas-sso-server 作为单独运行的项目,不在deploy到中央仓库: - ` mvn deploy -pl '!lcxm-cas-sso-server' -Pdeploy` 4. 父项目修改后,统一修改子模块依赖的父版本:`mvn versions:update-child-modules` ### 自动发布 参见[jenkinsfile](Jenkinsfile) 原来的[boot-support](https://gitee.com/xuqiudong/boot-support/blob/master/Jenkinsfile) 一下子部署发布太多了,这里把他们拆开,按需发布。 `mvn clean package -Denv=prod` [基于cas的sso编码实现](https://xuqiudong.cn/detail/24) [cas前置常识](https://xuqiudong.gitee.io/technology/#/solution/cas-sso/front-of-cas) > 项目地址:https://gitee.com/xuqiudong/boot > > 感谢:[smart-sso](https://gitee.com/a466350665/smart-sso) 请求流程: ![cas-sso流程图.drawio](docs/asset/process.png) #### 2 sso server ##### 2.1 server端主要配置参考SsoConfiguration.java ```java @Configuration @ConfigurationProperties(prefix = "sso") public class SsoConfiguration { /** * 超时时间:默认2个小时
*

* 包括:登录凭证TicketGrantingTicket(tgt); * 刷新凭证 RefreshToken; * *

*/ @Value("${sso.timeout:7200}") private int timeout; /** * 授权码超时时间 默认10分钟 */ @Value("${sso.codeTimeout:600}") private int codeTimeout; /** * accessToken 的超时时间 默认为timeout的一半 */ private int accessTokenTimeout; /** * 存储策略,只支持redis和local */ @Value("${sso.storageStrategy:local}") private String storageStrategy; /** * 可用的sso client 列表 */ private List clients; } /** * app信息,用户server端校验client端的合法性 包含id和secret, 暂时通过配置的方式,后续考虑存入数据库 */ public static class AppClient { String id; String secret; } ``` ##### 2.2 各类票据生成和校验参见包`cn.xuqiudong.sso.server.session` - AccessTokenManager:调用凭证AccessToken管理器接口 - CodeManager:授权码管理器接口 - RefreshTokenManager:刷新凭证refreshToken管理器接口 - SessionManager:全局tgt管理器 - TicketGrantingTicketManager:登录凭证(TGC)管理器接口 ##### 2.3 对各类票据的分布式和本地存储 > 最初设计为,2.2中每一类管理器接口分别做local以及redis方式的实现,从而实现本地或者分布式 > > 后面,觉得这样设计编码稍多,进而修改为,管理器不变,设计存储接口,实现不同的存储策略 ##### 2.4 存储策略参见包`pers.vic.sso.server.storage` > 根据管理器的需要在接口中提供相应的方法 - LocalStorageStrategy: 本地存储策略,默认 - RedisStorageStrategy:redis存储策略 ##### 2.5 控制器 - IndexController:提供登录登出,重定向相关功能 - Oauth2Controller:Oauth2接口提供,用以客户端校验code,刷新accessToken等 ##### 2.6其他 - AppService:应用服务相关接口 主要是针对客户端的检测 - SsoUserService:用于登录相关 #### 3 sso client > 主要提供登录登出过滤器,以及登出监听器 ##### 登录监听器LoginFilter - 流程参见上述流程图 - 下面为摘录的部分代码注释 ``` /** *

* 1. 判断本地session是否存在; * 2. 如果存在,则判断是否过期 * 3. 如果过期,则使用refreshToken前往服务端获刷新token,延长周期,获取新的accessToken * 4. 若果token不存在,或者过期,或者无法延期: * 5. 则获取请求中的授权码 * 6. 若获取不到授权码,则前往登录页面 * 7. 通过授权码拿到accessToken(且存储到本地),则去掉url中的code,再次重定向到当前地址 *

* * @param request * @param response * @return * @throws IOException */ ``` ###### 前后端分离清理下的登录监听器SeparationLoginFilter (仅供参考) ``` /** * 描述: * 前后端分离的login过滤器, * 前端在判断没有登录的情况下,直接在浏览器访问后端的某个url,然后后端进行相关重定向
* 怎么判断: 比如返回状态码为302 则操作 * *

* 1. 前后端应该在一个域内,保证session一致 * 校验流程:
* 1. 访问后端某个接口, 判断没有登录
* 2. 跳转到sso, 但是携带的redirectUrl地址应是当前后端的特点地址
* 3. 进行一系列校验工作后,登录成功,判断当前访问的是这个特定地址,则重定向到配置的前端地址
* 区别:
* 1. 需要配置htmlUrl:即检验成功后 重定向的前端地址
* 2. 需要配置clientHost:当前后端的根地址,用户sso Server重定向回来
* *

* * @author Vic.xu * @date 2021-11-12 11:13 */ ``` ##### 登出过滤器LogoutFilter > 退出登录过滤器:客户端不主动退出,而是跳转到sso server端 执行退出处理,然后sso端通过回调通知各个客户端分别退出; > 但是服务端调用的地址是登录时候携带的redirectUrl地址,可能是任何地址(除非限制回跳地址就是首页) - 需要配置客户端退出地址(默认为)/logout - 等客户端退出的时候,重定向到sso server进行退出 - 然后客户端发起通知,通知各客户端分别退出 - LogoutFilter 收到server端的通知,获取accessToken对应的session,进行销毁 ##### 登出监听器与SessionMappingStorage > LogoutListener :登出Listener,用于本地session过期,删除accessToken和session的映射关系 > > 默认为本地存储的方式,可配置不同SessionMappingStorage ###### accessToken和session的映射关系存储SessionMappingStorage - LocalSessionMappingStorage:本地维持session 和 accessToken相互之间的关系,默认方式 - ShiroRedisSessionMappingStorage:如果客户端通过shiro实现session共享的话 - SpringRedisSessionMappingStorage:如果客户端使用spring-session-data-redis 管理session的话 (@EnableRedisHttpSession) #### 4 sso common - 提供一些工具类如http请求等 - 提供通用的model - 提供通用的常量和枚举 #### 兼容移动端登录 > 移动端直接请求项目中的登录,在登录接口中调用本SsoOauth2Util方法的通过账号密码获取 accessToken > > **参见ApiLoginFilter 的注释说明** ``` /** * 描述:基于接口的登录 *

* 1. 使用与基于API接口的项目,比如移动端或者前后端分离 * 2. 流程: * 2.1 进入拦截器,没有登录 则返回401 前端自行跳转到登录页面,输入账号密码 * 2.2 后端进行基于账号密码的方式获取accessToken {@link cn.xuqiudong.sso.common.util.SsoOauth2Util#getAccessToken(String, String, String, String, String)} 并存入session * 2.3 前端在访问接口的时候携带 sessionId或者token *

* * 原本是通过此 SeparationLoginFilter 进行控制的,但是这种控制对后端耦合太强 * @see SeparationLoginFilter */ ``` ## config demo ```java package pers.vic.sso.demo.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletListenerRegistrationBean; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.session.events.AbstractSessionEvent; import org.springframework.session.web.http.SessionEventHttpSessionListenerAdapter; import pers.vic.sso.client.filter.LoginFilter; import pers.vic.sso.client.filter.LogoutFilter; import pers.vic.sso.client.listener.LogoutListener; import pers.vic.sso.client.session.shiro.ShiroRedisSessionMappingStorage; import pers.vic.sso.client.session.spring.SpringRedisSessionMappingStorage; import pers.vic.sso.common.constant.SsoConstant; import pers.vic.sso.demo.user.service.UserService; import javax.annotation.Resource; import javax.servlet.http.HttpSessionListener; import java.util.ArrayList; import java.util.List; /** * 描述: * 集成单点登录的配置 * * @author Vic.xu * @since 2021-11-03 17:09 */ @Configuration public class SsoConfig { @Value("${sso.server.url}") private String serverUrl; @Value("${sso.app.id}") private String appId; @Value("${sso.app.secret}") private String appSecret; @Value("${sso.html.url}") private String htmlUrl; @Value("${sso.client.host}") private String clientHost; @Resource private UserService userService; /** * 单实例方式单点登出Listener,因为它的存储策略就是LocalSessionMappingStorage, 所以无需额外处理 * * @return */ @Bean public ServletListenerRegistrationBean logoutListener() { ServletListenerRegistrationBean listenerRegBean = new ServletListenerRegistrationBean<>(); LogoutListener logoutListener = new LogoutListener(); listenerRegBean.setListener(logoutListener); return listenerRegBean; } /** * ★★ * 分布式的登出Listener ,需要为LogoutListener注入 SpringRedisSessionMappingStorage 或 ShiroRedisSessionMappingStorage(而这两种策略均依赖redis)
* 1. shiro的LogoutListener 注入方式:理应把Listener 放到shiro的SessionManage的sessionListeners中 ,以防止一些调用时机导致的session失效问题, * 但是由于LogoutListener实现的是HttpSessionListener 而不是shiro的SessionListener,故此处直接通过spring的方式直接注入
* 2. spring-session的shiro的LogoutListener注入方式:然后使用Spring的方式注入 LogoutListener,把Listener放进SessionEventHttpSessionListenerAdapter 中防止监听器失效
* 以下分别给出示例代码 */ /** * 分布式spring-redis方式登出Listener: * 先注入 SpringRedisSessionMappingStorage , */ // @Autowired private SpringRedisSessionMappingStorage springRedisSessionMappingStorage; // @Bean public SpringRedisSessionMappingStorage springRedisSessionMappingStorage() { return new SpringRedisSessionMappingStorage(); } // @Autowired private ShiroRedisSessionMappingStorage shiroRedisSessionMappingStorage; // @Bean public ShiroRedisSessionMappingStorage shiroRedisSessionMappingStorage() { return new ShiroRedisSessionMappingStorage(); } /** * 基于shiro 的分布式LogoutListener */ // @Bean public ServletListenerRegistrationBean shiroRedisLogoutListener() { ServletListenerRegistrationBean listenerRegBean = new ServletListenerRegistrationBean<>(); LogoutListener logoutListener = new LogoutListener(); //注入session的处理策略为shiro logoutListener.setSessionMappingStorage(shiroRedisSessionMappingStorage); listenerRegBean.setListener(logoutListener); return listenerRegBean; } /** * 基于spring session 的分布式LogoutListener */ // @Bean public ApplicationListener springRedisLogoutListener() { List httpSessionListeners = new ArrayList<>(); LogoutListener logoutListener = new LogoutListener(); //注入session的处理策略为spring-session logoutListener.setSessionMappingStorage(springRedisSessionMappingStorage); httpSessionListeners.add(logoutListener); return new SessionEventHttpSessionListenerAdapter(httpSessionListeners); } /** * 登录过滤器 * * @return */ @Bean public FilterRegistrationBean loginFilter() { LoginFilter loginFilter = new LoginFilter(); //前后端分离的登录过滤器 // SeparationLoginFilter loginFilter = new SeparationLoginFilter(clientHost, htmlUrl); loginFilter.setAppId(appId); loginFilter.setAppSecret(appSecret); loginFilter.setServerUrl(serverUrl); loginFilter.addExcludeUrl(SsoConstant.LOGOUT_URL); //登录成功之后的回调 loginFilter.setAfterLogin(accessToken -> { userService.afterLogin(accessToken); }); FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); filterRegistrationBean.setFilter(loginFilter); filterRegistrationBean.addUrlPatterns("/*"); filterRegistrationBean.setOrder(2); filterRegistrationBean.setName("loginFilter"); return filterRegistrationBean; } /** * 登出过滤器 * * @return */ @Bean public FilterRegistrationBean logoutFilter() { LogoutFilter logoutFilter = new LogoutFilter(); logoutFilter.setAppId(appId); logoutFilter.setAppSecret(appSecret); logoutFilter.setServerUrl(serverUrl); //登出成功后的回调 logoutFilter.setAfterLogout(s -> userService.afterLogout(s)); FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); filterRegistrationBean.setFilter(logoutFilter); filterRegistrationBean.addUrlPatterns("/*"); filterRegistrationBean.setOrder(1); filterRegistrationBean.setName("logoutFilter"); return filterRegistrationBean; } } ```