前幾篇說的都是基於session的SSO(客戶端應用的session、認證服務器的session),客戶端應用拿到認證服務器返回的token後,將其存在自己的session, 用戶登錄狀態是存在服務器端的。
本篇要說的是,要實現一個基於瀏覽器cookie的SSO,客戶端應用獲取到令牌後,不是將其存到session,而是寫入瀏覽器cookie,這個改變會帶來一些列問題,本篇將解決這些問題。
在OAuth授權回調裏處理
客戶端應用 客戶token後的改造,在OAuth授權回調裏處理,拿到token後寫入cookie:
CookieTokenFilter
在客戶端應用,引入zuul的依賴,寫一個CookieTokenFilter,從cookie拿出token 加在請求頭裏。
package com.nb.security.admin;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 從cookie獲取token,統一加到請求頭中去
*/
@Component
public class CookieTokenFilter extends ZuulFilter {
private RestTemplate restTemplate = new RestTemplate();
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
HttpServletResponse response = requestContext.getResponse();
String accessToken = getCookie("nb_access_token");
if(StringUtils.isNotBlank(accessToken)){
//令牌放到請求頭
requestContext.addZuulRequestHeader("Authorization","Bearer "+accessToken);
}else {
//從cookie把不到token說明token已過期,刷新令牌
String refreshToken = getCookie("nb_refresh_token");
if(StringUtils.isNotBlank(refreshToken)){
String oauthServiceUrl = "http://gateway.nb.com:9070/token/oauth/token";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);//不是json請求
//網關的appId,appSecret,需要在數據庫oauth_client_details註冊
headers.setBasicAuth("admin","123456");
MultiValueMap<String,String> params = new LinkedMultiValueMap<>();
params.add("refresh_token",refreshToken);//授權碼
params.add("grant_type","refresh_token");//授權類型-刷新令牌
HttpEntity<MultiValueMap<String,String>> entity = new HttpEntity<>(params,headers);
//刷新令牌的時候,可能refresh_token也過期了,這裏進行處理,讓用戶重新走授權流程
try{
ResponseEntity<AccessToken> newToken = restTemplate.exchange(oauthServiceUrl, HttpMethod.POST, entity, AccessToken.class);
//令牌放到請求頭
requestContext.addZuulRequestHeader("Authorization","Bearer "+newToken.getBody().getAccess_token());
//基於 Cookie的SSO,拿到token後寫入瀏覽器Cookie
Cookie accessTokenCookie = new Cookie("nb_access_token",newToken.getBody().getAccess_token());
accessTokenCookie.setMaxAge(newToken.getBody().getExpires_in().intValue()-3);//有效期
accessTokenCookie.setDomain("nb.com");//所有以nb.com結尾的二級域名都可以訪問到cookie
accessTokenCookie.setPath("/");
response.addCookie(accessTokenCookie);
Cookie refreshTokenCookie = new Cookie("nb_refresh_token",newToken.getBody().getRefresh_token());
refreshTokenCookie.setMaxAge(2592000);//這裏隨便寫一個很大的值(沒用),如果是過期的token服務器將處理的。
refreshTokenCookie.setDomain("nb.com");//所有以nb.com結尾的二級域名都可以訪問到cookie
refreshTokenCookie.setPath("/");
response.addCookie(refreshTokenCookie);
}catch (Exception e){
//有異常,重新登錄
requestContext.setSendZuulResponse(false);//zuul過濾器不往下走了
requestContext.setResponseStatusCode(500);//響應狀態碼
requestContext.setResponseBody("{\"message\":\"refresh fail\"}");
requestContext.getResponse().setContentType("application/json");
}
}else {
//沒用refresh——token,重新登錄
requestContext.setSendZuulResponse(false);//zuul過濾器不往下走了
requestContext.setResponseStatusCode(500);//響應狀態碼
requestContext.setResponseBody("{\"message\":\"refresh fail\"}");
requestContext.getResponse().setContentType("application/json");
}
}
return null;
}
private String getCookie(String name) {
String result = null;
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
Cookie[] cookies = request.getCookies();
for(Cookie cookie : cookies){
if(StringUtils.equals(cookie.getName(),name)){
result = cookie.getValue();
break;
}
}
return result;
}
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
}
客戶端判斷用戶登錄狀態
在前端服務器判斷用戶是否登錄,之前基於session的SSO的處理是,會在客戶端應用admin裏的session裏着token,往前端服務器發了一個/me請求,session如果有東西說明用戶已登錄,現在客戶端應用session裏已經不存token了,客戶端應用沒辦法知道你是否已經等了,所以這裏需要換一下,就換成,在客戶端應用的頁面,往網關發一個/api/user/me請求,因爲yml裏已經配置了,/api/開頭的請求,都會轉發到網關。
客戶端頁面的改造:
前端服務器Controller在基於session-token方案時候判斷用戶登錄狀態,不用了:
在網關上,由於從客戶端應用admin過來的請求,會在請求頭裏帶一個token,然後經過了網關的權限過濾器後,會從token解析出用戶名,放在請求頭傳下去:
這裏加一個MeFilter,排序Order在授權過濾器之後,專門映射處理/user/me請求,它不往任何一個服務轉發,只是從請求頭拿username,如果拿得到,就說明用戶是登錄狀態。
實驗
1,啓動4個服務
2,訪問客戶端應用 admin
3,點擊去登錄,跳轉到認證服務器的登錄頁
3,輸入用戶名aaa(隨便輸入,認證服務器沒校驗)密碼123456 (認證服務器寫死的),點擊登錄,可以看到,一級域名nb.com下的cookie裏已經存入了access_token、refresh_token 。
點擊【獲取訂單信息】按鈕,調用訂單服務,會攜帶cookie裏的token,然後在客戶端admin上, CookieTokenFilter 會從cookie裏讀取到access_token和refresh_token,攜帶到請求頭,轉發給網關,網關校驗token後,再將請求轉發給訂單服務。
到現在已經實現了基於cookie 的SSO,token信息是存在cookie裏的,客戶端應用的session裏沒有存token信息。
模擬access_token失效後,客戶端應用admin 拿refresh_token 去認證服務器換取access_token。
客戶端應用配置表裏,access_token失效時間是20秒,refresh_token 失效時間是30秒
訪問訂單服務 正常是70多毫秒,大概在17秒(cookie失效時間是20-3秒)後,可以看到訪問訂單服務時間是200多毫秒,此時在admin上是拿refresh_token去認證服務器刷新了acces_token。
30秒後,refresh_token也失效了,調用訂單服務,會返回異常,捕獲這個異常,前端做判斷,給用戶提示,讓用戶退出登錄。
logout
function logout() {
//1瀏覽器cookie失效掉
$.removeCookie('nb_access_token',{domain:'nb.com',path:'/'});
$.removeCookie('nb_refresh_token',{domain:'nb.com',path:'/'});
//2,將認證服務器的session失效, /logout 是SpringSecurity OAuth默認的退出過濾器
// org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
window.location.href = "http://auth.nb.com:9090/logout?redirect_uri=http://admin.nb.com:8080/index";
}
這樣就在refresh_token失效後,就完全退出登錄,跳轉客戶端的登錄頁
目前的架構是這樣的,token信息都存在了瀏覽器cookie,客戶端應用並沒有存token信息
基於cookie的SSO的優缺點
1,登錄狀態 用戶的登錄狀態存在了瀏覽器的cookie,當cookie裏的refresh_token失效的時候纔會去認證服務器做登錄 。這種方案不需要在認證服務器上設置有效期很長的session,只要一個很短的就可以了,比如30分鐘,因爲決定能不能訪問服務的不是認證服務器的session,而是瀏覽器cookie裏的refresh_token
2,安全性低,token存在了瀏覽器,有一定的風險(使用https,縮短access_token有效期)
3,可控性低,refresh_token和access_token存在了客戶的瀏覽器裏,沒辦法主動失效掉。
4,跨域:cookie只能放在nb.com ,只有nb.com的二級域名(admin.nb.com 、order.nb.com等)可以做SSO
好處:
複雜程度低,相對於基於session的SSO來說,只需要做access_token和refresh_token過期的處理
不佔服務器的資源,適合於海量用戶。
代碼github : https://github.com/lhy1234/springcloud-security/tree/chapt-5-7-tokensso 如果對你幫助了,給個小星星唄