首頁  >  文章  >  後端開發  >  單一登入原理和簡單實現

單一登入原理和簡單實現

PHPz
PHPz原創
2017-03-12 16:28:153743瀏覽

一、單一系統登入機制

1、http#無狀態協定

  web應用採用browser/ server架構,http作為通訊協定。 http是無狀態協議,瀏覽器的每一個請求,服務器會獨立處理,不與之前或之後的請求產生關聯,這個過程用下圖說明,三次請求/回應對之間沒有任何聯繫

單一登入原理和簡單實現

  但這也同時意味著,任何使用者都能透過瀏覽器存取伺服器資源,如果想保護伺服器的某些資源,必須限制瀏覽器請求;要限制瀏覽器請求,必須鑑別瀏覽器請求,回應合法請求,忽略非法請求;要鑑別瀏覽器請求,必須清楚瀏覽器請求狀態。既然http協定無狀態,那就讓伺服器和瀏覽器共同維護一個狀態吧!這就是會話機制

2、會話機制

  瀏覽器第一次請求伺服器,伺服器建立一個會話,並將會話的id作為回應的一部分傳送給瀏覽器,瀏覽器存儲會話id,並在後續第二次和第三次請求中帶上會話id,伺服器取得請求中的會話id就知道是不是同一個用戶了,這個過程用下圖說明,後續請求與第一次請求產生了關聯

單一登入原理和簡單實現

  伺服器在記憶體中保存會話物件,瀏覽器怎麼保存會話id呢?你可能會想到兩種方式

  1. 請求參數

  2. #cookie

#  將會話id作為每一個請求的參數,伺服器接收請求自然能解析參數獲得會話id,並藉此判斷是否來自同一會話,很明顯,這種方式不靠譜。那就瀏覽器自己來維護這個會話id吧,每次發送http請求時瀏覽器自動發送會話id,cookie機制正好用來做這件事。 cookie是瀏覽器用來儲存少量資料的機制,資料以」key/value「形式存儲,瀏覽器發送http請求時自動附帶cookie訊息

  tomcat會話機制當然也實作了cookie,造訪tomcat伺服器時,瀏覽器中可以看到一個名為「JSESSIONID」的cookie,這就是tomcat會話機制維護的會話id,使用了cookie的請求回應流程如下圖

單一登入原理和簡單實現

3、登入狀態

  有了會話機制,登入狀態就好明白了,我們假設瀏覽器第一次請求伺服器需要輸入使用者名稱與密碼驗證身份,伺服器拿到用戶名密碼去資料庫比對,正確的話說明目前持有這個會話的用戶是合法用戶,應該將這個會話標記為「已授權」或「已登入」等等之類的狀態,既然是會話的狀態,自然要保存在會話對像中,tomcat在會話對像中設定登入狀態如下


HttpSession session = request.getSession();
session.setAttribute("isLogin", true);

  使用者再次造訪時,tomcat在會話對象中查看登入狀態


HttpSession session = request.getSession();
session.getAttribute("isLogin");

  實作了登入狀態的瀏覽器請求伺服器模型如下圖描述

單一登入原理和簡單實現

  每次請求受保護資源時都會檢查會話物件中的登入狀態,只有isLogin=true 的會話才能訪問,登入機制因此而實現。

二、多系統的複雜性

  web系統早已從久遠的單系統發展成為如今由多系統組成的應用群,面對如此眾多的系統,用戶難道要一個一個登入、然後一個一個註銷嗎?就像下圖描述的這樣

單一登入原理和簡單實現

  web系統由單一系統發展成多系統所組成的應用群,複雜度應該由系統內部承擔,而不是使用者。無論web系統內部多麼複雜,對使用者而言,都是一個統一的整體,也就是說,使用者存取web系統的整個應用群組與存取單一系統一樣,登入/登出只要一次就夠了

單一登入原理和簡單實現

#  雖然單一系統的登入解決方案很完美,但對於多系統應用群組已經不再適用了,為什麼呢?

  單系統登入解決方案的核心是cookie,cookie攜帶會話id在瀏覽器與伺服器之間維護會話狀態。但cookie是有限制的,這個限制就是cookie的網域(通常對應網站的網域),瀏覽器發送http請求時會自動攜帶與該網域相符的cookie,而不是所有cookie

