项目地址: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地址,本地标识)
参考配置如下:
@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;
}
}