Spring Cloud微服務安全實戰_4-9_用zuul網關解耦安全邏輯和業務邏輯


上一篇通過網關,

解決了 問題1:微服務場景下,客戶端訪問服務的復雜性
未解決 問題2:安全邏輯和業務邏輯的耦合;問題3:微服務過多對認證服務器的壓力增大

本篇將微服務里的安全相關的邏輯挪到網關上來,這樣就能解決這兩個問題。

 

 

 在之前的訂單服務里(資源服務器),主要做了兩件事:

1,認證,拿token去認證服務器驗令牌

 

 

2,授權,post請求的token必須要有write權限,get請求的token必需要有read權限

 有了網關之后,所有的請求都要走網關來轉發到微服務上,所以網關上處理認證和授權,之前篇章說的所有的認證機制都要加到網關上:認證、授權、審計、限流,

 

下面開始在網關上實現 認證、授權、審計、限流 

 1,認證Filter 

新建類過濾器 OAuthFilter,繼承 ZuulFilter,重寫其方法

package com.nb.security.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.constants.ZuulConstants;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.FilterType;
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.HttpServletRequest;

/**
 * OAuth認證過濾器
 * Created by: 李浩洋 on 2019-12-28
 **/
@Slf4j
@Component
public class OAuthFilter extends ZuulFilter {

    private RestTemplate restTemplate = new RestTemplate();

    /**
     * 過濾器類型:
     *  "pre":在業務邏輯執行之前執行run()的邏輯
     *  "post":在業務邏輯執行之后執行run()的邏輯
     *  "error":在業務邏輯拋出異常執行run()的邏輯
     *  "route":控制路由,一般不用這個,zuul已實現
     * @return
     */
    @Override
    public String filterType() {
        return "pre";
    }

    //執行順序
    @Override
    public int filterOrder() {
        return 1;
    }

    //是否過濾
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 具體的業務邏輯
     * 這里是認證邏輯,
     */
    @Override
    public Object run() throws ZuulException {
        log.info("oauth start ");
        //獲取請求和響應
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        if(StringUtils.startsWith(request.getRequestURI(),"/token")){
            // /token開頭的請求,是發往認證服務器的請求,獲取token的,直接放行
            return null;
        }
        //獲取請求頭的token
        String authHeader = request.getHeader("Authorization");

        if(StringUtils.isBlank(authHeader)){
            //如果請求頭沒有帶token,不管認證信息有沒有,對不對,都往下走,(要做審計日志)
            return null;
        }
        if(!StringUtils.startsWithIgnoreCase(authHeader,"bearer ")){
            //這個過濾器只處理OAuth認證的請求,不是OAuth的token(如 HTTP basic),也往下走
            return null;
        }
        //走到這里,說明攜帶的OAuth認證的請求,驗token
        try {
            TokenInfo info = getTokenInfo(authHeader);
            request.setAttribute("tokenInfo",info);
        }catch (Exception e){
            log.info("獲取tokenInfo 失敗!",e);
        }
        return null;
    }

    /**
     * 去認證服務器校驗token
     * @param authHeader
     * @return
     */
    private TokenInfo getTokenInfo(String authHeader) {

        //截取請求頭里的bearer token
        String token = StringUtils.substringAfter(authHeader,"bearer ");
        //認證服務器驗token地址 /oauth/check_token 是  spring .security.oauth2的驗token端點
        String oauthServiceUrl = "http://localhost:9090/oauth/check_token";

        HttpHeaders headers = new HttpHeaders();//org.springframework.http.HttpHeaders
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);//不是json請求
        //網關的appId,appSecret,需要在數據庫oauth_client_details注冊
        headers.setBasicAuth("gateway","123456");

        MultiValueMap<String,String> params = new LinkedMultiValueMap<>();
        params.add("token",token);

        HttpEntity<MultiValueMap<String,String>> entity = new HttpEntity<>(params,headers);
        ResponseEntity<TokenInfo> response = restTemplate.exchange(oauthServiceUrl, HttpMethod.POST, entity, TokenInfo.class);

        log.info("token info : {}",response.getBody().toString());

        return response.getBody();//返回tokenInfo
    }
}

TokenInfo封裝token信息:

package com.nb.security.filter;

import lombok.Data;

import java.util.Date;

/**
 * 包裝從認證服務器獲取token信息響應對象
 */
@Data
public class TokenInfo {

    //token是否可用
    private boolean active;

    //令牌發給那個客戶端應用的 客戶端id
    private String client_id;

    //令牌scope
    private String[] scope;

    //用戶名
    private String user_name;

    //令牌能訪問哪些資源服務器,資源服務器的id
    private String[] aud;
    //令牌過期時間
    private Date exp;
    //令牌對應的user的 權限集合 UserDetailsService里loadUserByUsername()返回的User的權限集合
    private String[] authorities;
}

 2,審計日志Filter

 審計日志過濾器,請求過來的時候,記錄一條日志,請求出去的時候更新日志
package com.nb.security.filter;

import com.nb.security.entity.AuditLog;
import com.nb.security.service.IAuditLogService;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import jdk.nashorn.internal.parser.Token;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;

/**
 * 審計過濾器
 * 1流控--2認證--3審計--4授權
 */
