单点登陆

单系统登陆
普通的单个系统登陆流程是什么样子的呢?
用户访问系统,如果访问的是受限制的资源,比如http://localhost:8080/orderList,请求经过拦截器处理:

public class LoginInterceptor extends HandlerInterceptorAdapter{ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 取登录标记 HttpSession session = request.getSession(); Object key = session.getAttribute("session-user-info"); // 取请求路径,并将路径中的context path去除 String uri = request.getRequestURI().replace(request.getContextPath(),""); //如果是登陆方法,直接不拦截 if(uri.matches("/login") || uri.matches("/userLogin")){ return true; } // 如果用户没有登录或者session过期,则转到登录页面 if (session == null || key == null || key.toString().trim().length() == 0 || uri.matches("/")) { response.sendRedirect(request.getContextPath() + "/login"); return false; } return true; } }

对orderList这个uri的处理是直接跳转到登陆页面,在登陆页面用户输入用户名密码等信息后请求:http://localhost:8080/userLogin,拦截器判断是登陆请求,return true,交给下一个拦截器或者其他处理器处理,最终到达LoginController:
// 验证签名信息,验证时间戳信息,验证用户名是否存在,验证密码是否正确代码省略...... // 当验证用户名密码正确后,设置session HttpSession session = request.getSession(); session.setMaxInactiveInterval(60 * 60 * 10); //单位为秒,此处设最长时间为1年 request.getSession().setAttribute("user-info", user);

请求返回客户端浏览器的时候,会带上一个jsessionid放在客户端的cookie中,相当于session的一个唯一标示,下次再次请求服务端的时候,会把这个jsessionid一起带上。到这里一个简单的登陆流程就完成了。
单系统登陆存在的问题
就以我所处的公司为例,公司有自己的home系统,固资系统,运营系统,CRM系统,OA系统等等,都属于公司内部系统,如此多系统,一个一个去登陆,注销非常麻烦,这时候我们希望登陆其中一个系统(比如使用公司工号登陆)不需要再次登陆其他系统也可以访问这些系统。这时候我们单系统登陆就不再适合这种应用场景了。
问题1:cookie跨域问题
单系统登陆其中一个核心是cookie,cookie携带会话唯一id(上文提到的jsession)维持会话状态,多个系统使用不同的域名,那么多个系统生成的jsessionid是不会在同一个域的cookie下,浏览器发送请求的时候,只能带上当前域对应的cookie里面的jsessionid
问题2:不同应用服务器
如果将多个应用放在同一个域下,不就能解决问题1了吗?但是,这就需要这些放在同一个域下面的应用使用的是同一种技术,同一个web服务器,否则,放在cookie中的key值可能就不是叫JSESSIONID了。同时cookie本身的安全性也不高
为了解决以上这些问题,单点登录SSO出现啦~~
SSO原理
应用系统不提供登陆验证,认证中心来对每一个应用系统进行登陆验证,验证成功,即创建一个授权令牌给个子系统,子系统拿到令牌后创建局部会话,局部会话的登陆方式同单系统的登陆方式。流程如下:
1. 用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数 2. sso认证中心发现用户未登录,将用户引导至登录页面 3. 用户输入用户名密码提交登录申请 4. sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌 5. sso认证中心带着令牌跳转会最初的请求地址(系统1) 6. 系统1拿到令牌,去sso认证中心校验令牌是否有效 7. sso认证中心校验令牌,返回有效,注册系统1 8. 系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源 10. 系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数 11. sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌 12. 系统2拿到令牌,去sso认证中心校验令牌是否有效 13. sso认证中心校验令牌,返回有效,注册系统2 14. 系统2使用该令牌创建与用户的局部会话,返回受保护资源

【单点登陆】系统部署方式:
每一个子系统集群部署,并且我们会实现一个sso-client.jar,这个jar处理和sso相关的逻辑,在每一个子系统中引入这个sso-jar,认证中心单独部署为sso-server.war
伪代码(sso-client和sso-server之间的通信使用HttpClient):
1. sso-client拦截未登录请求
用户请求子系统的时候,我们需要对用户的请求做拦截,java中拦截用户请求有多种方式,servlet,filter,listener都可以,这里我们选用filter过滤器:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; HttpSession session = req.getSession(); if (session.getAttribute("isLogin")) { chain.doFilter(request, response); return; } //跳转至sso认证中心 res.sendRedirect("sso-server-url-with-system-url"); }

以上代码中的session就是子系统和用户之间的局部会话,如果局部会话是存在的,那么就通过过滤器,进行下一个过滤或者拦截直至访问受限资源。局部会话不存在的话,那么就需要跳转到sso认证中心了。
2. sso-server拦截未登录请求
sso-server拦截方式和sso-client基本一致,如果拦截到用户没有和sso-server创建令牌,那么跳转到登陆页面
3. sso-server验证用户登录信息并创建授权令牌
@RequestMapping("/login") public String login(String username, String password, HttpServletRequest req) { this.checkLoginInfo(username, password); req.getSession().setAttribute("isLogin", true); // 这里的session是全局会话 return "success"; }

sso-server创建令牌:
String token = UUID.randomUUID().toString(); // 使用redis来创建也可以,只要不重复,不容易伪造就行了 reids.hmapset(token, "子系统注册地址list集合"); // 将token作为key,子系统的注册地址作为集合存储在redis中,后续注销的时候就知道要注销哪些子系统的session了

4. sso-client取得令牌并校验
在sso-client的filter中加入代码来获取sso-server返回的token并验证这个token:
// 请求附带token参数 String token = req.getParameter("token"); if (token != null) { // 去sso认证中心校验token boolean verifyResult = this.verify("sso-server-verify-url", token); if (!verifyResult) { res.sendRedirect("sso-server-url"); return; } chain.doFilter(request, response); }

5. sso-server接收并处理校验令牌请求
sso-server拿到子系统的校验请求,验证token是否存在,是否过期,如果验证成功,将token和当前验证请求一起放入redis中
jedis.lpush(token, "验证请求list"); // 将验证请求和token绑定存到redis中的目的是为了后面注销系统的时候知道要注销哪些系统

到这里,SSO的登陆原理就差不多ok了

    推荐阅读