前言
在早期,项目规模不大的时候,企业中存在的系统不多,通常为1或者2个。每个系统都有自己独立的登陆模块,这样用户进行登陆也不是特别麻烦,分别进行登陆就可以了。但是随着企业规模不断变大,随之而然的系统模块也越来越多,而每个模块都有自己独立的登陆,那么用户就会有很多登陆账号,想要进入系统都得进行单独登陆,这不然是最痛苦的。
那么能不能只在一个系统进行登陆成功之后,在其他系统就不用再登陆了呢?就行我在淘宝网站进行登陆之后,点击链接进入天猫商城一样,不用再重新进行登陆。解决办法是有的,那就是使用单点登陆方案解决。方案有了,不同实现的技术就会出现。下面介绍的是许雪里老师编写开源出来的xxl-sso框架。
单点登陆英文全称Single Sign On ,简称就是SSO。它的解释就是:在多个应用系统中,只需要在一个系统中完成登陆,就可以访问其他相互信任的应用系统。
之前是每一个系统都有单独的登陆认证模块,现在是将登陆功能统一的由SSO进行登陆认证,其它的应用模块没有登陆功能。
Xxl-sso是一个分布式单点登陆框架。只需要登陆一次就可以访问所有相互信任的应用系统。下面先介绍一下它的特性有哪些:
简洁:API直观简洁,可快速上手
轻量级:环境依赖小,部署与接入成本较低
单点登录:只需要登录一次就可以访问所有相互信任的应用系统
分布式:接入SSO认证中心的应用,支持分布式部署
HA:Server端与Client端,均支持集群部署,提高系统可用性
跨域:支持跨域应用接入SSO认证中心
Cookie+Token均支持:支持基于Cookie和基于Token两种接入方式,并均提供Sample项目
Web+APP均支持:支持Web和APP接入
实时性:系统登陆、注销状态,全部Server与Client端实时共享
CS结构:基于CS结构,包括Server”认证中心”与Client”受保护应用”
记住密码:未记住密码时,关闭浏览器则登录态失效;记住密码时,支持登录态自动延期,在自定义延期时间的基础上,原则上可以无限延期
路径排除:支持自定义多个排除路径,支持Ant表达式,用于排除SSO客户端不需要过滤的路径
xxl-sso框架结构
项目下载地址: https://github.com/xuxueli/xxl-sso
- 包含三个模块
xxl-sso-server:中央认证服务,支持集群
xxl-sso-core:Client端依赖,主要作用为路径的排除,哪些路径不需要登陆就可以访问的、登陆时token的认证检查等
xxl-sso-samples:单点登陆Client端接入示例项目
xxl-sso-web-sample-springboot:基于Cookie接入方式,供用户浏览器访问,springboot版本
xxl-sso-token-sample-springboot:基于Token接入方式,常用于无法使用Cookie的场景使用,如APP、Cookie被禁用等,springboot版本
-
环境
JDK:1.7+
Redis:4.0+
- 架构图
- 相关配置文件介绍
-
xxl-sso-server认证中心(SSO Server)
配置文件位置:application.properties
### xxl-sso
redis 地址: 如 "{ip}"、"{ip}:{port}"、"{redis/rediss}://xxl-sso:{password}@{ip}:{port:6379}/{db}";多地址逗号分隔
xxl.sso.redis.address=redis://127.0.0.1:6379
登录态有效期窗口,默认24H,当登录态有效期窗口过半时,自动顺延一个周期
xxl.sso.redis.expire.minite=1440
密码配置:可查看com.xxl.sso.core.util下的JedisUtil类中获取ShardedJedis实例代码
2. xxl-sso-web-sample-springboot(应用client)
引入xxl-sso-core核心依赖包
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-sso-core</artifactId>
<version>${最新稳定版}</version>
</dependency>
配置xxlSsoFilter
配置文件位置:application.properties
### xxl-sso
### Sso server认证中心地址
xxl.sso.server=http://xxlssoserver.com:8080/xxl-sso-server
### 注销登陆path
xxl.sso.logout.path=/logout
### 路径排除path,允许设置多个,且支持Ant表达式。用于排除SSO客户端不需要过滤的路径
xxl-sso.excluded.paths=
// redis address, like "{ip}"、"{ip}:{port}"、"{redis/rediss}://xxl-sso:{password}@{ip}:{port:6379}/{db}";Multiple "," separated
xxl.sso.redis.address=redis://127.0.0.1:6379
- 修改hosts文件,模拟真实环境
127.0.0.1 xxlssoserver.com
127.0.0.1 xxlssoclient1.com
127.0.0.1 xxlssoclient2.com
如果在hosts文件中添加了以上内容,不生效,解决方法:
打开命令行窗口:
ipconfig /displaydns 查看配置的dns是否存在
ipconfig /flushdns刷新dns配置
-
项目启动
分别运行“xxl-sso-server”“xxl-sso-web-sample-springboot”项目
相应访问测试地址
1、SSO认证中心地址:
http://xxlssoserver.com:8080/xxl-sso-server
2、Client1应用地址:
http://xxlssoclient1.com:8081/xxl-sso-token-sample-springboot/
3、Client2应用地址:
http://xxlssoclient2.com:8081/xxl-sso-token-sample-springboot/
- 源码跟踪
- 访问Client1应用地址,会经过XxlSsoWebFilter拦截。XxlSsoWebFilter存在于核心依赖包中,应用中配置如下:
- 在XxlSsoWebFilter过滤器中,先进行init初始化操作,初始化内容为在Client1应用中配置的相关信息。然后走doFilter方法,实现登陆检查。
- doFilter方法具体代码如下:
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
// (1) 请求路径
String servletPath = req.getServletPath();
//(2) 路径排除
if (excludedPaths!=null && excludedPaths.trim().length()>0) {
for (String excludedPath:excludedPaths.split(",")) {
String uriPattern = excludedPath.trim();
// 支持ANT表达式
if (antPathMatcher.match(uriPattern, servletPath)) {
chain.doFilter(request, response);
return;
}
}
}
// (3) 登出
if (logoutPath!=null
&& logoutPath.trim().length()>0
&& logoutPath.equals(servletPath)) {
// remove cookie
SsoWebLoginHelper.removeSessionIdByCookie(req, res);
// redirect logout
String logoutPageUrl = ssoServer.concat(Conf.SSO_LOGOUT);
res.sendRedirect(logoutPageUrl);
return;
}
// (4) 登陆检查
XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(req, res);
// (5) 用户信息不存在时
if (xxlUser == null) {
String header = req.getHeader("content-type");
boolean isJson= header!=null && header.contains("json");
if (isJson) {
//(6) json消息
res.setContentType("application/json;charset=utf-8");
res.getWriter().println("{\"code\":"+Conf.SSO_LOGIN_FAIL_RESULT.getCode()+", \"msg\":\""+ Conf.SSO_LOGIN_FAIL_RESULT.getMsg() +"\"}");
return;
} else {
// (7) 访问源地址 http://xxlssoclient1.com:8081/xxl-sso-web-sample-springboot/
String link = req.getRequestURL().toString();
// (8) 重定向地址 http://xxlssoserver.com:8080/xxl-sso-server/login?redirect_url=http://xxlssoclient1.com:8081/xxl-sso-web-sample-springboot/
String loginPageUrl = ssoServer.concat(Conf.SSO_LOGIN)
+ "?" + Conf.REDIRECT_URL + "=" + link;
res.sendRedirect(loginPageUrl);
return;
}
}
// ser sso user
request.setAttribute(Conf.SSO_USER, xxlUser);
// (9) 过滤器放行
chain.doFilter(request, response);
return;
}
编号注释(1-9):
(1)-(3):主要做路径排除和用户登出的操作。
(4):登陆用户信息检查(重点部分),下面会进行代码跟踪。
(5):如果没有用户信息的处理。
(6-9):相关信息注释,下面会使用到。
-
接下来,我们看些编号(4)做的具体处理逻辑
调用SsoWebLoginHelper中的loginCheck方法
又会调用SsoTokenLoginHelper中的lgoinCheck方法,代码如下:
public static XxlSsoUser loginCheck(String sessionId){
// 解析key sessionId生成规则 userId+"_"+version
String storeKey = SsoSessionIdHelper.parseStoreKey(sessionId);
if (storeKey == null) {
return null;
}
//根据key从redis中获取用户信息
XxlSsoUser xxlUser = SsoLoginStore.get(storeKey);
if (xxlUser != null) {
String version = SsoSessionIdHelper.parseVersion(sessionId);
if (xxlUser.getVersion().equals(version)) {
// 判断时间是否过半,如果是 自动刷新
if ((System.currentTimeMillis() - xxlUser.getExpireFreshTime()) > xxlUser.getExpireMinite()/2) {
xxlUser.setExpireFreshTime(System.currentTimeMillis());
SsoLoginStore.put(storeKey, xxlUser);
}
return xxlUser;
}
}
return null;
}
/**
* login check
*
* @param request
* @return
*/
public static XxlSsoUser loginCheck(HttpServletRequest request){
String headerSessionId = request.getHeader(Conf.SSO_SESSIONID);
return loginCheck(headerSessionId);
}
- 如果(4)走完,XxlSsoUser为空
判断是否是json请求,此时请求不是json的。重定向到(8)SSO Server项目进行认证:
认证地址:http://xxlssoserver.com:8080/xxl-sso-server/login?redirect_url=http://xxlssoclient1:8081/xxl-sso-web-sample-springboot/
将重定向的地址存放到登陆隐藏域中 -
登陆
点击登陆,请求/doLogin
@RequestMapping("/doLogin")
public String doLogin(HttpServletRequest request,
HttpServletResponse response,
RedirectAttributes redirectAttributes,
String username,
String password,
String ifRemember) {
//判断是否勾选了记住我的功能
boolean ifRem = (ifRemember!=null&&"on".equals(ifRemember))?true:false;
// 查询用户逻辑
ReturnT<UserInfo> result = userService.findUser(username, password);
if (result.getCode() != ReturnT.SUCCESS_CODE) {
redirectAttributes.addAttribute("errorMsg", result.getMsg());
redirectAttributes.addAttribute(Conf.REDIRECT_URL, request.getParameter(Conf.REDIRECT_URL));
return "redirect:/login";
}
// 模拟用户信息
XxlSsoUser xxlUser = new XxlSsoUser();
xxlUser.setUserid(String.valueOf(result.getData().getUserid()));
xxlUser.setUsername(result.getData().getUsername());
xxlUser.setVersion(UUID.randomUUID().toString().replaceAll("-", ""));
xxlUser.setExpireMinite(SsoLoginStore.getRedisExpireMinite());
xxlUser.setExpireFreshTime(System.currentTimeMillis());
// 生成sessionId,生成规则:userId+"_"+version
String sessionId = SsoSessionIdHelper.makeSessionId(xxlUser);
// 存储到redis和cookie中
SsoWebLoginHelper.login(response, sessionId, xxlUser, ifRem);
// 获取隐藏域中存放的登陆成功之后的地址
String redirectUrl = request.getParameter(Conf.REDIRECT_URL);
if (redirectUrl!=null && redirectUrl.trim().length()>0) {
String redirectUrlFinal = redirectUrl + "?" + Conf.SSO_SESSIONID + "=" + sessionId;
return "redirect:" + redirectUrlFinal;
} else {
return "redirect:/";
}
}
携带sessionId重定向到源请求资源地址:http://xxlssoclient1.com:8081/xxl-sso-web-sample-springboot/,此时又会经过XxlSsoWebFilter过滤器
走doFilter方法
调用loginCheck方法检查验证,并写入cookie
到这里xxlssoclient1.com登陆成功
-
请求Client2应用地址
同样会经过XxlSsoWebFilter,调用SsoWebLoginHelper.loginCheck方法校验用户信息。最后会被重定向到Sso Server
此时SSO Server怎么知道client2不用在进行登陆了呢?看SSO Server项目中代码
@RequestMapping(Conf.SSO_LOGIN)
public String login(Model model, HttpServletRequest request, HttpServletResponse response) {
// 做登陆检查,此时获取cookieSessionid是从xxlssoserver.com域名下获取的,一定存在用户信息,此前在client1系统中已经登陆过了。
XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(request, response);
if (xxlUser != null) {
// success redirect
String redirectUrl = request.getParameter(Conf.REDIRECT_URL);
if (redirectUrl!=null && redirectUrl.trim().length()>0) {
String sessionId = SsoWebLoginHelper.getSessionIdByCookie(request);
String redirectUrlFinal = redirectUrl + "?" + Conf.SSO_SESSIONID + "=" + sessionId;;
// 重定向到client2应用地址,携带sessionId
return "redirect:" + redirectUrlFinal;
} else {
return "redirect:/";
}
}
model.addAttribute("errorMsg", request.getParameter("errorMsg"));
model.addAttribute(Conf.REDIRECT_URL, request.getParameter(Conf.REDIRECT_URL));
return "login";
}
同client1重定向方式相同,会经过XxlSsoWebFilter过滤器
走doFilter方法
调用loginCheck方法检查验证,并写入cookie
至此实现了在Client1系统中完成登陆后,访问Client2系统,不再进行登陆操作。