Spring Cloud微服務安全實戰_5-8_基於Cookie的SSO

前幾篇說的都是基於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 如果對你幫助了,給個小星星唄

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章