单点登录介绍
基于spring session 的方式只能解决session在同域或者是同服务的session共享问题。但是无法解决不同域名之前的共享问题,因此我们需要一台认证服务器。
sso思路
sso思路
记住一个核心思想:建议一个公共的登陆点server,他登录了代表这个集团的产品就登录过了
流程
有两个子系统app1、app2
登录app1
- 用户访问app1系统,app1系统是需要登录的,但用户现在没有登录。
- 跳转到CAS server,即SSO登录系统,以后图中的CAS Server我们统一叫做SSO系统。 SSO系统也没有登录,弹出用户登录页。
- 用户填写用户名、密码,SSO系统进行认证后,将登录状态写入SSO的session,浏览器(Browser)中写入SSO域下的Cookie。
- SSO系统登录完成后会生成一个ST(Service Ticket),然后跳转到app1系统,同时将ST作为参数传递给app1系统。
- app1系统拿到ST后,从后台向SSO发送请求,验证ST是否有效。
- 验证通过后,app系统将登录状态写入session并设置app域下的Cookie。
再去登录app2
- 用户访问app2系统,app2系统没有登录,跳转到SSO。
- 由于SSO已经登录了,不需要重新登录认证。
- SSO生成ST,浏览器跳转到app2系统,并将ST作为参数传递给app2。
- app2拿到ST,后台访问SSO,验证ST是否有效。
- 验证成功后,app2将登录状态写入session,并在app2域下写入Cookie。
开源框架xxl-sso
目前比较火的开源的单点登录框架参考,地址如下
实现原理
源码分析
客户端代码分析
登录客户端服务
- 首先通过过滤器拦截访问请求获取cookie中的sessionId字段获取到cookie中存的值,存在先去redis或者其他的一些中间存储的第三方获取到存储的登录用户对象
第一层核心代码部分
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
... 其他判断
// 重点是这个
//valid login user, cookie + redirect
XxlSsoUser xxlUser = SsoWebLoginHelper.loginCheck(req, res);
... xxlUser 没有xxlUser重新登录 有就可以登录访问系统
}
第二层
public static XxlSsoUser loginCheck(HttpServletRequest request, HttpServletResponse response){
String cookieSessionId = CookieUtil.getValue(request, Conf.SSO_SESSIONID);
// cookie user
XxlSsoUser xxlUser = SsoTokenLoginHelper.loginCheck(cookieSessionId);
if (xxlUser != null) {
return xxlUser;
}
// redirect user
// remove old cookie
SsoWebLoginHelper.removeSessionIdByCookie(request, response);
// set new cookie
String paramSessionId = request.getParameter(Conf.SSO_SESSIONID);
xxlUser = SsoTokenLoginHelper.loginCheck(paramSessionId);
if (xxlUser != null) {
CookieUtil.set(response, Conf.SSO_SESSIONID, paramSessionId, false); // expire when browser close (client cookie)
return xxlUser;
}
return null;
}
- 比较session key部分的version和取出的redis用户对象的version版本比较
- 如果有和上次的版本一致就刷新过期时间 直接到最上层返回用户对象
第三层 代码部分
/**
* login check
*
* @param sessionId
* @return
*/
public static XxlSsoUser loginCheck(String sessionId){
String storeKey = SsoSessionIdHelper.parseStoreKey(sessionId);
if (storeKey == null) {
return null;
}
XxlSsoUser xxlUser = SsoLoginStore.get(storeKey);
if (xxlUser != null) {
String version = SsoSessionIdHelper.parseVersion(sessionId);
if (xxlUser.getVersion().equals(version)) {
// After the expiration time has passed half, Auto refresh
if ((System.currentTimeMillis() - xxlUser.getExpireFreshTime()) > xxlUser.getExpireMinute()/2) {
xxlUser.setExpireFreshTime(System.currentTimeMillis());
SsoLoginStore.put(storeKey, xxlUser);
}
return xxlUser;
}
}
return null;
}
4.直接登录当前系统,并保存用户信息
第一层代码
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
... 其他判断
//valid login user, cookie + redirect
...
... xxlUser 没有xxlUser重新登录 有就可以登录访问系统
// ser sso user
request.setAttribute(Conf.SSO_USER, xxlUser);
// already login, allow
chain.doFilter(request, response);
return;
}
- 如果redis没有就返回最上层(第一层)让用户去登录
回到第二层代码
public static XxlSsoUser loginCheck(HttpServletRequest request, HttpServletResponse response){
String cookieSessionId = CookieUtil.getValue(request, Conf.SSO_SESSIONID);
// cookie user
XxlSsoUser xxlUser = SsoTokenLoginHelper.loginCheck(cookieSessionId);
...
SsoWebLoginHelper.removeSessionIdByCookie(request, response);
// set new cookie
String paramSessionId = request.getParameter(Conf.SSO_SESSIONID);
xxlUser = SsoTokenLoginHelper.loginCheck(paramSessionId);
...
return null;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
... 其他判断
//valid login user, cookie + redirect
...
... xxlUser 没有xxlUser重新登录 有就可以登录访问系统
// valid login fail
if (xxlUser == null) {
String header = req.getHeader("content-type");
boolean isJson= header!=null && header.contains("json");
if (isJson) {
// json msg
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 {
// total link
String link = req.getRequestURL().toString();
// redirect logout
String loginPageUrl = ssoServer.concat(Conf.SSO_LOGIN)
+ "?" + Conf.REDIRECT_URL + "=" + link;
res.sendRedirect(loginPageUrl);
return;
}
}
}
服务端代码分析
这里我们排除用户名密码输入错误的情况
服务端就是登录保存cookie逻辑
- 首先进入登录页面输入用户名密码登录
- 设置正确的用户对象信息
@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;
// valid login
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";
}
// 1、make xxl-sso user
XxlSsoUser xxlUser = new XxlSsoUser();
xxlUser.setUserid(String.valueOf(result.getData().getUserid()));
xxlUser.setUsername(result.getData().getUsername());
xxlUser.setVersion(UUID.randomUUID().toString().replaceAll("-", ""));
xxlUser.setExpireMinute(SsoLoginStore.getRedisExpireMinute());
xxlUser.setExpireFreshTime(System.currentTimeMillis());
// 2、make session id
String sessionId = SsoSessionIdHelper.makeSessionId(xxlUser);
// 3、login, store storeKey + cookie sessionId
SsoWebLoginHelper.login(response, sessionId, xxlUser, ifRem);
// 4、return, redirect sessionId
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:/";
}
}
3.对原来的sessionId处理由用户id和用户版本号构成
/**
* make client sessionId
*
* @param xxlSsoUser
* @return
*/
public static String makeSessionId(XxlSsoUser xxlSsoUser){
String sessionId = xxlSsoUser.getUserid().concat("_").concat(xxlSsoUser.getVersion());
return sessionId;
}
- 保存到cookie和redis当中
public static void login(HttpServletResponse response,
String sessionId,
XxlSsoUser xxlUser,
boolean ifRemember) {
String storeKey = SsoSessionIdHelper.parseStoreKey(sessionId);
if (storeKey == null) {
throw new RuntimeException("parseStoreKey Fail, sessionId:" + sessionId);
}
SsoLoginStore.put(storeKey, xxlUser);
CookieUtil.set(response, Conf.SSO_SESSIONID, sessionId, ifRemember);
}
- 其中redis的key是处理还原了取其中的用户id
/**
* parse storeKey from sessionId
*
* @param sessionId
* @return
*/
public static String parseStoreKey(String sessionId) {
if (sessionId!=null && sessionId.indexOf("_")>-1) {
String[] sessionIdArr = sessionId.split("_");
if (sessionIdArr.length==2
&& sessionIdArr[0]!=null
&& sessionIdArr[0].trim().length()>0) {
String userId = sessionIdArr[0].trim();
return userId;
}
}
return null;
}
5.最后通过登录接口返回原先需要认证的系统(客户端),通过客户端的逻辑完成登录流程
其他客户端登录
其他客户端登录由于cookie和redis中已经有值了,直接通过过滤器登陆成功