Spring Cloud微服務安全實戰_6-5_jwt改造之日志以及錯誤處理(403/401)


到現在為止基於Jwt的認證和授權的改造已經完成了。在網關上,剛開始都是自己定義一系列的Filter實現認證和授權。現在已經沒有了這些過濾器,完全由SpringSecurity的過濾器接管了。

一、審計日志過濾器

現在來實現在SpringSecurity過濾器鏈上加入自己的邏輯,現在的過濾器鏈上只處理了認證和授權。其他的安全機制比如限流、日志,也需要加入SpringSecurity的實現。

 1,新建審計日志過濾器GatewayAuditLogFilter

安全處理的幾個步驟是   : 流控 - 認證 - 審計 - 授權  。

注意審計日志過濾器的位置,要添加在認證過濾器之后,所以GatewayAuditLogFilter  上不要直接加@Component 注解,加上該注解,springboot會自動把這個過濾器加在web過濾器鏈里,如果再自己配置其位置,就會加兩次。

 

 

package com.nb.security.filter;

import com.nb.security.entity.AuditLog;
import com.nb.security.service.IAuditLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;

/**
 * 審計日志過濾器:
 * 流控 - 認證 - 審計 - 授權
 *  這里不要聲名為spring的Component
 *  如果聲名了,springboot會自動把這個過濾器加在web過濾器鏈里,再自己配置其位置就會加兩次。
 */
public class GatewayAuditLogFilter extends OncePerRequestFilter {

    //@Autowired
    private IAuditLogService auditLogService;

    public GatewayAuditLogFilter(IAuditLogService auditLogService){
        this.auditLogService = auditLogService;
    }


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //認證過濾器會把jwt令牌轉換為Authentication放在SecurityContext安全上下文里,Principal就是申請令牌的用戶名
        String username = (String)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        //1,記錄日志
        AuditLog log = new AuditLog();
        log.setUsername(username);
        log.setMethod(request.getMethod());
        log.setPath(request.getRequestURI());
        log.setCreateTime(new Date());
        auditLogService.save(log);
        System.err.println("1 記錄日志 :" + log.toString());
        //2,調用其他過濾器鏈
        filterChain.doFilter(request,response);
        //3,更新日志
        log.setUpdateTime(new Date());
        log.setStatus(response.getStatus());
        auditLogService.updateById(log);
        System.err.println("3 更新日志 :" + log.toString());
    }
}

  

  

 

 

2,配置審計日志過濾器位置

 審計日志過濾器需要加在授權過濾器前面,因為授權過濾器會拋出401或403異常,都是由ExceptionTranslationFilter 過濾器處理的,所以把審計日志過濾器加在這個過濾器前面。

package com.nb.security.config;

import com.nb.security.GatewayWebSecurityExpressionHandler;
import com.nb.security.filter.GatewayAuditLogFilter;
import com.nb.security.service.IAuditLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfiguration;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.web.access.ExceptionTranslationFilter;

/**
 * 作為一個資源服務器存在
 */
@Configuration
@EnableResourceServer
public class GatewaySecurityConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private GatewayWebSecurityExpressionHandler gatewayWebSecurityExpressionHandler;

    @Autowired
    private IAuditLogService auditLogService;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        //資源服務器id
        //resources.resourceId("gateway");
        //注入自己的 表達式處理器
        resources.expressionHandler(gatewayWebSecurityExpressionHandler);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            //可以指定過濾器位置,加載授權過濾器前面
            //授權過濾器里,會拋出異常 401或403,這兩個異常拋出來后都會由ExceptionTranslationFilter來處理,所以加在這里
            .addFilterBefore(new GatewayAuditLogFilter(auditLogService), ExceptionTranslationFilter.class)
            .authorizeRequests()
            .antMatchers("/token/**").permitAll() //放過/token開頭的請求,是在申請令牌
            .anyRequest()
                //指定權限訪問規則,permissionService需要自己實現,返回布爾值,true-能訪問;false-無權限
                // 傳進去2個參數,1-當前請求 ,2-當前用戶
            .access("#permissionService.hasPermission(request,authentication)");

    }
}

3,實驗

  通過網關獲得一個token,再通過網關訪問創建訂單服務 http://localhost:9070/order/orders

  

 

 

   查看打印的日志信息,跟預期結果一致:

  

   審計日志表

  

 

 二、錯誤處理

  2.1 403 無權限處理

    默認的403無權限,是由  AccessDeniedHandler 接口的實現類  OAuth2AccessDeniedHandler 來處理的,返回的信息默認是這樣的

  

 

 

   可以自己指定403的響應信息,新建一個403處理器,繼承OAuth2AccessDeniedHandler  ,重寫其 handler方法即可

    