單一登入原理和簡單實現

  既然這樣,為什麼不將web應用群中所有子系統的域名統一在一個頂級域名下,例如“*.baidu.com”,然後將它們的cookie域設置為“baidu.com” ,這種做法理論上是可以的,甚至早期很多多系統登入就採用這種同域名共享cookie的方式。

  然而,可行並不代表好,共享cookie的方式存在眾多限制。首先,應用群組網域名稱得統一;其次,應用群組各系統使用的技術(至少是web伺服器)要相同,不然cookie的key值(tomcat為JSESSIONID)不同,無法維持會話,共享cookie的方式是無法實現跨語言技術平台登入的,例如java、php、.net系統之間;第三,cookie本身不安全

  因此,我們需要一種全新的登錄方式來實現多系統應用群的登錄,這就是單點登錄

#三、單點登錄

  什麼是單點登入?單點登入全稱Single Sign On(以下簡稱SSO),是指在多系統應用群中登錄一個系統,便可在其他所有系統中得到授權而無需再次登錄,包括單點登錄與單點註銷兩部分

1、登入

  相比於單系統登錄,sso需要一個獨立的認證中心,只有認證中心能接受用戶的用戶名密碼等安全信息,其他系統不提供登錄入口,只接受認證中心的間接授權。間接授權透過令牌實現,sso認證中心驗證使用者的使用者名稱密碼沒問題,建立授權令牌,在接下來的跳轉過程中,授權令牌作為參數傳送給各個子系統,子系統拿到令牌,即得到了授權,可以藉此建立局部會話,局部會話登入方式與單一系統的登入方式相同。這個過程,也就是單一登入的原理,用下圖說明

  下面對上圖簡單描述

  1. 用戶訪問系統1的受保護資源,系統1發現用戶未登錄,跳到sso認證中心,並將自己的地址作為參數

  2. sso認證中心發現用戶未登錄,將使用者引導至登入頁面

  3. 使用者輸入使用者名稱密碼提交登入申請

  4. sso認證中心校驗使用者訊息,建立使用者與sso認證中心之間的會話,稱為全域會話,同時建立授權令牌

  5. sso認證中心帶著令牌跳到最初的請求位址(系統1)

  6. 系統1拿到令牌,去sso認證中心校驗令牌是否有效

  7. #sso認證中心校驗令牌,返回有效,註冊系統1

  8. 系統1使用該令牌建立與使用者的會話,稱為局部會話,傳回受保護資源

  9. 使用者存取系統2的受保護資源

  10. 系統2發現使用者未登錄,跳到sso認證中心,並將自己的位址作為參數

  11. ##sso認證中心發現使用者已登錄,跳到系統2的位址,並附上令牌

  12. 系統2拿到令牌,去sso認證中心校驗令牌是否有效

  13. sso認證中心校驗令牌,傳回有效,註冊系統2

  14. 系統2使用該令牌建立與使用者的局部會話,返回受保護資源

  用户登录成功之后,会与sso认证中心及各个子系统建立会话,用户与sso认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过sso认证中心,全局会话与局部会话有如下约束关系

  1. 局部会话存在,全局会话一定存在

  2. 全局会话存在,局部会话不一定存在

  3. 全局会话销毁,局部会话必须销毁

  你可以通过博客园、百度、csdn、淘宝等网站的登录过程加深对单点登录的理解,注意观察登录过程中的跳转url与参数

2、注销

  单点登录自然也要单点注销,在一个子系统中注销,所有子系统的会话都将被销毁,用下面的图来说明

單一登入原理和簡單實現

  sso认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知所有注册系统执行注销操作

  下面对上图简要说明

  1. 用户向系统1发起注销请求

  2. 系统1根据用户与系统1建立的会话id拿到令牌,向sso认证中心发起注销请求

  3. sso认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址

  4. sso认证中心向所有注册系统发起注销请求

  5. 各注册系统接收sso认证中心的注销请求,销毁局部会话

  6. sso认证中心引导用户至登录页面

四、部署图

  单点登录涉及sso认证中心与众子系统,子系统与sso认证中心需要通信以交换令牌、校验令牌及发起注销请求,因而子系统必须集成sso的客户端,sso认证中心则是sso服务端,整个单点登录过程实质是sso客户端与服务端通信的过程,用下图描述

單一登入原理和簡單實現

  sso认证中心与sso客户端通信方式有多种,这里以简单好用的httpClient为例,web service、rpc、restful api都可以

五、实现

  只是简要介绍下基于java的实现过程,不提供完整源码,明白了原理,我相信你们可以自己实现。sso采用客户端/服务端架构,我们先看sso-client与sso-server要实现的功能(下面:sso认证中心=sso-server)

  sso-client

  1. 拦截子系统未登录用户请求,跳转至sso认证中心

  2. 接收并存储sso认证中心发送的令牌

  3. 与sso-server通信,校验令牌的有效性

  4. 建立局部会话

  5. 拦截用户注销请求,向sso认证中心发送注销请求

  6. 接收sso认证中心发出的注销请求,销毁局部会话

  sso-server

  1. 验证用户的登录信息

  2. 创建全局会话

  3. 创建授权令牌

  4. 与sso-client通信发送令牌

  5. 校验sso-client令牌有效性

  6. 系统注册

  7. 接收sso-client注销请求,注销所有会话

  接下来,我们按照原理来一步步实现sso吧!

1、sso-client拦截未登录请求

  java拦截请求的方式有servlet、filter、listener三种方式,我们采用filter。在sso-client中新建LoginFilter.java类并实现Filter接口,在doFilter()方法中加入对未登录用户的拦截


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");
}

2、sso-server拦截未登录请求

  拦截从sso-client跳转至sso认证中心的未登录请求,跳转至登录页面,这个过程与sso-client完全一样

3、sso-server验证用户登录信息

  用户在登录页面输入用户名密码,请求登录,sso认证中心校验用户信息,校验成功,将会话状态标记为“已登录”


@RequestMapping("/login")
public String login(String username, String password, HttpServletRequest req) {
    this.checkLoginInfo(username, password);
    req.getSession().setAttribute("isLogin", true);
    return "success";
}

4、sso-server创建授权令牌

  授权令牌是一串随机字符,以什么样的方式生成都没有关系,只要不重复、不易伪造即可,下面是一个例子


String token = UUID.randomUUID().toString();

5、sso-client取得令牌并校验

  sso认证中心登录后,跳转回子系统并附上令牌,子系统(sso-client)取得令牌,然后去sso认证中心校验,在LoginFilter.java的doFilter()中添加几行


// 请求附带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);
}

  verify()方法使用httpClient实现,这里仅简略介绍,httpClient详细使用方法请参考官方文档


HttpPost httpPost = new HttpPost("sso-server-verify-url-with-token");
HttpResponse httpResponse = httpClient.execute(httpPost);

6、sso-server接收并处理校验令牌请求

  用户在sso认证中心登录成功后,sso-server创建授权令牌并存储该令牌,所以,sso-server对令牌的校验就是去查找这个令牌是否存在以及是否过期,令牌校验成功后sso-server将发送校验请求的系统注册到sso认证中心(就是存储起来的意思)

  令牌与注册系统地址通常存储在key-value数据库(如redis)中,redis可以为key设置有效时间也就是令牌的有效期。redis运行在内存中,速度非常快,正好sso-server不需要持久化任何数据。

  令牌与注册系统地址可以用下图描述的结构存储在redis中,可能你会问,为什么要存储这些系统的地址?如果不存储,注销的时候就麻烦了,用户向sso认证中心提交注销请求,sso认证中心注销全局会话,但不知道哪些系统用此全局会话建立了自己的局部会话,也不知道要向哪些子系统发送注销请求注销局部会话

單一登入原理和簡單實現

7、sso-client校验令牌成功创建局部会话

  令牌校验成功后,sso-client将当前局部会话标记为“已登录”,修改LoginFilter.java,添加几行


if (verifyResult) {
    session.setAttribute("isLogin", true);
}

  sso-client还需将当前会话id与令牌绑定,表示这个会话的登录状态与令牌相关,此关系可以用java的hashmap保存,保存的数据用来处理sso认证中心发来的注销请求

8、注销过程

  用户向子系统发送带有“logout”参数的请求(注销请求),sso-client拦截器拦截该请求,向sso认证中心发起注销请求


String logout = req.getParameter("logout");
if (logout != null) {
    this.ssoServer.logout(token);
}

  sso认证中心也用同样的方式识别出sso-client的请求是注销请求(带有“logout”参数),sso认证中心注销全局会话


@RequestMapping("/logout")
public String logout(HttpServletRequest req) {
    HttpSession session = req.getSession();
    if (session != null) {
        session.invalidate();//触发LogoutListener
    }
    return "redirect:/";
}

  sso认证中心有一个全局会话的监听器,一旦全局会话注销,将通知所有注册系统注销


public class LogoutListener implements HttpSessionListener {
    @Override
    public void sessionCreated(HttpSessionEvent event) {}
    @Override
    public void sessionDestroyed(HttpSessionEvent event) {
        //通过httpClient向所有注册系统发送注销请求
    }
}

以上是單一登入原理和簡單實現的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn