单点登陆

作者: 小吴可是全村的希望 | 来源:发表于2018-05-17 11:10 被阅读0次

    单系统登陆

    普通的单个系统登陆流程是什么样子的呢?
    用户访问系统,如果访问的是受限制的资源,比如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系统等等,都属于公司内部系统,如此多系统,一个一个去登陆,注销非常麻烦,这时候我们希望登陆其中一个系统(比如使用公司工号登陆)不需要再次登陆其他系统也可以访问这些系统。这时候我们单系统登陆就不再适合这种应用场景了。

    <font color='red'>问题1:cookie跨域问题</font>
    单系统登陆其中一个核心是cookie,cookie携带会话唯一id(上文提到的jsession)维持会话状态,多个系统使用不同的域名,那么多个系统生成的jsessionid是不会在同一个域的cookie下,浏览器发送请求的时候,只能带上当前域对应的cookie里面的jsessionid

    <font color='red'>问题2:不同应用服务器</font>
    如果将多个应用放在同一个域下,不就能解决问题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):

    <font color='red'>1. sso-client拦截未登录请求</font>
    用户请求子系统的时候,我们需要对用户的请求做拦截,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认证中心了。

    <font color='red'>2. sso-server拦截未登录请求</font>
    sso-server拦截方式和sso-client基本一致,如果拦截到用户没有和sso-server创建令牌,那么跳转到登陆页面

    <font color='red'>3. sso-server验证用户登录信息并创建授权令牌</font>

    @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了
    

    <font color='red'>4. sso-client取得令牌并校验</font>
    在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);
    }
    

    <font color='red'>5. sso-server接收并处理校验令牌请求</font>
    sso-server拿到子系统的校验请求,验证token是否存在,是否过期,如果验证成功,将token和当前验证请求一起放入redis中

    jedis.lpush(token, "验证请求list");
    // 将验证请求和token绑定存到redis中的目的是为了后面注销系统的时候知道要注销哪些系统
    

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

    相关文章

      网友评论

        本文标题:单点登陆

        本文链接:https://www.haomeiwen.com/subject/leqbdftx.html