基于CAS协议的SSO单点登录代码实现

文章来源原创   作者:临窗旋墨   发布时间:2021-11-15   阅读:2130   标签:spring 分类:我的编码 专题:解决方案

我的 CAS SSO

cas前置常识

项目地址:https://gitee.com/xuqiudong/boot

感谢:smart-sso

1 项目结构

  • boot-project-sso-server 单独部署的服务端

  • boot-jar-sso-client sso的客户端,主要是提供Filter

  • boot-jar-sso-common sso 共用代码

  • boot-demo-sso 可单独运行的示例项目

请求流程:

cas-sso流程图.drawio

2 sso server

2.1 server端主要配置参考SsoConfiguration.java
  1. @Configuration
  2. @ConfigurationProperties(prefix = "sso")
  3. public class SsoConfiguration {
  4. /**
  5. * 超时时间:默认2个小时 <br />
  6. * <p>
  7. * 包括:登录凭证TicketGrantingTicket(tgt);
  8. * 刷新凭证 RefreshToken;
  9. *
  10. *</p>
  11. */
  12. @Value("${sso.timeout:7200}")
  13. private int timeout;
  14. /**
  15. * 授权码超时时间 默认10分钟
  16. */
  17. @Value("${sso.codeTimeout:600}")
  18. private int codeTimeout;
  19. /**
  20. * accessToken 的超时时间 默认为timeout的一半
  21. */
  22. private int accessTokenTimeout;
  23. /**
  24. * 存储策略,只支持redis和local
  25. */
  26. @Value("${sso.storageStrategy:local}")
  27. private String storageStrategy;
  28. /**
  29. * 可用的sso client 列表
  30. */
  31. private List<AppClient> clients;
  32. }
  33. /**
  34. * app信息,用户server端校验client端的合法性 包含id和secret, 暂时通过配置的方式,后续考虑存入数据库
  35. */
  36. public static class AppClient {
  37. String id;
  38. String secret;
  39. }
2.2 各类票据生成和校验参见包pers.vic.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. /**
  2. * <p>
  3. * 1. 判断本地session是否存在;
  4. * 2. 如果存在,则判断是否过期
  5. * 3. 如果过期,则使用refreshToken前往服务端获刷新token,延长周期,获取新的accessToken
  6. * 4. 若果token不存在,或者过期,或者无法延期:
  7. * 5. 则获取请求中的授权码
  8. * 6. 若获取不到授权码,则前往登录页面
  9. * 7. 通过授权码拿到accessToken(且存储到本地),则去掉url中的code,再次重定向到当前地址
  10. * </p>
  11. *
  12. * @param request
  13. * @param response
  14. * @return
  15. * @throws IOException
  16. */
前后端分离清理下的登录监听器SeparationLoginFilter (仅供参考)
  1. /**
  2. * 描述:
  3. * 前后端分离的login过滤器,
  4. * 前端在判断没有登录的情况下,直接在浏览器访问后端的某个url,然后后端进行相关重定向 <br />
  5. * 怎么判断: 比如返回状态码为302 则操作
  6. *
  7. * <p>
  8. * 1. 前后端应该在一个域内,保证session一致
  9. * 校验流程:<br />
  10. * 1. 访问后端某个接口, 判断没有登录<br />
  11. * 2. 跳转到sso, 但是携带的redirectUrl地址应是当前后端的特点地址<br />
  12. * 3. 进行一系列校验工作后,登录成功,判断当前访问的是这个特定地址,则重定向到配置的前端地址<br />
  13. * 区别:<br />
  14. * 1. 需要配置htmlUrl:即检验成功后 重定向的前端地址<br />
  15. * 2. 需要配置clientHost:当前后端的根地址,用户sso Server重定向回来<br />
  16. *
  17. * </p>
  18. *
  19. * @author Vic.xu
  20. * @date 2021-11-12 11:13
  21. */
登出过滤器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
  • 提供通用的常量和枚举

5 sso demo

可单独启动的demo项目,

主要配置 登录管理器,登出管理器,登出监听器。以及他们对应的参数,(如sso server地址,本地标识)

参考配置如下:

  1. @Configuration
  2. public class SsoConfig {
  3. @Value("${sso.server.url}")
  4. private String serverUrl;
  5. @Value("${sso.app.id}")
  6. private String appId;
  7. @Value("${sso.app.secret}")
  8. private String appSecret;
  9. @Value("${sso.html.url}")
  10. private String htmlUrl;
  11. @Value("${sso.client.host}")
  12. private String clientHost;
  13. @Resource
  14. private UserService userService;
  15. /**
  16. * 单实例方式单点登出Listener,因为它的存储策略就是LocalSessionMappingStorage, 所以无需额外处理
  17. *
  18. * @return
  19. */
  20. @Bean
  21. public ServletListenerRegistrationBean<HttpSessionListener> LogoutListener() {
  22. ServletListenerRegistrationBean<HttpSessionListener> listenerRegBean = new ServletListenerRegistrationBean<>();
  23. LogoutListener logoutListener = new LogoutListener();
  24. listenerRegBean.setListener(logoutListener);
  25. return listenerRegBean;
  26. }
  27. /**
  28. * ★★
  29. * 分布式的登出Listener ,需要为LogoutListener注入 SpringRedisSessionMappingStorage 或 ShiroRedisSessionMappingStorage(而这两种策略均依赖redis) <br />
  30. * 1. shiro的LogoutListener 注入方式:理应把Listener 放到shiro的SessionManage的sessionListeners中 ,以防止一些调用时机导致的session失效问题,
  31. * 但是由于LogoutListener实现的是HttpSessionListener 而不是shiro的SessionListener,故此处直接通过spring的方式直接注入<br />
  32. * 2. spring-session的shiro的LogoutListener注入方式:然后使用Spring的方式注入 LogoutListener,把Listener放进SessionEventHttpSessionListenerAdapter 中防止监听器失效 <br />
  33. * 以下分别给出示例代码
  34. */
  35. /**
  36. * 分布式spring-redis方式登出Listener:
  37. * 先注入 SpringRedisSessionMappingStorage ,
  38. */
  39. // @Autowired
  40. private SpringRedisSessionMappingStorage springRedisSessionMappingStorage;
  41. // @Bean
  42. public SpringRedisSessionMappingStorage springRedisSessionMappingStorage(){
  43. return new SpringRedisSessionMappingStorage();
  44. }
  45. // @Autowired
  46. private ShiroRedisSessionMappingStorage shiroRedisSessionMappingStorage;
  47. // @Bean
  48. public ShiroRedisSessionMappingStorage shiroRedisSessionMappingStorage(){
  49. return new ShiroRedisSessionMappingStorage();
  50. }
  51. /**
  52. * 基于shiro 的分布式LogoutListener
  53. */
  54. // @Bean
  55. public ServletListenerRegistrationBean<HttpSessionListener> shiroRedisLogoutListener() {
  56. ServletListenerRegistrationBean<HttpSessionListener> listenerRegBean = new ServletListenerRegistrationBean<>();
  57. LogoutListener logoutListener = new LogoutListener();
  58. //注入session的处理策略为shiro
  59. logoutListener.setSessionMappingStorage(shiroRedisSessionMappingStorage);
  60. listenerRegBean.setListener(logoutListener);
  61. return listenerRegBean;
  62. }
  63. /**
  64. * 基于spring session 的分布式LogoutListener
  65. */
  66. // @Bean
  67. public ApplicationListener<AbstractSessionEvent> springRedisLogoutListener(){
  68. List<HttpSessionListener> httpSessionListeners = new ArrayList<>();
  69. LogoutListener logoutListener = new LogoutListener();
  70. //注入session的处理策略为spring-session
  71. logoutListener.setSessionMappingStorage(springRedisSessionMappingStorage);
  72. httpSessionListeners.add(logoutListener);
  73. return new SessionEventHttpSessionListenerAdapter(httpSessionListeners);
  74. }
  75. /**
  76. * 登录过滤器
  77. *
  78. * @return
  79. */
  80. @Bean
  81. public FilterRegistrationBean<LoginFilter> loginFilter() {
  82. LoginFilter loginFilter = new LoginFilter();
  83. //前后端分离的登录过滤器
  84. // SeparationLoginFilter loginFilter = new SeparationLoginFilter(clientHost, htmlUrl);htmlUrl);
  85. loginFilter.setAppId(appId);
  86. loginFilter.setAppSecret(appSecret);
  87. loginFilter.setServerUrl(serverUrl);
  88. loginFilter.addExcludeUrl(SsoConstant.LOGOUT_URL);
  89. //登录成功之后的回调
  90. loginFilter.setAfterLogin(accessToken -> {
  91. userService.afterLogin(accessToken);
  92. });
  93. FilterRegistrationBean<LoginFilter> filterRegistrationBean = new FilterRegistrationBean<>();
  94. filterRegistrationBean.setFilter(loginFilter);
  95. filterRegistrationBean.addUrlPatterns("/*");
  96. filterRegistrationBean.setOrder(2);
  97. filterRegistrationBean.setName("loginFilter");
  98. return filterRegistrationBean;
  99. }
  100. /**
  101. * 登出过滤器
  102. *
  103. * @return
  104. */
  105. @Bean
  106. public FilterRegistrationBean<LogoutFilter> logoutFilter() {
  107. LogoutFilter logoutFilter = new LogoutFilter();
  108. logoutFilter.setAppId(appId);
  109. logoutFilter.setAppSecret(appSecret);
  110. logoutFilter.setServerUrl(serverUrl);
  111. //登出成功后的回调
  112. logoutFilter.setAfterLogout(s -> userService.afterLogout(s));
  113. FilterRegistrationBean<LogoutFilter> filterRegistrationBean = new FilterRegistrationBean<>();
  114. filterRegistrationBean.setFilter(logoutFilter);
  115. filterRegistrationBean.addUrlPatterns("/*");
  116. filterRegistrationBean.setOrder(1);
  117. filterRegistrationBean.setName("logoutFilter");
  118. return filterRegistrationBean;
  119. }
  120. }
测试方式:
  1. server端进行相应配置
  2. server端可重写SsoUserService 进行登录逻辑修改
  3. demo端配置调整,可配置多个配置文件 如dev,prod等
  4. demo端启动不同的配置,然后通过浏览器配置
其他注意实现:
  1. 如果修改存储策略 需要进行相关配置,如redis/spring-session/shiro-redis
  2. 如果前后端分离,可基于nginx 反向代理,实现同域访问,然后前端代码做简单调整

发表评论

目录