做一個RBAC權限

在分布式應用場景下,我們可以利用網關對請求進行集中處理,實現了低耦合,高內聚的特性。

登陸權限驗證和鑒權的功能都可以在網關層面進行處理:

  • 用戶登錄后簽署的jwt保存在header中,用戶信息則保存在redis中
  • 網關應該對不需要登錄即可查看的網頁請求放行,即需要一個白名單存放放行請求路徑
  • gateway 依賴包已經包含了webflux組件,能夠有效利用線程資源,提高效率,減少不必要的阻塞時間
  • spring-cloud-starter-loadbalancer 通過服務名調用并自動輪詢實例,可以在后端代碼(WebClient + @LoadBalanced)中向其他服務請求數據庫數據
  • 負載均衡就是把大量請求按照某種算法分攤到多個后端實例上,以提高吞吐量、避免單點熱點,并在實例掛掉時自動剔除。

在這里插入圖片描述
從上至下:
定義一個日志記錄器,用于打印日志信息。
從配置文件中讀取 JWT 的密鑰。
Jackson 提供的 工具類,用于序列化/反序列化 JSON。
注入你自定義的網關配置類(GatewayConfig)。
用于發起 HTTP 請求,特別是微服務之間的調用。
從配置文件中讀取某個網關地址或標識。

public class AuthFilter implements GlobalFilter,先要實現這個接口

先看幾個輔助函數:

1、writeJson : 渲染json數據到前端,這里用來將錯誤情況下的信息返回給前端,封裝了狀態碼,響應頭等數據

private Mono<Void> writeJson(ServerHttpResponse resp, HttpStatus status, ResponseResult jsonResult) {resp.setStatusCode(status);//401狀態碼resp.getHeaders().add("Content-Type", "application/json;charset=utf-8");try {String json = objectMapper.writeValueAsString(jsonResult);DataBuffer db = resp.bufferFactory().wrap(json.getBytes(StandardCharsets.UTF_8));return resp.writeWith(Mono.just(db));} catch (JsonProcessingException ex) {throw new RuntimeException("json序列化異常", ex);}}

這 6 行代碼就是 WebFlux 版“返回 JSON 響應”的模板:
設狀態碼 → 設頭 → 序列化 → 包成 Netty 緩沖區 → 異步寫出 → 異常兜底。

這里展示一下Mono< T>的區別

對比維度Mono<String>Mono<Void>
語義0~1 個字符串0 個數據(只發 onComplete/onError
能拿到值嗎?可以 block() / subscribe(s -> …) 拿到具體字符串拿不到任何值;只能知道“事件結束”或“出錯”
典型用途查數據庫、調接口、讀文件……有返回體寫響應、刪數據、發消息……只關心成功/失敗
序列化內容會把字符串寫出去沒有 body,只能寫狀態碼/頭
實際發出的 HTTP 包有 Content-Length > 0 的 bodybody 長度為 0(只有響應行+頭)

2、Java的record

private record UriPattern(String[] allowedMethods, String pattern) {//判斷是否匹配某個請求類型private boolean matchMethod(String method) {for (String am : allowedMethods) {if (am.equalsIgnoreCase(method) || "*".equals(am)) {return true;}}return false;}private static UriPattern of(String uri) {String[] parts = uri.split(":");if (parts.length == 1) {return new UriPattern(new String[]{"*"}, parts[0]);} else {return new UriPattern(parts[0].split(","), parts[1]);}}}

等效于下面的類:

import java.util.Arrays;
import java.util.Objects;public final class UriPattern {/* 1. 字段 */private final String[] allowedMethods;private final String pattern;/* 2. 全參構造器 */public UriPattern(String[] allowedMethods, String pattern) {// 防御性復制,防止外部數組被修改this.allowedMethods = Arrays.copyOf(allowedMethods, allowedMethods.length);this.pattern = pattern;}/* 3. 業務方法:判斷方法是否允許 */public boolean matchMethod(String method) {for (String am : allowedMethods) {if (am.equalsIgnoreCase(method) || "*".equals(am)) {return true;}}return false;}/* 4. 靜態工廠:解析配置串  "GET,POST:/api/user/**"  */public static UriPattern of(String uri) {String[] parts = uri.split(":");if (parts.length == 1) {// 只有 URI 模式,方法默認通配return new UriPattern(new String[]{"*"}, parts[0]);} else {// parts[0] 是方法列表,parts[1] 是 URI 模式return new UriPattern(parts[0].split(","), parts[1]);}}/* 5. getter(可選,方便外部讀取) */public String[] getAllowedMethods() {return Arrays.copyOf(allowedMethods, allowedMethods.length);}public String getPattern() {return pattern;}/* 6. equals / hashCode / toString  與 record 默認邏輯一致 */@Overridepublic boolean equals(Object o) {if (this == o) return true;if (!(o instanceof UriPattern)) return false;UriPattern that = (UriPattern) o;return Arrays.equals(allowedMethods, that.allowedMethods)&& Objects.equals(pattern, that.pattern);}@Overridepublic int hashCode() {int result = Objects.hash(pattern);result = 31 * result + Arrays.hashCode(allowedMethods);return result;}@Overridepublic String toString() {return "UriPattern[allowedMethods=" + Arrays.toString(allowedMethods)+ ", pattern=" + pattern + ']';}
}

of工廠類最為關鍵,作用是:

配置字符串split(“:”) 結果parts.length進入分支最終 UriPattern
get:/api/v1/user/captcha/**["get", "/api/v1/user/captcha/**"]2elseallowedMethods=["get"], pattern=/api/v1/user/captcha/**
/api/v1/user/captcha/**["/api/v1/user/captcha/**"]1ifallowedMethods=["*"], pattern=/api/v1/user/captcha/**
GET,POST:/api/health["GET,POST", "/api/health"]2elseallowedMethods=["GET","POST"], pattern=/api/health
*["*"]1ifallowedMethods=["*"], pattern=*

3、hasPerm,檢查請求的uri是否是有權限的,鑒權核心

private boolean hasPerm(String uri, String method, List<String> userPerms, Map<String, List<String>> resourcePermMappings) {PathMatcher matcher = new AntPathMatcher();//1.找到訪問uri所需要的權限for (Map.Entry<String, List<String>> entry : resourcePermMappings.entrySet()) {String resource = entry.getKey();//資源,模式List<String> perms = entry.getValue();UriPattern up = UriPattern.of(resource);if (matcher.match(up.pattern(), uri) && up.matchMethod(method)) {for (String perm : perms) {if (userPerms.contains(perm)) {return true;}}}}return false;}

按資源模式輪詢 → 路徑+方法匹配 → 任一所需權限在用戶列表里即立即放行,全掃完還沒命中就拒絕。

理解這三個輔助方法,思路就很明確了,我們要讀取前端發過來的請求,并通過負載均衡輪詢方式請求rbac服務的資源映射關系,之后就能夠利用hasPerm方法來判斷了,有錯的就返回對應的錯誤信息即可,難點是解析uri

別忘了jwt驗證要先做,之后再鑒權

全部代碼如下:

@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest req = exchange.getRequest();String uri = req.getPath().toString();//當前請求路徑String ctx = req.getPath().contextPath().value();//上下文路徑uri = uri.replace(ctx, "");//無上下文的請求路徑String method = req.getMethod().toString();final String finalUri = uri;//對白名單中的地址直接放行List<String> ignoreUrls = gatewayConfig.getWhiteList();PathMatcher matcher = new AntPathMatcher();for (String pattern : ignoreUrls) {UriPattern up = UriPattern.of(pattern);if (matcher.match(up.pattern(), uri) && up.matchMethod(method)) {return chain.filter(exchange);//直接放行}}String jwt = req.getHeaders().getFirst("Authorization");if (!StringUtils.hasText(jwt)) {jwt = req.getQueryParams().getFirst("jwt");}if (StringUtils.hasText(jwt)) {//校驗tokenJWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(jwtSecret)).build();//從jwt中取出有效數據,進行業務使用,如從Redis中獲取用戶數據try {DecodedJWT dj = jwtVerifier.verify(jwt);//從session或Redis中取出用戶數據Integer userId = dj.getClaim("userId").asInt();//用戶idString username = dj.getAudience().get(0);//用戶名//鑒權System.out.println(userId + ":" + username);//1.找出當前請求所需要的權限,resource_perm_mappingsWebClient webClient = webClientBuilder.build();return webClient.get().uri("http://" + gateway + "/api/v1/rbac/resource_perm_mappings").retrieve().bodyToMono(ResponseResult.class).flatMap(it -> {Map<String, List<String>> resourcePermMappings = (Map<String, List<String>>) it.getData();//2.找出當前用戶所有的權限return webClient.get().uri("http://" + gateway + "/api/v1/rbac/perms/{userId}/true", userId).retrieve().bodyToMono(ResponseResult.class).flatMap(it1 -> {List<String> perms = (List<String>) it1.getData();//當前用戶擁有的所有權限if (hasPerm(finalUri, method, perms, resourcePermMappings)) {//return chain.filter(exchange);//放行String authInfo = userId + ":" + username;authInfo = Base64.getEncoder().encodeToString(authInfo.getBytes());ServerHttpRequest mutatedRequest = exchange.getRequest().mutate().header("x-auth-info", authInfo).build();return chain.filter(exchange.mutate().request(mutatedRequest).build());} else {//鑒權未通過return writeJson(exchange.getResponse(), HttpStatus.FORBIDDEN, ResponseResult.errorResult(-1,"無權訪問"));}});});} catch (JWTVerificationException e) {log.log(Level.SEVERE, "jwt校驗異常", e);return writeJson(exchange.getResponse(), HttpStatus.UNAUTHORIZED,ResponseResult.errorResult(-1,"jwt無效或已過期"));}} else {return writeJson(exchange.getResponse(), HttpStatus.UNAUTHORIZED, ResponseResult.errorResult(-1,"無jwt,請重新認證"));}}

問題來了:A服務遠程調用B服務時,該怎么確定他有沒有權限呢?

答:通過AOP切面編程,先鑒權再進入方法執行。

具體做法:

1、獲取用戶信息放到當前線程中:(Servlet 容器(Tomcat)默認是“一個請求 = 一條線程”全程處理)

上面的鑒權系統,只能進行鑒權,無法將已經鑒權的用戶信息進行保存,因此這里要新建一個攔截器來進行操作

public class AuthHandlerInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest req, @NonNull HttpServletResponse resp, @NonNull Object handler) throws Exception {//從請求頭中獲取認證信息,經過base64編碼過的String base64AuthInfo = req.getHeader(AuthInfo.AUTH_INFO_HEADER_KEY);if (base64AuthInfo == null) {base64AuthInfo = req.getParameter(AuthInfo.AUTH_INFO_HEADER_KEY);//嘗試從請求參數中獲取(針對特殊情況)}if (StringUtils.hasText(base64AuthInfo)) {//base64解碼String authInfo = new String(getDecoder().decode(base64AuthInfo));String[] arr = authInfo.split(":");String userId = arr[0];//用戶id,非主鍵String userName = arr[1];//用戶名//設置到當前線程中AuthInfo.setCurrent(AuthInfo.of(userId, userName));}return true;}
}

這里的AuthInfo是自定義類:

/*** 認證信息記錄類,用于封裝用戶認證相關信息* 使用Java record類型實現,自動生成構造方法、getter、equals、hashCode和toString方法*/
public record AuthInfo(String userId, String username) {/*** 認證信息請求頭名稱常量*/public static final String AUTH_INFO_HEADER_KEY = "x-auth-info";/*** 空認證信息實例,表示匿名用戶*/public static final AuthInfo EMPTY = AuthInfo.of("0", "匿名用戶");/*** 線程本地變量,用于存儲當前線程的認證信息*/private static final ThreadLocal<AuthInfo> INFO_THREAD_LOCAL = new ThreadLocal<>();/*** 創建認證信息實例的工廠方法* @param userId 用戶ID* @param userName 用戶名* @return 新的AuthInfo實例*/public static AuthInfo of(String userId, String userName) {return new AuthInfo(userId, userName);}/*** 獲取當前線程的認證信息* @return 當前線程的認證信息,如果不存在則返回空認證信息*/public static AuthInfo current() {AuthInfo ai = INFO_THREAD_LOCAL.get();return ai != null ? ai : EMPTY;}/*** 設置當前線程的認證信息* @param info 要設置的認證信息,不能為null* @throws NullPointerException 如果傳入的認證信息為null*/public static void setCurrent(AuthInfo info) {Objects.requireNonNull(info, "用戶認證信息不可為空");INFO_THREAD_LOCAL.set(info);}public static void clear() {INFO_THREAD_LOCAL.remove();}
}

2、創建方法攔截器,并加上增加/修改時間等信息

/*** aop,攔截所有業務操作,如果是保存或修改操作,如果參數是繼承自AuditEntity的,則自動填充創建時間和更新時間*/
public class AutoAuditMethodInterceptor implements MethodInterceptor {@Overridepublic Object invoke(@NonNull MethodInvocation invocation) throws Throwable {String method = invocation.getMethod().getName();if (method.equals("save") || method.equals("update")) {Object[] args = invocation.getArguments();if (args.length > 0) {Object arg0 = args[0];//首參if (arg0 instanceof BaseModel ae) {AuthInfo authInfo = AuthInfo.current();ae.setUpdatedBy(authInfo.username());ae.setUpdatedTime(LocalDateTime.now());if (method.equals("save")) {ae.setCreatedBy(authInfo.username());ae.setCreatedTime(LocalDateTime.now());}}}}return invocation.proceed();}
}

3、創建全局配置類

/*** 用于微服務自動獲取當前認證用戶信息的自動配置。* 當前配置類和Advisor上添加@Role(BeanDefinition.ROLE_INFRASTRUCTURE),是為了避免進行代理檢查,導致控制臺出現警告(對程序正常運行無影響)*/
@AutoConfiguration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class GlobalAutoConfiguration implements WebMvcConfigurer {private ApplicationContext applicationContext;@Autowiredpublic void setApplicationContext(ApplicationContext applicationContext) {this.applicationContext = applicationContext;}@Overridepublic void addInterceptors(@NonNull InterceptorRegistry registry) {try {AuthHandlerInterceptor interceptor = applicationContext.getBean(AuthHandlerInterceptor.class);registry.addInterceptor(interceptor);} catch (BeansException e) {//do nothing...}}/*** 創建攔截器,獲取當前認證用戶信息** @return 攔截器實例*/@ConditionalOnProperty(prefix = "shoplook2025.services.auto-get-auth", name = "enabled", havingValue = "true", matchIfMissing = true)@ConditionalOnMissingBean(name = "authHandlerInterceptor")@Beanpublic AuthHandlerInterceptor authHandlerInterceptor() {return new AuthHandlerInterceptor();}/*** 創建切面,自動為審計類型的模型類,添加創建時間和更新時間,以及創建人、更新人* 注意:必須必須添加@EnableAspectJAutoProxy注解,否則Advisor不生效** @return 切面實例*/@Role(BeanDefinition.ROLE_INFRASTRUCTURE)@Beanpublic Advisor autoAuditAspect() {AspectJExpressionPointcut pc = new AspectJExpressionPointcut();//execution表達式默認僅匹配指定類中直接聲明的方法。若方法定義在父類或接口中,但未被實現類重寫,則不會被識別pc.setExpression("execution(* com.situ.shoplook2025.*.api.service.impl.*.*(..))");return new DefaultPointcutAdvisor(pc, new AutoAuditMethodInterceptor());}
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/922417.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/922417.shtml
英文地址,請注明出處:http://en.pswp.cn/news/922417.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【算法】day1 雙指針

1、移動零&#xff08;同向分3區域&#xff09; 283. 移動零 - 力扣&#xff08;LeetCode&#xff09; 題目&#xff1a; 思路&#xff1a;注意原地操作。快排也是這個方法&#xff1a;左邊小于等于 tmp&#xff0c;右邊大于 tmp&#xff0c;最后 tmp 放到 dest。 代碼&#…

Linux 日志分析:用 ELK 搭建個人運維監控平臺

Linux 日志分析&#xff1a;用 ELK 搭建個人運維監控平臺 &#x1f31f; Hello&#xff0c;我是摘星&#xff01; &#x1f308; 在彩虹般絢爛的技術棧中&#xff0c;我是那個永不停歇的色彩收集者。 &#x1f98b; 每一個優化都是我培育的花朵&#xff0c;每一個特性都是我放飛…

Linux網絡:socket編程UDP

文章目錄前言一&#xff0c;socket二&#xff0c;服務端socket3-1 創建socket3-2 綁定地址和端口3-3 接收數據3-4 回復數據3-5關閉socket3-6 完整代碼三&#xff0c;客戶端socket3-1 為什么客戶端通常不需要手動定義 IP 和端口前言 學習 socket 編程的意義在于&#xff1a;它讓…

【從零到公網】本地電腦部署服務并實現公網訪問(IPv4/IPv6/DDNS 全攻略)

從零到公網&#xff1a;本地電腦部署服務并實現公網訪問&#xff08;IPv4/IPv6/DDNS 全攻略&#xff09; 適用場景&#xff1a;本地 API 服務、大模型推理服務、NAS、遠程桌面等需要公網訪問的場景 關鍵詞&#xff1a;公網 IP、端口映射、內網穿透、IPv6、Cloudflare DDNS 一、…

模塊二 落地微服務

11 | 服務發布和引用的實踐 服務發布和引用常見的三種方式&#xff1a;Restful API、XML配置以及IDL文件。今天我將以XML配置方式為例&#xff0c;給你講解服務發布和引用的具體實踐以及可能會遇到的問題。 XML配置方式的服務發布和引用流程 1. 服務提供者定義接口 服務提供者發…

C++程序員速通C#:從Hello World到數據類型

C程序員光速入門C#&#xff08;一&#xff09;&#xff1a;總覽、數據類型、運算符 一.Hello world&#xff01; 隨著.NET的深入人心,作為一個程序員&#xff0c;當然不能在新技術面前停而止步&#xff0c;面對著c在.net中的失敗,雖然有一絲遺憾&#xff0c;但是我們應該認識到…

Linux相關概念和易錯知識點(44)(IP地址、子網和公網、NAPT、代理)

目錄1.IP地址&#xff08;1&#xff09;局域網和公網①局域網a.網關地址b.局域網通信②運營商子網③公網&#xff08;2&#xff09;NAPT①NAPT過程②理解NAPT③理解源IP和目的IPa.目的IPb.源IP③最長前綴匹配④NAT技術缺陷2.代理服務&#xff08;1&#xff09;正向代理&#xf…

工業智能終端賦能自動化生產線建設數字化管理

在當今數字化浪潮的推動下&#xff0c;自動化生產線正逐漸成為各行各業提升效率和降低成本的重要選擇。隨著智能制造的深入發展&#xff0c;工業智能終端的引入不僅為生產線帶來了技術革新&#xff0c;也賦予了數字化管理新的動力。一、工業智能終端&#xff1a;一體化設計&…

【Vue2手錄06】計算屬性Computed

一、表單元素的v-model綁定&#xff08;核心場景&#xff09; v-model 是Vue實現“表單元素與數據雙向同步”的語法糖&#xff0c;不同表單元素的綁定規則存在差異&#xff0c;需根據元素類型選擇正確的綁定方式。 1.1 四大表單元素的綁定規則對比表單元素類型綁定數據類型核心…

FPGA入門-數碼管靜態顯示

19. 數碼管的靜態顯示 在許多項目設計中&#xff0c;我們通常需要一些顯示設備來顯示我們需要的信息&#xff0c;可以選擇的顯示設備有很多&#xff0c;而數碼管是使用最多&#xff0c;最簡單的顯示設備之一。數碼管是一種半導體發光器件&#xff0c;具有響應時間短、體積小、…

深入理解大語言模型(5)-關于token

到目前為止對 LLM 的描述中&#xff0c;我們將其描述為一次預測一個單詞&#xff0c;但實際上還有一個更重要的技術細 節。即 LLM 實際上并不是重復預測下一個單詞&#xff0c;而是重復預測下一個 token 。對于一個句子&#xff0c;語言模型會 先使用分詞器將其拆分為一個個 to…

視覺智能的「破壁者」——Transformer如何重塑計算機視覺范式?三大CV算法論文介紹 ViTMAESwin Transformer

當自然語言處理領域因Transformer而煥發新生時&#xff0c;計算機視覺卻長期困于卷積神經網絡的架構桎梏。直到ViT&#xff08;Vision Transformer&#xff09;的橫空出世&#xff0c;才真正打破了視覺與語言之間的壁壘。它不僅是技術的革新&#xff0c;更是范式革命的開始&…

Java 并發容器源碼解析:ConcurrentSkipListSet 行級深度剖析

Java 并發容器源碼解析&#xff1a;ConcurrentSkipListSet 行級深度剖析 本文將深入解析 Java 并發容器 ConcurrentSkipListSet 的核心源碼&#xff0c;結合流程圖、代碼注釋、設計思想、優缺點分析、業務場景、調試與優化、集成方案、高階應用等&#xff0c;幫助你系統掌握這款…

答題卡自動識別案例

目錄 1.答題卡自動批閱整體實現思路 2.關鍵技術步驟與原理 答題卡區域提取 ①輪廓檢測并排序 ②執行透視變換 ③找到每一個圓圈輪廓 ④先對所有圓圈輪廓從上到下排序 ⑤再通過循環每次只提取出五個輪廓再進行從左到右的排序 3.完整代碼 1.答題卡自動批閱整體實現思路 …

C#實現通過POST實現讀取數據

C# POST請求與MySQL數據存儲實現下面是一個完整的C#解決方案&#xff0c;用于發送POST請求、接收響應數據&#xff0c;并將數據保存到MySQL數據庫中。完整代碼實現 using System; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Newtonsoft.J…

Java 字符編碼問題,怎么優雅地解決?

網羅開發&#xff08;小紅書、快手、視頻號同名&#xff09;大家好&#xff0c;我是 展菲&#xff0c;目前在上市企業從事人工智能項目研發管理工作&#xff0c;平時熱衷于分享各種編程領域的軟硬技能知識以及前沿技術&#xff0c;包括iOS、前端、Harmony OS、Java、Python等方…

STL之string類(C++)

1.string類核心定位std::string 本質是對 “字符序列” 的封裝&#xff0c;內部通過動態數組存儲字符&#xff0c;并自動管理內存&#xff08;分配、擴容、釋放&#xff09;&#xff0c;對外提供了簡潔的接口用于字符串的創建、修改、拼接、查找等操作。1.1 使用前提頭文件包含…

[Maven 基礎課程]第一個 Maven 項目

idea 新建一個項目&#xff1a; 來到 New Project 頁面&#xff1a; 這里我們有兩種方式創建 maven 項目&#xff0c;一種是自定義創建&#xff0c;另一種是使用 maven 模版項目創建。 自定義創建 maven 項目 基本配置 Name: first_maven_project 項目名稱&#xff0c;設為 …

uni小程序中使用Echarts圖表

前言 今天雞米花給大家帶來的是在uni里面使用echarts&#xff0c;能夠完美支持和PC端一樣的效果&#xff0c;我這邊的工程是uni轉為微信小程序&#xff0c;用的是vue3vite來寫的&#xff0c;然后實現了豎屏和橫屏的展示方式&#xff0c;好了獻上效果圖。 效果圖 一、引入插件 這…

從FOTA測試到汽車電子安全體系的啟蒙之旅

我是穿拖鞋的漢子,魔都中堅持長期主義的汽車電子工程師。 老規矩,分享一段喜歡的文字,避免自己成為高知識低文化的工程師: 做到欲望極簡,了解自己的真實欲望,不受外在潮流的影響,不盲從,不跟風。把自己的精力全部用在自己。一是去掉多余,凡事找規律,基礎是誠信;二是…