@Slf4j
@Component
public class AuditLogFilter extends ZuulFilter {

    @Autowired
    private IAuditLogService auditLogService;

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 2; //在OAuthFilter后
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {

        log.info(" audit log insert ....");

        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        AuditLog log = new AuditLog();
        log.setCreateTime(new Date());
        log.setPath(request.getRequestURI());
        log.setMethod(request.getMethod());
        TokenInfo info = (TokenInfo) request.getAttribute("tokenInfo");
        if(info != null){
            log.setUsername(info.getUser_name());
        }
        auditLogService.save(log);
        request.setAttribute("auditLogId",log.getId());
        return null;
    }
}

 3,授權過濾器

在授權過濾器里,需要自己去查數據庫,判斷當前用戶是否有權限。

package com.nb.security.filter;

import com.nb.security.entity.AuditLog;
import com.nb.security.service.IAuditLogService;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;

/**
 * 授權過濾器
 */
@Slf4j
@Component
public class AuthorizationFilter extends ZuulFilter {

    @Autowired
    private IAuditLogService auditLogService;

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 3; //在審計過濾器后
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        log.info("authorization start");

        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();


        //判斷是否需要認證
        if(isNeedAuth(request)){
            //需要認證,從request取出AuthFilter放入的tokenInfo
            TokenInfo tokenInfo = (TokenInfo)request.getAttribute("tokenInfo");
            if(tokenInfo != null && tokenInfo.isActive()){//不為空且為激活狀態
                //認證成功,看是否有權限
                if(!hasPermission(tokenInfo,request)){
                    //沒有權限
                    log.info("audit log update fail 403 ");
                    //更新審計日志 ,403
                    Long auditLogId = (Long)request.getAttribute("auditLogId");
                    AuditLog log = auditLogService.getById(auditLogId);
                    log.setUpdateTime(new Date());
                    log.setStatus(403);
                    auditLogService.updateById(log);

                    handleError(403,requestContext);
                }
                //走到這里說明權限也通過了,將用戶信息放到請求頭,供其他微服務獲取
                requestContext.addZuulRequestHeader("username",tokenInfo.getUser_name());


            }else{
                //不是以 /token開頭的,才攔截,否則登錄請求也就被攔截了。這里放過
                if(!StringUtils.startsWith(request.getRequestURI(),"/token")){
                    //////////更新審計日志////////////////
                    log.info("audit log update fail 401 ");
                    Long auditLogId = (Long)request.getAttribute("auditLogId");
                    AuditLog log = auditLogService.getById(auditLogId);
                    log.setUpdateTime(new Date());
                    log.setStatus(401);
                    auditLogService.updateById(log);

                    //認證失敗,沒有tokenInfo,報錯,修改審計日志狀態
                    handleError(401,requestContext);
                }
            }
        }
        return null;
    }

    /**
     * 認證成功,看是否有權限
     * TODO:從數據庫查詢權限,這里直接返回
     * @param tokenInfo
     * @param request
     * @return
     */
    private boolean hasPermission(TokenInfo tokenInfo, HttpServletRequest request) {
        return true;//RandomUtils.nextInt() % 2 == 0;
    }


    /**
     * 處理認證失敗或者沒有權限
     * @param status http狀態碼
     * @param requestContext
     */
    private void handleError(int status, RequestContext requestContext) {
        requestContext.getResponse().setContentType("application/json");//響應json
        requestContext.setResponseStatusCode(status);//響應狀態碼
        requestContext.setResponseBody("{\"message\":\"auth fail\"}");
        requestContext.setSendZuulResponse(false);//這一句是說,當前過濾器到此返回,不會再往下走了、
    }

    /**
     * 判斷當前請求是否需要認證
     * TODO:查數據庫判斷權限
     * @param request
     * @return
     */
    private boolean isNeedAuth(HttpServletRequest request) {
        return true;
    }
}

實驗 

依次啟動訂單,認證,網關 三個微服務

 

 

 

在OAuth客戶端配置的表里,配上網關的appId,appSecret,使其成為一個OAuth客戶端。注意,一定要把client_secret配置正確,配置錯誤會一直報 HttpClientErrorException$Unauthorized: 401 null異常。

 

 訪問網關獲取token:

 

 

 

 

 

 訪問網關,創建訂單:

 

 

 一切還算順利。下面開始刪掉訂單服務里,關於安全的一些個代碼:

訂單服務里,刪除oauth2的maven依賴,刪除跟資源服務器相關的一切代碼,只剩下如下干凈的代碼:

 

 

 目前在其他微服務中獲取用戶信息的辦法是,在網關的授權過濾器中,當一切條件都通過后,將用戶信息,添加到Zuul的請求頭里,在其他微服務,就可以從請求頭中獲取用戶信息了,甚至可以穿進去一個json字符串,然后取的時候將json字符串轉換為對象。(這種做法不好,后續文章介紹其他方法)

 

 

 重復上邊的實驗步驟,依然可以從網關獲取token,創建訂單!

 

代碼github:https://github.com/lhy1234/springcloud-security/tree/chapt-4-9-gateway02

歡迎關注個人公眾號一起交流學習:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM