项目地址:https://gitee.com/xuqiudong/boot
感谢:smart-sso
boot-project-sso-server 单独部署的服务端
boot-jar-sso-client sso的客户端,主要是提供Filter
boot-jar-sso-common sso 共用代码
boot-demo-sso 可单独运行的示例项目
请求流程:

@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;}
pers.vic.sso.server.session最初设计为,2.2中每一类管理器接口分别做local以及redis方式的实现,从而实现本地或者分布式
后面,觉得这样设计编码稍多,进而修改为,管理器不变,设计存储接口,实现不同的存储策略
pers.vic.sso.server.storage根据管理器的需要在接口中提供相应的方法
主要提供登录登出过滤器,以及登出监听器
/*** <p>* 1. 判断本地session是否存在;* 2. 如果存在,则判断是否过期* 3. 如果过期,则使用refreshToken前往服务端获刷新token,延长周期,获取新的accessToken* 4. 若果token不存在,或者过期,或者无法延期:* 5. 则获取请求中的授权码* 6. 若获取不到授权码,则前往登录页面* 7. 通过授权码拿到accessToken(且存储到本地),则去掉url中的code,再次重定向到当前地址* </p>** @param request* @param response* @return* @throws IOException*/
/*** 描述:* 前后端分离的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*/
退出登录过滤器:客户端不主动退出,而是跳转到sso server端 执行退出处理,然后sso端通过回调通知各个客户端分别退出;
但是服务端调用的地址是登录时候携带的redirectUrl地址,可能是任何地址(除非限制回跳地址就是首页)
LogoutListener :登出Listener,用于本地session过期,删除accessToken和session的映射关系
默认为本地存储的方式,可配置不同SessionMappingStorage
可单独启动的demo项目,
主要配置 登录管理器,登出管理器,登出监听器。以及他们对应的参数,(如sso server地址,本地标识)
参考配置如下:
@Configurationpublic 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;@Resourceprivate UserService userService;/*** 单实例方式单点登出Listener,因为它的存储策略就是LocalSessionMappingStorage, 所以无需额外处理** @return*/@Beanpublic 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 ,*/// @Autowiredprivate SpringRedisSessionMappingStorage springRedisSessionMappingStorage;// @Beanpublic SpringRedisSessionMappingStorage springRedisSessionMappingStorage(){return new SpringRedisSessionMappingStorage();}// @Autowiredprivate ShiroRedisSessionMappingStorage shiroRedisSessionMappingStorage;// @Beanpublic ShiroRedisSessionMappingStorage shiroRedisSessionMappingStorage(){return new ShiroRedisSessionMappingStorage();}/*** 基于shiro 的分布式LogoutListener*/// @Beanpublic ServletListenerRegistrationBean<HttpSessionListener> shiroRedisLogoutListener() {ServletListenerRegistrationBean<HttpSessionListener> listenerRegBean = new ServletListenerRegistrationBean<>();LogoutListener logoutListener = new LogoutListener();//注入session的处理策略为shirologoutListener.setSessionMappingStorage(shiroRedisSessionMappingStorage);listenerRegBean.setListener(logoutListener);return listenerRegBean;}/*** 基于spring session 的分布式LogoutListener*/// @Beanpublic ApplicationListener<AbstractSessionEvent> springRedisLogoutListener(){List<HttpSessionListener> httpSessionListeners = new ArrayList<>();LogoutListener logoutListener = new LogoutListener();//注入session的处理策略为spring-sessionlogoutListener.setSessionMappingStorage(springRedisSessionMappingStorage);httpSessionListeners.add(logoutListener);return new SessionEventHttpSessionListenerAdapter(httpSessionListeners);}/*** 登录过滤器** @return*/@Beanpublic 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*/@Beanpublic 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;}}