package com.nb.security;

import com.alibaba.druid.support.json.JSONUtils;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.nb.security.entity.AuditLog;
import com.nb.security.service.IAuditLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 自定義403異常處理器,可以自定義響應信息
 */
@Component
public class GatewayAccessDeniedHandler extends OAuth2AccessDeniedHandler {

    @Autowired
    private IAuditLogService auditLogService;

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException authException) throws IOException, ServletException {

        //更新日志信息
        Long logId = (Long)request.getAttribute("logId");
        if(logId != null){
            AuditLog log = new AuditLog();
            log.setUpdateTime(new Date());
            log.setStatus(response.getStatus());
            auditLogService.update(log,new UpdateWrapper<AuditLog>().eq("id",logId));
        }
        //super.handle(request, response, authException); //默認處理
        Map<String,Object> resultMap = new HashMap<>();
        resultMap.put("status",403);
        resultMap.put("msg","sorry! 403");
        response.getWriter().write(JSONUtils.toJSONString(resultMap));

        //通知審計日志過濾器,403已經被處理過的標識,那里加個判斷,否則就會更新兩次
        request.setAttribute("logUpdated","yes");
    }
}

  審計日志過濾器修改

  

  網關安全配置,配置自定義403 handler

  

 

   至此完成 自定義403 無權限的處理。

  2.2  401的處理

       

 

    401有兩種情況:

      1,令牌是錯的,就不會經過審計日志的過濾器(日志過濾器在認證之后)

         2,沒傳令牌,都會經過logFilter,又分兩種情況:1-通過權限認證(返回的401是微服務返回的),2-未通過權限認證

 

    網關安全處理,401默認的處理是 OAuth2AuthenticationEntryPoint

 

    

 

 

     GatewayAuthenticationEntryPoint代碼:

    

package com.nb.security;

import com.alibaba.druid.support.json.JSONUtils;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.nb.security.entity.AuditLog;
import com.nb.security.service.IAuditLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.client.http.AccessTokenRequiredException;
import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 自定義401處理
 */
@Component
public class GatewayAuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {

    @Autowired
    private IAuditLogService auditLogService;
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {


        //這里分情況:1,令牌是錯的,就不會經過審計日志的過濾器(日志過濾器在認證之后)
        //          2,沒傳令牌,都會經過logFilter,又分兩種情況:1-通過權限認證(返回的401是微服務返回的),2-未通過權限認證
        if(authException instanceof AccessTokenRequiredException){
            //說明沒傳令牌,但是已記錄401日志,這里更新日志
            Long logId = (Long)request.getAttribute("logId");
            if(logId != null){
                AuditLog log = new AuditLog();
                log.setUpdateTime(new Date());
                log.setStatus(response.getStatus());
                auditLogService.update(log,new UpdateWrapper<AuditLog>().eq("id",logId));
                System.err.println("自定義處理401,更新日志 logId=" + logId);
            }
        }else {
            //到這里說明令牌錯誤,沒有經過身份認證過濾器,沒記錄日志,就insert日志
            AuditLog log = new AuditLog();
            log.setUsername("");
            log.setMethod(request.getMethod());
            log.setPath(request.getRequestURI());
            log.setCreateTime(new Date());
            auditLogService.save(log);
            System.err.println("自定義處理401,新增日志");
        }
        super.commence(request,response,authException);
        //通知審計日志過濾器,401已經被處理過的標識,那里加個判斷,否則就會更新兩次
        request.setAttribute("logUpdated","yes");
    }
}

  因為現在網關安全配置里, 沒有經過認證的請求,也是可以進入權限處理邏輯 permissionService 的(之前都是http.anyRequest().authenticated() 所有的請求必須經過身份認證)

  修改自定義的權限處理邏輯,判斷Authentication 如果是 AnonymousAuthenticationToken 說明是沒經過認證的,

 

  沒有經過身份認證的請求,SpringSecurity會給一個匿名的 Authentication ----- AnonymousAuthenticationToken ,所以在自定義的權限處理邏輯里,判斷如果入參 Authentication 是AnonymousAuthenticationToken  類型,就拋出需要認證token異常。

  

 

 

  至此,401自定義處理也完成了。可以下載下代碼,跑一下看下打印日志。

  代碼 :https://github.com/lhy1234/springcloud-security/tree/chapt-6-4-log

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

 


免責聲明!

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



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