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

原创 临窗旋墨 2021-11-15 阅读:6262 分类: java后端 专题: SpringBoot进阶实践 标签: sso,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
java 复制代码
@Configuration
@ConfigurationProperties(prefix = "sso")
public class SsoConfiguration {

    /**
     * 超时时间:默认2个小时 <br />
     * <p>
     *  包括:登录凭证TicketGrantingTicket(tgt);
     *  刷新凭证 RefreshToken;
     *
     *</p>
     */
    @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<AppClient> clients;
}

/**
* app信息,用户server端校验client端的合法性 包含id和secret, 暂时通过配置的方式,后续考虑存入数据库
*/
public static class AppClient {
    String id;

    String secret;
}    
    
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
  • 流程参见上述流程图
  • 下面为摘录的部分代码注释
复制代码
/**
* <p>
* 1. 判断本地session是否存在;
* 2. 如果存在,则判断是否过期
* 3. 如果过期,则使用refreshToken前往服务端获刷新token,延长周期,获取新的accessToken
* 4. 若果token不存在,或者过期,或者无法延期:
* 5. 则获取请求中的授权码
* 6. 若获取不到授权码,则前往登录页面
* 7. 通过授权码拿到accessToken(且存储到本地),则去掉url中的code,再次重定向到当前地址
* </p>
*
* @param request
* @param response
* @return
* @throws IOException
*/
前后端分离清理下的登录监听器SeparationLoginFilter (仅供参考)
复制代码
/**
 * 描述:
 * 前后端分离的login过滤器,
 * 前端在判断没有登录的情况下,直接在浏览器访问后端的某个url,然后后端进行相关重定向 <br />
 *      怎么判断: 比如返回状态码为302  则操作
 *
 *      <p>
 *          1. 前后端应该在一个域内,保证session一致
 *          校验流程:<br />
 *          1. 访问后端某个接口, 判断没有登录<br />
 *          2. 跳转到sso, 但是携带的redirectUrl地址应是当前后端的特点地址<br />
 *          3. 进行一系列校验工作后,登录成功,判断当前访问的是这个特定地址,则重定向到配置的前端地址<br />
 *          区别:<br />
 *        1. 需要配置htmlUrl:即检验成功后 重定向的前端地址<br />
 *        2. 需要配置clientHost:当前后端的根地址,用户sso Server重定向回来<br />
 *
 * </p>
 *
 * @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
  • 提供通用的常量和枚举

5 sso demo

可单独启动的demo项目,

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

参考配置如下:

java 复制代码
@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<HttpSessionListener> LogoutListener() {
        ServletListenerRegistrationBean<HttpSessionListener> listenerRegBean = new ServletListenerRegistrationBean<>();
        LogoutListener logoutListener = new LogoutListener();
        listenerRegBean.setListener(logoutListener);
        return listenerRegBean;
    }

    /**
     * ★★
     * 分布式的登出Listener ,需要为LogoutListener注入 SpringRedisSessionMappingStorage 或 ShiroRedisSessionMappingStorage(而这两种策略均依赖redis) <br />
     * 1. shiro的LogoutListener 注入方式:理应把Listener 放到shiro的SessionManage的sessionListeners中 ,以防止一些调用时机导致的session失效问题,
     *      但是由于LogoutListener实现的是HttpSessionListener 而不是shiro的SessionListener,故此处直接通过spring的方式直接注入<br />
     * 2. spring-session的shiro的LogoutListener注入方式:然后使用Spring的方式注入 LogoutListener,把Listener放进SessionEventHttpSessionListenerAdapter 中防止监听器失效  <br />
     * 以下分别给出示例代码
     */

    /**
     * 分布式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<HttpSessionListener> shiroRedisLogoutListener() {
        ServletListenerRegistrationBean<HttpSessionListener> listenerRegBean = new ServletListenerRegistrationBean<>();
        LogoutListener logoutListener = new LogoutListener();
        //注入session的处理策略为shiro
        logoutListener.setSessionMappingStorage(shiroRedisSessionMappingStorage);
        listenerRegBean.setListener(logoutListener);
        return listenerRegBean;
    }

    /**
     * 基于spring session 的分布式LogoutListener
     */
//    @Bean
    public ApplicationListener<AbstractSessionEvent> springRedisLogoutListener(){
        List<HttpSessionListener> 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 loginFilter = new LoginFilter();
        //前后端分离的登录过滤器
//        SeparationLoginFilter loginFilter = new SeparationLoginFilter(clientHost, htmlUrl);htmlUrl);
        loginFilter.setAppId(appId);
        loginFilter.setAppSecret(appSecret);
        loginFilter.setServerUrl(serverUrl);
        loginFilter.addExcludeUrl(SsoConstant.LOGOUT_URL);
        //登录成功之后的回调
        loginFilter.setAfterLogin(accessToken -> {
            userService.afterLogin(accessToken);
        });

        FilterRegistrationBean<LoginFilter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(loginFilter);
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.setOrder(2);
        filterRegistrationBean.setName("loginFilter");
        return filterRegistrationBean;
    }

    /**
     * 登出过滤器
     *
     * @return
     */
    @Bean
    public FilterRegistrationBean<LogoutFilter> logoutFilter() {
        LogoutFilter logoutFilter = new LogoutFilter();
        logoutFilter.setAppId(appId);
        logoutFilter.setAppSecret(appSecret);
        logoutFilter.setServerUrl(serverUrl);
        //登出成功后的回调
        logoutFilter.setAfterLogout(s -> userService.afterLogout(s));

        FilterRegistrationBean<LogoutFilter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(logoutFilter);
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.setName("logoutFilter");
        return filterRegistrationBean;
    }

}
测试方式:
  1. server端进行相应配置
  2. server端可重写SsoUserService 进行登录逻辑修改
  3. demo端配置调整,可配置多个配置文件 如dev,prod等
  4. demo端启动不同的配置,然后通过浏览器配置
其他注意实现:
  1. 如果修改存储策略 需要进行相关配置,如redis/spring-session/shiro-redis
  2. 如果前后端分离,可基于nginx 反向代理,实现同域访问,然后前端代码做简单调整

评论区

avatar
未登录

暂无评论,来发表第一条评论吧~