到現在為止基於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
歡迎關注個人公眾號一起交流學習:

