上一篇通過網關,
解決了 問題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
歡迎關注個人公眾號一起交流學習: