# 五、實現
只是簡要介紹下基于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向所有注冊系統發送注銷請求
}
}
```