前幾篇說的都是基於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 如果對你幫助了,給個小星星唄
歡迎關注個人公眾號一起交流學習: