告別 Spring Security!Sa-Token + Gateway + Nacos 極簡(jiǎn)鑒權(quán)實(shí)戰(zhàn)
兄弟們,作為 Java 開(kāi)發(fā)者,誰(shuí)沒(méi)在 Spring Security 上栽過(guò)跟頭啊?明明就想做個(gè) “判斷用戶能不能訪問(wèn)接口” 的簡(jiǎn)單需求,結(jié)果一打開(kāi)文檔,又是 OAuth2、又是 JWT、又是 SecurityContextHolder,配置文件寫了一大堆,還動(dòng)不動(dòng)就報(bào)個(gè) “403 Forbidden” 找不著北。
我之前就踩過(guò)這坑:為了加個(gè)簡(jiǎn)單的 token 驗(yàn)證,硬著頭皮啃了三天 Spring Security 文檔,配置類堆了快 200 行,最后還因?yàn)?“權(quán)限注解沒(méi)掃描到” 卡了一下午。當(dāng)時(shí)就想:就沒(méi)有個(gè) “開(kāi)箱能用、配置簡(jiǎn)單、報(bào)錯(cuò)還能看懂” 的鑒權(quán)框架嗎?
還真有!今天咱就聊個(gè) “極簡(jiǎn)派” 方案 ——Sa-Token + Gateway + Nacos,不用復(fù)雜配置,不用繞彎子,5 步搞定全鏈路鑒權(quán),看完你絕對(duì)會(huì)說(shuō):“早知道這玩意兒,誰(shuí)還折騰 Spring Security 啊!”
一、先搞懂:為啥選這仨組合?
在擼代碼之前,咱先掰扯清楚:這三個(gè)工具各自是干啥的?湊一起為啥這么牛?
1. Sa-Token:鑒權(quán)界的 “小清新”
Sa-Token 這玩意兒,官網(wǎng)一句話總結(jié)得特到位:“一個(gè)輕量級(jí) Java 權(quán)限認(rèn)證框架,讓鑒權(quán)變得簡(jiǎn)單、優(yōu)雅”。咱用大白話翻譯下:
- 不用寫復(fù)雜配置:Spring Security 要配 “安全鏈、認(rèn)證管理器、權(quán)限過(guò)濾器”,Sa-Token 一行代碼搞定登錄 ——StpUtil.login(userId),沒(méi)了。
- 功能全還不啰嗦:token 過(guò)期、刷新、角色權(quán)限、單點(diǎn)登錄,這些常用功能它都有,而且 API 長(zhǎng)得特直觀,比如判斷角色就是StpUtil.hasRole("admin"),判斷權(quán)限就是StpUtil.hasPermission("user:add"),誰(shuí)看誰(shuí)懂。
- 報(bào)錯(cuò)信息賊友好:Spring Security 報(bào) “AccessDeniedException”,你還得猜是 “沒(méi)登錄” 還是 “沒(méi)權(quán)限”;Sa-Token 直接給你報(bào) “未登錄,請(qǐng)先登錄”“無(wú)此權(quán)限,請(qǐng)聯(lián)系管理員”,連排查方向都給你指好了。
簡(jiǎn)單說(shuō):Sa-Token 就是把 “鑒權(quán)” 這件事,從 “需要解密的復(fù)雜工程” 變成了 “擰瓶蓋級(jí)別的簡(jiǎn)單操作”。
2. Gateway:流量入口的 “守門神”
Gateway 咱都熟,Spring Cloud 全家桶里的網(wǎng)關(guān),負(fù)責(zé) “轉(zhuǎn)發(fā)請(qǐng)求、攔截請(qǐng)求、統(tǒng)一處理跨域”。為啥鑒權(quán)要帶它玩?
你想?。喝绻總€(gè)微服務(wù)都自己做鑒權(quán),那不是重復(fù)勞動(dòng)嗎?用戶訪問(wèn) “訂單服務(wù)” 要驗(yàn) token,訪問(wèn) “用戶服務(wù)” 還要驗(yàn) token,萬(wàn)一 token 規(guī)則改了,所有服務(wù)都得改一遍,這不瘋了?
Gateway 作為 “所有請(qǐng)求的入口”,剛好能把 “鑒權(quán)邏輯” 抽出來(lái)統(tǒng)一處理:所有請(qǐng)求先經(jīng)過(guò) Gateway,驗(yàn)完 token 沒(méi)問(wèn)題了再轉(zhuǎn)發(fā)到具體服務(wù),有問(wèn)題直接在網(wǎng)關(guān)層就打回去。這樣一來(lái),后面的微服務(wù)根本不用管鑒權(quán)的事兒,專心搞業(yè)務(wù)就行 —— 這才叫 “解耦” 嘛!
3. Nacos:配置界的 “變形金剛”
Nacos 咱也熟,配置中心 + 服務(wù)發(fā)現(xiàn)。它在這組合里干啥用?
鑒權(quán)場(chǎng)景里,有很多 “經(jīng)常變的配置”:比如 “哪些接口不用驗(yàn) token(像登錄、注冊(cè)接口)”“token 過(guò)期時(shí)間設(shè)多久”“黑名單 IP 列表”。如果這些配置寫死在代碼里,改一次就得重啟服務(wù),多麻煩?
Nacos 剛好能解決這問(wèn)題:把這些動(dòng)態(tài)配置放到 Nacos 上,服務(wù)啟動(dòng)時(shí)從 Nacos 拉取,配置改了還能實(shí)時(shí)刷新,不用重啟服務(wù)。比如你想臨時(shí)開(kāi)放某個(gè)測(cè)試接口,直接在 Nacos 上改 “排除攔截列表”,10 秒內(nèi)生效,多爽!
總結(jié)下:這仨組合的優(yōu)勢(shì)
- 簡(jiǎn)單:Sa-Token 讓鑒權(quán)代碼量減少 80%,新手也能上手。
- 統(tǒng)一:Gateway 集中處理鑒權(quán),微服務(wù)不用重復(fù)造輪子。
- 靈活:Nacos 動(dòng)態(tài)配置,改規(guī)則不用重啟服務(wù)。
- 穩(wěn)定:都是經(jīng)過(guò)大量實(shí)踐的成熟框架,踩坑概率低。
好了,廢話不多說(shuō),咱直接上實(shí)戰(zhàn) —— 從 0 到 1 搭一個(gè)完整的鑒權(quán)系統(tǒng),保證你跟著做就能跑通!
二、實(shí)戰(zhàn)準(zhǔn)備:環(huán)境搭好,少走彎路
先把 “彈藥” 備齊,避免等會(huì)兒擼代碼的時(shí)候 “缺這少那”。咱用的版本都是經(jīng)過(guò)驗(yàn)證的,兼容性沒(méi)問(wèn)題,別瞎換版本踩坑!
1. 基礎(chǔ)環(huán)境
- JDK:1.8(別問(wèn)為啥不用 11,大部分公司還在 8 呢,實(shí)用為主)
- Maven:3.6.3(版本太新可能和依賴不兼容)
- Spring Boot:2.6.13(穩(wěn)定版,別用 2.7+,Gateway 有些配置不一樣)
- Spring Cloud:2021.0.5(和 Boot 2.6.x 匹配)
- Spring Cloud Alibaba:2021.0.5.0(Nacos 用這個(gè)版本不報(bào)錯(cuò))
2. 核心依賴清單
后面搭項(xiàng)目會(huì)用到這些依賴,先列出來(lái)讓你有個(gè)底,不用記,后面直接復(fù)制粘貼就行:
依賴名稱 | 作用 |
sa-token-spring-boot-starter | Sa-Token 核心依賴,開(kāi)箱即用 |
sa-token-reactor-spring-boot-starter | Sa-Token 適配 Gateway 的依賴(關(guān)鍵) |
spring-cloud-starter-gateway | Gateway 核心依賴 |
com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config | Nacos 配置中心依賴 |
com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery | Nacos 服務(wù)發(fā)現(xiàn)依賴(可選,這次實(shí)戰(zhàn)用不上,但建議加) |
lombok | 省代碼神器,不用寫 getter/setter |
spring-boot-starter-web | 但注意:Gateway 是基于 WebFlux 的,別加 spring-boot-starter-web,會(huì)沖突! |
3. 項(xiàng)目結(jié)構(gòu)
咱這次搭個(gè) “多模塊項(xiàng)目”,結(jié)構(gòu)清晰,也符合實(shí)際開(kāi)發(fā)場(chǎng)景:
sa-token-auth-demo
├── sa-token-auth-parent(父工程,管理依賴版本)
├── sa-token-auth-gateway(網(wǎng)關(guān)模塊,核心鑒權(quán)邏輯在這)
└── sa-token-auth-service(業(yè)務(wù)服務(wù)模塊,比如用戶服務(wù),演示鑒權(quán)效果)為啥這么分?因?yàn)閷?shí)際項(xiàng)目里,網(wǎng)關(guān)和業(yè)務(wù)服務(wù)肯定是分開(kāi)部署的,咱這么搭更貼近真實(shí)場(chǎng)景。
三、第一步:搭父工程,統(tǒng)一管理依賴
先搞父工程,把所有依賴的版本定好,后面子模塊直接繼承就行,不用每個(gè)模塊都寫版本號(hào),避免版本混亂。
1. 創(chuàng)建父工程(sa-token-auth-parent)
新建一個(gè) Maven 項(xiàng)目,打包方式選pom(父工程都是 pom 打包),然后修改pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 父工程坐標(biāo),自己改groupId和artifactId -->
<groupId>com.example</groupId>
<artifactId>sa-token-auth-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>sa-token-auth-parent</name>
<description>Sa-Token+Gateway+Nacos鑒權(quán)實(shí)戰(zhàn)父工程</description>
<!-- 統(tǒng)一管理依賴版本 -->
<properties>
<java.version>1.8</java.version>
<spring-boot.version>2.6.13</spring-boot.version>
<spring-cloud.version>2021.0.5</spring-cloud.version>
<spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>
<sa-token.version>1.34.0</sa-token.version>
<lombok.version>1.18.24</lombok.version>
</properties>
<!-- dependencyManagement:只管理版本,不實(shí)際引入依賴 -->
<dependencyManagement>
<dependencies>
<!-- Spring Boot 父依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Cloud 依賴 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Cloud Alibaba 依賴 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Sa-Token 依賴 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- Lombok 依賴 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 子模塊聲明:后面加子模塊的時(shí)候要在這寫 -->
<modules>
<module>sa-token-auth-gateway</module>
<module>sa-token-auth-service</module>
</modules>
</project>這里要注意:dependencyManagement標(biāo)簽只是 “管理版本”,子模塊要實(shí)際引入依賴還得寫dependency標(biāo)簽,只是不用寫版本號(hào)了 —— 這是 Maven 父工程的常規(guī)操作,老司機(jī)都懂,新手記著就行。
四、第二步:搭網(wǎng)關(guān)模塊,實(shí)現(xiàn)統(tǒng)一鑒權(quán)
網(wǎng)關(guān)模塊(sa-token-auth-gateway)是這次實(shí)戰(zhàn)的核心,所有鑒權(quán)邏輯都在這處理。咱分三步走:先搭基礎(chǔ)框架,再配 Sa-Token 鑒權(quán),最后整合 Nacos 動(dòng)態(tài)配置。
1. 創(chuàng)建網(wǎng)關(guān)模塊(sa-token-auth-gateway)
在父工程下新建一個(gè) Maven 子模塊,artifactId 設(shè)為sa-token-auth-gateway,然后修改它的pom.xml,引入依賴:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>sa-token-auth-parent</artifactId>
<groupId>com.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sa-token-auth-gateway</artifactId>
<name>sa-token-auth-gateway</name>
<description>網(wǎng)關(guān)模塊:統(tǒng)一鑒權(quán)入口</description>
<dependencies>
<!-- Gateway 核心依賴 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Sa-Token 核心依賴 + Gateway適配依賴 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot-starter</artifactId>
</dependency>
<!-- Nacos 配置中心依賴 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- Spring Boot 測(cè)試依賴(可選) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- 打包插件,不然SpringBoot項(xiàng)目跑不起來(lái) -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>這里有個(gè)坑要注意:Gateway 是基于 WebFlux 的,千萬(wàn)不能引入spring-boot-starter-web依賴,不然會(huì)沖突!如果不小心加了,趕緊刪掉,不然啟動(dòng)會(huì)報(bào) “Circular view path” 之類的錯(cuò)。
2. 寫網(wǎng)關(guān)啟動(dòng)類
新建一個(gè)啟動(dòng)類GatewayApplication.java,很簡(jiǎn)單,就加個(gè)@SpringBootApplication注解:
package com.example.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 網(wǎng)關(guān)啟動(dòng)類
*/
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
System.out.println("網(wǎng)關(guān)啟動(dòng)成功!??");
}
}3. 配置 Nacos 連接(關(guān)鍵?。?/h3>
因?yàn)橐獜?Nacos 拉取配置,所以得先配置 Nacos 的地址。在src/main/resources下新建bootstrap.yml文件(注意是 bootstrap.yml,不是 application.yml,因?yàn)?bootstrap 加載優(yōu)先級(jí)更高,要先連 Nacos):
# bootstrap.yml:先加載這個(gè)文件,連接Nacos
spring:
application:
name: sa-token-auth-gateway # 服務(wù)名,后面Nacos配置要用到
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 # Nacos地址,本地搭的話就是這個(gè)
file-extension: yaml # 配置文件格式,yaml或properties
group: DEFAULT_GROUP # 配置分組,默認(rèn)DEFAULT_GROUP
namespace: # Nacos命名空間,默認(rèn)是空,不用改(如果自己建了命名空間就填I(lǐng)D)
discovery:
server-addr: ${spring.cloud.nacos.config.server-addr} # 服務(wù)發(fā)現(xiàn)地址,和配置中心一樣
# 日志配置:讓Sa-Token的日志打印出來(lái),方便排查問(wèn)題
logging:
level:
cn.dev33: debug # Sa-Token的包日志級(jí)別設(shè)為debug這里要先確保你本地的 Nacos 已經(jīng)啟動(dòng)了!如果還沒(méi)裝 Nacos,趕緊去官網(wǎng)下載個(gè) 1.4.3 版本(穩(wěn)定),解壓后雙擊bin/startup.cmd(Windows)或bin/startup.sh(Linux)就能啟動(dòng),默認(rèn)端口 8848,訪問(wèn)http://localhost:8848/nacos,賬號(hào)密碼都是 nacos。
4. 在 Nacos 上創(chuàng)建網(wǎng)關(guān)配置
啟動(dòng) Nacos 后,登錄控制臺(tái),點(diǎn)擊左側(cè) “配置管理”→“配置列表”→“+” 號(hào),新建配置:
- Data ID:sa-token-auth-gateway.yaml(格式:服務(wù)名。文件格式,和 bootstrap.yml 里的配置對(duì)應(yīng))
- Group:DEFAULT_GROUP(和 bootstrap.yml 里的 group 對(duì)應(yīng))
- 配置格式:YAML
- 配置內(nèi)容:下面這段,包含 Gateway 路由配置和 Sa-Token 基礎(chǔ)配置
# Nacos上的sa-token-auth-gateway.yaml配置
server:
port: 8080 # 網(wǎng)關(guān)端口,后面訪問(wèn)都走這個(gè)端口
spring:
cloud:
gateway:
# 路由配置:把請(qǐng)求轉(zhuǎn)發(fā)到對(duì)應(yīng)的業(yè)務(wù)服務(wù)
routes:
# 路由1:轉(zhuǎn)發(fā)到用戶服務(wù)(sa-token-auth-service)
- id: user-service-route # 路由ID,唯一就行
uri: http://localhost:8081 # 業(yè)務(wù)服務(wù)地址(實(shí)際項(xiàng)目用服務(wù)名,這里先寫固定地址)
predicates: # 路由匹配規(guī)則:請(qǐng)求路徑以/api/user開(kāi)頭的,都走這個(gè)路由
- Path=/api/user/**
filters: # 過(guò)濾器:給請(qǐng)求加個(gè)前綴(可選,看業(yè)務(wù)需求)
- StripPrefix=1 # 去掉路徑的第一個(gè)前綴,比如/api/user/info變成/user/info
# Sa-Token 核心配置
sa-token:
# token名稱(Header里的key)
token-name: Authorization
# token有效期(單位:秒),默認(rèn)30天,這里設(shè)1小時(shí)方便測(cè)試
timeout: 3600
# token過(guò)期后是否允許刷新,默認(rèn)true
is-refresh-token: true
# 刷新token的有效時(shí)間(單位:秒),默認(rèn)7天,這里設(shè)2小時(shí)
refresh-token-timeout: 7200
# 排除攔截的路徑(不用登錄就能訪問(wèn)的接口)
exclude-path-patterns:
- /api/user/login # 登錄接口
- /api/user/register # 注冊(cè)接口
- /doc.html # Swagger文檔(如果加了的話)
- /webjars/** # Swagger靜態(tài)資源
- /v3/api-docs/** # Swagger接口文檔
# 是否在控制臺(tái)打印日志,默認(rèn)false
is-log: true配置完點(diǎn)擊 “發(fā)布”,這樣網(wǎng)關(guān)啟動(dòng)時(shí)就會(huì)從 Nacos 拉取這些配置了。
5. 寫 Sa-Token 網(wǎng)關(guān)鑒權(quán)過(guò)濾器
這步是核心!要在 Gateway 里加一個(gè) Sa-Token 的過(guò)濾器,實(shí)現(xiàn) “所有請(qǐng)求先驗(yàn) token,沒(méi) token 或 token 無(wú)效就攔截” 的邏輯。
新建一個(gè)配置類SaTokenGatewayConfig.java:
package com.example.gateway.config;
import cn.dev33.satoken.reactor.filter.SaTokenGatewayFilter;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
/**
* Sa-Token網(wǎng)關(guān)鑒權(quán)配置
*/
@Configuration
public class SaTokenGatewayConfig {
/**
* 注冊(cè)Sa-Token網(wǎng)關(guān)過(guò)濾器
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE) // 優(yōu)先級(jí)設(shè)最高,確保先執(zhí)行鑒權(quán)
public WebFilter saTokenGatewayFilter() {
return new SaTokenGatewayFilter()
// 配置攔截規(guī)則:除了exclude-path-patterns里的路徑,其他都要鑒權(quán)
.addAuth(obj -> {
// 1. 獲取當(dāng)前請(qǐng)求路徑
ServerWebExchange exchange = (ServerWebExchange) obj;
String path = exchange.getRequest().getURI().getPath();
System.out.println("當(dāng)前請(qǐng)求路徑:" + path);
// 2. 路由匹配:排除不需要鑒權(quán)的路徑
SaRouter.match("/**", stp -> {
// 3. 執(zhí)行鑒權(quán):檢查是否登錄(如果需要角色/權(quán)限,這里可以加StpUtil.hasRole("admin")等)
StpUtil.checkLogin();
// (可選)如果需要更細(xì)粒度的權(quán)限控制,比如某個(gè)路徑需要特定角色
// SaRouter.match("/api/admin/**", () -> StpUtil.hasRole("admin"));
})
// 排除不需要鑒權(quán)的路徑(和Nacos里的exclude-path-patterns對(duì)應(yīng),雙重保險(xiǎn))
.notMatch("/api/user/login", "/api/user/register", "/doc.html", "/webjars/**", "/v3/api-docs/**")
.doAuth();
})
// 配置未登錄的處理邏輯
.setUnauthorizedHandler(obj -> {
ServerWebExchange exchange = (ServerWebExchange) obj;
// 設(shè)置響應(yīng)狀態(tài)碼401(未授權(quán))
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// 返回JSON提示:未登錄
return SaResult.error("未登錄,請(qǐng)先登錄!").toMono(exchange);
})
// 配置無(wú)權(quán)限的處理邏輯
.setAccessDeniedHandler(obj -> {
ServerWebExchange exchange = (ServerWebExchange) obj;
// 設(shè)置響應(yīng)狀態(tài)碼403(禁止訪問(wèn))
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
// 返回JSON提示:無(wú)權(quán)限
return SaResult.error("無(wú)此權(quán)限,請(qǐng)聯(lián)系管理員!").toMono(exchange);
});
}
/**
* 配置跨域(前后端分離必加,不然前端調(diào)接口會(huì)報(bào)跨域錯(cuò))
*/
@Bean
public WebFilter corsFilter() {
return (ServerWebExchange exchange, WebFilterChain chain) -> {
// 允許所有來(lái)源(實(shí)際項(xiàng)目要寫具體的前端地址,比如http://localhost:8080)
exchange.getResponse().getHeaders().add("Access-Control-Allow-Origin", "*");
// 允許的請(qǐng)求頭
exchange.getResponse().getHeaders().add("Access-Control-Allow-Headers", "*");
// 允許的請(qǐng)求方法
exchange.getResponse().getHeaders().add("Access-Control-Allow-Methods", "*");
// 允許攜帶Cookie(如果需要的話)
exchange.getResponse().getHeaders().add("Access-Control-Allow-Credentials", "true");
// 預(yù)檢請(qǐng)求的緩存時(shí)間(秒),避免頻繁發(fā)預(yù)檢請(qǐng)求
exchange.getResponse().getHeaders().add("Access-Control-Max-Age", "3600");
// 如果是預(yù)檢請(qǐng)求(OPTIONS),直接返回成功
if ("OPTIONS".equals(exchange.getRequest().getMethodValue())) {
exchange.getResponse().setStatusCode(HttpStatus.OK);
return Mono.empty();
}
// 不是預(yù)檢請(qǐng)求,繼續(xù)走過(guò)濾鏈
return chain.filter(exchange);
};
}
}這段代碼要重點(diǎn)說(shuō)下:
- SaTokenGatewayFilter:Sa-Token 專門為 Gateway 提供的過(guò)濾器,不用自己寫復(fù)雜的攔截邏輯。
- addAuth:配置鑒權(quán)規(guī)則,StpUtil.checkLogin()就是 “檢查是否登錄”,一行代碼搞定核心鑒權(quán)。
- setUnauthorizedHandler:沒(méi)登錄時(shí)的處理,返回 401 和 “未登錄” 提示,前端能直接拿到。
- setAccessDeniedHandler:沒(méi)權(quán)限時(shí)的處理,返回 403 和 “無(wú)權(quán)限” 提示。
- corsFilter:跨域配置,前后端分離項(xiàng)目必加,不然前端調(diào)接口會(huì)報(bào) “Access to XMLHttpRequest at ... from origin ... has been blocked by CORS policy” 錯(cuò)。
6. 測(cè)試網(wǎng)關(guān)鑒權(quán)(先跑通基礎(chǔ)流程)
現(xiàn)在網(wǎng)關(guān)模塊基本搭好了,咱先啟動(dòng)網(wǎng)關(guān),測(cè)試下鑒權(quán)邏輯:
- 啟動(dòng) Nacos(確保配置已發(fā)布)。
- 啟動(dòng)網(wǎng)關(guān)模塊(GatewayApplication),控制臺(tái)看到 “網(wǎng)關(guān)啟動(dòng)成功!??” 就說(shuō)明沒(méi)問(wèn)題。
- 用 Postman 或?yàn)g覽器訪問(wèn) “不需要鑒權(quán)的接口”,比如http://localhost:8080/api/user/login(雖然業(yè)務(wù)服務(wù)還沒(méi)寫,但網(wǎng)關(guān)會(huì)轉(zhuǎn)發(fā)請(qǐng)求,此時(shí)會(huì)報(bào) “503 Service Unavailable”,因?yàn)闃I(yè)務(wù)服務(wù)沒(méi)啟動(dòng),這是正常的)。
- 訪問(wèn) “需要鑒權(quán)的接口”,比如http://localhost:8080/api/user/info,此時(shí)網(wǎng)關(guān)會(huì)攔截,返回:
{
"code": 401,
"msg": "未登錄,請(qǐng)先登錄!",
"data": null
}這就對(duì)了!說(shuō)明鑒權(quán)過(guò)濾器生效了 —— 沒(méi)登錄的請(qǐng)求被攔截了。
五、第三步:搭業(yè)務(wù)服務(wù),演示鑒權(quán)效果
網(wǎng)關(guān)搭好了,現(xiàn)在要搭個(gè)業(yè)務(wù)服務(wù)(sa-token-auth-service),寫個(gè)登錄接口和需要鑒權(quán)的接口,演示 “登錄獲取 token→攜帶 token 訪問(wèn)接口” 的完整流程。
1. 創(chuàng)建業(yè)務(wù)服務(wù)模塊(sa-token-auth-service)
在父工程下新建 Maven 子模塊,artifactId 設(shè)為sa-token-auth-service,修改pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>sa-token-auth-parent</artifactId>
<groupId>com.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sa-token-auth-service</artifactId>
<name>sa-token-auth-service</name>
<description>業(yè)務(wù)服務(wù)模塊:用戶服務(wù)示例</description>
<dependencies>
<!-- Spring Boot Web依賴(業(yè)務(wù)服務(wù)用Web,網(wǎng)關(guān)用WebFlux,不沖突) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Sa-Token 核心依賴(業(yè)務(wù)服務(wù)也要加,用來(lái)操作登錄、判斷權(quán)限) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
</dependency>
<!-- Nacos 配置中心依賴(可選,業(yè)務(wù)服務(wù)如果要?jiǎng)討B(tài)配置也可以加) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- Spring Boot 測(cè)試依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>這里注意:業(yè)務(wù)服務(wù)用的是spring-boot-starter-web(基于 Servlet),網(wǎng)關(guān)用的是spring-cloud-starter-gateway(基于 WebFlux),兩者不沖突,因?yàn)槭遣煌哪K。
2. 寫業(yè)務(wù)服務(wù)啟動(dòng)類
新建UserServiceApplication.java:
package com.example.service;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 業(yè)務(wù)服務(wù)啟動(dòng)類(用戶服務(wù))
*/
@SpringBootApplication
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
System.out.println("用戶服務(wù)啟動(dòng)成功!??");
}
}3. 配置業(yè)務(wù)服務(wù)(application.yml)
新建src/main/resources/application.yml:
server:
port: 8081 # 業(yè)務(wù)服務(wù)端口,和網(wǎng)關(guān)路由里的uri對(duì)應(yīng)
spring:
application:
name: sa-token-auth-service # 服務(wù)名
# Sa-Token 配置(和網(wǎng)關(guān)保持一致,比如token名稱)
sa-token:
token-name: Authorization # 和網(wǎng)關(guān)的token-name一致,不然解析不到token
is-log: true # 打印日志,方便排查4. 寫核心業(yè)務(wù)代碼(登錄 + 用戶信息接口)
咱寫個(gè)簡(jiǎn)單的用戶服務(wù),包含三個(gè)接口:
- 登錄接口:/user/login(不用鑒權(quán),返回 token)
- 用戶信息接口:/user/info(需要鑒權(quán),返回當(dāng)前登錄用戶信息)
- 注冊(cè)接口:/user/register(不用鑒權(quán),模擬注冊(cè))
(1)定義用戶實(shí)體類
新建entity/User.java:
package com.example.service.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用戶實(shí)體類
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
publicclass User {
private Long id; // 用戶ID
privateString username; // 用戶名
privateString password; // 密碼(實(shí)際項(xiàng)目要加密,這里演示用明文)
privateString role; // 角色(比如admin、user)
}(2)寫用戶服務(wù)(模擬數(shù)據(jù)庫(kù)操作)
新建service/UserService.java:
package com.example.service.service;
import com.example.service.entity.User;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 用戶服務(wù)(模擬數(shù)據(jù)庫(kù)操作,實(shí)際項(xiàng)目要連MySQL)
*/
@Service
publicclass UserService {
// 模擬數(shù)據(jù)庫(kù):存儲(chǔ)用戶信息
privatestatic final Map<String, User> USER_MAP = new HashMap<>();
// 初始化數(shù)據(jù):加個(gè)測(cè)試用戶(username: test, password: 123456)
static {
USER_MAP.put("test", new User(1L, "test", "123456", "user"));
USER_MAP.put("admin", new User(2L, "admin", "admin123", "admin"));
}
/**
* 登錄:根據(jù)用戶名和密碼查詢用戶
*/
public User login(String username, String password) {
// 1. 從模擬數(shù)據(jù)庫(kù)獲取用戶
User user = USER_MAP.get(username);
// 2. 判斷用戶是否存在,密碼是否正確
if (user == null || !user.getPassword().equals(password)) {
returnnull; // 登錄失敗
}
return user; // 登錄成功
}
/**
* 注冊(cè):新增用戶到模擬數(shù)據(jù)庫(kù)
*/
publicboolean register(String username, String password) {
// 1. 判斷用戶名是否已存在
if (USER_MAP.containsKey(username)) {
returnfalse; // 用戶名已存在,注冊(cè)失敗
}
// 2. 新增用戶(ID用UUID簡(jiǎn)化,實(shí)際項(xiàng)目用自增ID)
User newUser = new User(
Long.parseLong(UUID.randomUUID().toString().substring(0, 8), 16),
username,
password,
"user"http:// 新用戶默認(rèn)角色是user
);
USER_MAP.put(username, newUser);
returntrue; // 注冊(cè)成功
}
/**
* 根據(jù)用戶名獲取用戶信息(用于登錄后查詢)
*/
public User getUserByUsername(String username) {
return USER_MAP.get(username);
}
}(3)寫控制器(接口)
新建controller/UserController.java:
package com.example.service.controller;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import com.example.service.entity.User;
import com.example.service.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 用戶控制器:提供登錄、注冊(cè)、用戶信息接口
*/
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor// Lombok注解:自動(dòng)注入依賴,不用寫@Autowired
publicclass UserController {
// 注入用戶服務(wù)
private final UserService userService;
/**
* 登錄接口
* 請(qǐng)求地址:http://localhost:8080/api/user/login(通過(guò)網(wǎng)關(guān)訪問(wèn))
* 請(qǐng)求參數(shù):username(用戶名),password(密碼)
*/
@PostMapping("/login")
public SaResult login(String username, String password) {
// 1. 調(diào)用服務(wù)層驗(yàn)證用戶名密碼
User user = userService.login(username, password);
if (user == null) {
return SaResult.error("用戶名或密碼錯(cuò)誤!");
}
// 2. 登錄成功:調(diào)用Sa-Token的login方法,傳入用戶ID(這里用用戶名當(dāng)ID,實(shí)際項(xiàng)目用用戶表的ID)
StpUtil.login(user.getUsername());
// 3. 獲取token(Sa-Token自動(dòng)生成)
String token = StpUtil.getTokenValue();
// 4. 返回結(jié)果:token + 用戶信息(脫敏,不要返回密碼)
Map<String, Object> data = new HashMap<>();
data.put("token", token);
data.put("user", new HashMap<String, Object>() {{
put("id", user.getId());
put("username", user.getUsername());
put("role", user.getRole());
}});
return SaResult.ok("登錄成功!").setData(data);
}
/**
* 注冊(cè)接口
* 請(qǐng)求地址:http://localhost:8080/api/user/register(通過(guò)網(wǎng)關(guān)訪問(wèn))
* 請(qǐng)求參數(shù):username(用戶名),password(密碼)
*/
@PostMapping("/register")
public SaResult register(String username, String password) {
// 1. 調(diào)用服務(wù)層注冊(cè)用戶
boolean success = userService.register(username, password);
if (!success) {
return SaResult.error("用戶名已存在!");
}
return SaResult.ok("注冊(cè)成功!");
}
/**
* 獲取當(dāng)前登錄用戶信息(需要鑒權(quán))
* 請(qǐng)求地址:http://localhost:8080/api/user/info(通過(guò)網(wǎng)關(guān)訪問(wèn))
* 請(qǐng)求頭:Authorization: token(登錄時(shí)返回的token)
*/
@GetMapping("/info")
public SaResult getUserInfo() {
// 1. 獲取當(dāng)前登錄用戶的ID(這里是用戶名,因?yàn)榈卿洉r(shí)傳的是用戶名)
String username = (String) StpUtil.getLoginId();
// 2. 根據(jù)用戶名查詢用戶信息
User user = userService.getUserByUsername(username);
if (user == null) {
return SaResult.error("用戶不存在!");
}
// 3. 返回用戶信息(脫敏)
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("id", user.getId());
userInfo.put("username", user.getUsername());
userInfo.put("role", user.getRole());
userInfo.put("tokenTimeout", StpUtil.getTokenTimeout()); // 返回token剩余有效期(秒)
return SaResult.ok("獲取用戶信息成功!").setData(userInfo);
}
/**
* 退出登錄接口(需要鑒權(quán))
* 請(qǐng)求地址:http://localhost:8080/api/user/logout(通過(guò)網(wǎng)關(guān)訪問(wèn))
* 請(qǐng)求頭:Authorization: token
*/
@PostMapping("/logout")
public SaResult logout() {
// 調(diào)用Sa-Token的退出方法,清除token
StpUtil.logout();
return SaResult.ok("退出登錄成功!");
}
}這段代碼里有個(gè)關(guān)鍵:StpUtil.login(user.getUsername())—— 這就是 Sa-Token 的登錄核心方法,傳入用戶唯一標(biāo)識(shí)(這里用用戶名,實(shí)際項(xiàng)目用用戶 ID),Sa-Token 會(huì)自動(dòng)生成 token,不用你自己處理 token 的生成、存儲(chǔ)邏輯,太省心了!
六、第四步:完整流程測(cè)試,驗(yàn)證鑒權(quán)效果
現(xiàn)在網(wǎng)關(guān)和業(yè)務(wù)服務(wù)都搭好了,咱來(lái)測(cè)一遍完整流程,確保每個(gè)環(huán)節(jié)都沒(méi)問(wèn)題。測(cè)試工具用 Postman(或 Apifox,都一樣)。
1. 啟動(dòng)所有服務(wù)
- 啟動(dòng) Nacos(必須先啟動(dòng),不然網(wǎng)關(guān)和業(yè)務(wù)服務(wù)拉不到配置)。
- 啟動(dòng)網(wǎng)關(guān)模塊(GatewayApplication,端口 8080)。
- 啟動(dòng)業(yè)務(wù)服務(wù)模塊(UserServiceApplication,端口 8081)。
確保三個(gè)服務(wù)都啟動(dòng)成功,控制臺(tái)沒(méi)有報(bào)錯(cuò)。
2. 測(cè)試 1:注冊(cè)用戶
- 請(qǐng)求地址:POST http://localhost:8080/api/user/register
- 請(qǐng)求參數(shù):username=zhangsan&password=654321(用表單形式傳參)
- 預(yù)期結(jié)果:返回 “注冊(cè)成功!”
實(shí)際返回:
{
"code": 200,
"msg": "注冊(cè)成功!",
"data": null
}注冊(cè)成功!說(shuō)明 “不需要鑒權(quán)的接口” 能正常訪問(wèn)。
3. 測(cè)試 2:登錄獲取 token
- 請(qǐng)求地址:POST http://localhost:8080/api/user/login
- 請(qǐng)求參數(shù):username=zhangsan&password=654321(用剛注冊(cè)的用戶,或測(cè)試用戶 test/123456)
- 預(yù)期結(jié)果:返回 token 和用戶信息
實(shí)際返回(重點(diǎn)看data.token,后面要用到):
{
"code": 200,
"msg": "登錄成功!",
"data": {
"token": "satoken:623a232f-7f5a-4b5c-8d1e-9a0b1c2d3e4f", // 這是token,每個(gè)人的不一樣
"user": {
"id": 123456789,
"username": "zhangsan",
"role": "user"
}
}
}登錄成功!拿到 token 了,下一步用這個(gè) token 訪問(wèn)需要鑒權(quán)的接口。
4. 測(cè)試 3:攜帶 token 訪問(wèn)用戶信息接口
- 請(qǐng)求地址:GET http://localhost:8080/api/user/info
- 請(qǐng)求頭:Authorization: satoken:623a232f-7f5a-4b5c-8d1e-9a0b1c2d3e4f(把登錄返回的 token 填進(jìn)去)
- 預(yù)期結(jié)果:返回當(dāng)前登錄用戶的信息
實(shí)際返回:
{
"code": 200,
"msg": "獲取用戶信息成功!",
"data": {
"id": 123456789,
"username": "zhangsan",
"role": "user",
"tokenTimeout": 3580 // token剩余有效期(秒),因?yàn)樵O(shè)置的是3600秒,過(guò)了20秒
}
}完美!說(shuō)明鑒權(quán)通過(guò)了,網(wǎng)關(guān)正確識(shí)別了 token,業(yè)務(wù)服務(wù)正確獲取了當(dāng)前登錄用戶。
5. 測(cè)試 4:不攜帶 token 訪問(wèn)需要鑒權(quán)的接口
- 請(qǐng)求地址:GET http://localhost:8080/api/user/info
- 不填A(yù)uthorization請(qǐng)求頭
- 預(yù)期結(jié)果:網(wǎng)關(guān)攔截,返回 “未登錄,請(qǐng)先登錄!”
實(shí)際返回:
{
"code": 401,
"msg": "未登錄,請(qǐng)先登錄!",
"data": null
}正確!鑒權(quán)攔截生效了。
6. 測(cè)試 5:攜帶無(wú)效 token 訪問(wèn)
- 請(qǐng)求地址:GET http://localhost:8080/api/user/info
- 請(qǐng)求頭:Authorization: invalid-token(隨便寫個(gè)無(wú)效的 token)
- 預(yù)期結(jié)果:返回 “未登錄,請(qǐng)先登錄!”(Sa-Token 會(huì)識(shí)別無(wú)效 token 為未登錄)
實(shí)際返回和測(cè)試 4 一樣,正確。
7. 測(cè)試 6:退出登錄后訪問(wèn)接口
- 先調(diào)用退出登錄接口:POST http://localhost:8080/api/user/logout,請(qǐng)求頭帶之前的 token,返回 “退出登錄成功!”。
- 再用同一個(gè) token 訪問(wèn)/api/user/info,預(yù)期結(jié)果:返回 “未登錄,請(qǐng)先登錄!”。
實(shí)際返回正確,說(shuō)明退出登錄后 token 失效了,鑒權(quán)邏輯沒(méi)問(wèn)題。
七、第五步:Nacos 動(dòng)態(tài)配置實(shí)戰(zhàn)(進(jìn)階)
前面咱把 Nacos 搭好了,現(xiàn)在來(lái)演示 “動(dòng)態(tài)修改鑒權(quán)規(guī)則,不用重啟服務(wù)”—— 這才是 Nacos 的核心價(jià)值之一。
1. 需求:臨時(shí)開(kāi)放一個(gè)測(cè)試接口,不用鑒權(quán)
比如業(yè)務(wù)服務(wù)加了個(gè)/user/test接口,想臨時(shí)開(kāi)放,不用登錄就能訪問(wèn),怎么用 Nacos 動(dòng)態(tài)配置實(shí)現(xiàn)?
(1)業(yè)務(wù)服務(wù)加測(cè)試接口
在UserController里加一個(gè)接口:
/**
* 測(cè)試接口(臨時(shí)開(kāi)放,不用鑒權(quán))
* 請(qǐng)求地址:http://localhost:8080/api/user/test
*/
@GetMapping("/test")
public SaResult test() {
return SaResult.ok("這是臨時(shí)開(kāi)放的測(cè)試接口,不用登錄就能訪問(wèn)!");
}重啟業(yè)務(wù)服務(wù)(這次是因?yàn)榧恿私涌?,?shí)際改配置不用重啟)。
(2)不修改配置時(shí)訪問(wèn)測(cè)試接口
用 Postman 訪問(wèn)GET http://localhost:8080/api/user/test,不攜帶 token,預(yù)期結(jié)果:網(wǎng)關(guān)攔截,返回 “未登錄”。
實(shí)際返回確實(shí)是 “未登錄”,因?yàn)?api/user/test不在 Nacos 的exclude-path-patterns里。
(3)在 Nacos 上動(dòng)態(tài)修改配置
登錄 Nacos 控制臺(tái),找到sa-token-auth-gateway.yaml配置,修改sa-token.exclude-path-patterns,加上/api/user/test:
sa-token:
# 其他配置不變,只加一行
exclude-path-patterns:
- /api/user/login
- /api/user/register
- /doc.html
- /webjars/**
- /v3/api-docs/**
- /api/user/test # 新增:測(cè)試接口不用鑒權(quán)點(diǎn)擊 “發(fā)布”,不用重啟網(wǎng)關(guān)!
(4)再次訪問(wèn)測(cè)試接口
還是訪問(wèn)GET http://localhost:8080/api/user/test,不攜帶 token,預(yù)期結(jié)果:返回測(cè)試接口的信息。
實(shí)際返回:
{
"code": 200,
"msg": "這是臨時(shí)開(kāi)放的測(cè)試接口,不用登錄就能訪問(wèn)!",
"data": null
}成了!配置改了 10 秒內(nèi)就生效了,不用重啟網(wǎng)關(guān),這就是動(dòng)態(tài)配置的魅力!
2. 再試一個(gè):動(dòng)態(tài)修改 token 有效期
比如想把 token 有效期從 1 小時(shí)(3600 秒)改成 2 小時(shí)(7200 秒),直接在 Nacos 上改sa-token.timeout:
sa-token:
timeout: 7200 # 從3600改成7200
# 其他配置不變發(fā)布后,新登錄的用戶 token 有效期就是 2 小時(shí)了,老用戶的 token 還是按之前的 1 小時(shí)算 —— 這很合理,動(dòng)態(tài)配置只對(duì)新生成的 token 生效。
八、實(shí)戰(zhàn)踩坑指南(必看?。?/h2>
咱實(shí)戰(zhàn)過(guò)程中肯定會(huì)遇到坑,我把我踩過(guò)的坑整理出來(lái),幫你少走彎路:
1. 網(wǎng)關(guān)啟動(dòng)報(bào) “Circular view path” 錯(cuò)
- 原因:網(wǎng)關(guān)模塊引入了spring-boot-starter-web依賴,和 Gateway 的 WebFlux 沖突了。
- 解決:刪掉網(wǎng)關(guān)模塊的spring-boot-starter-web依賴,只留spring-cloud-starter-gateway。
2. 網(wǎng)關(guān)拉不到 Nacos 配置,報(bào) “Could not resolve placeholder” 錯(cuò)
- 原因 1:bootstrap.yml沒(méi)寫對(duì),比如 Nacos 地址錯(cuò)了,或者 Data ID 和服務(wù)名不匹配。
- 原因 2:Nacos 里的配置沒(méi)發(fā)布,或者 Group、Namespace 和bootstrap.yml里的不一致。
- 解決:檢查bootstrap.yml的spring.application.name和 Nacos 的 Data ID 是否一致(Data ID 是 “服務(wù)名。文件格式”),檢查 Nacos 地址是否正確,配置是否發(fā)布。
3. 攜帶 token 訪問(wèn)接口,還是返回 “未登錄”
- 原因 1:請(qǐng)求頭的 key 和 Sa-Token 配置的token-name不一致,比如配置的是Authorization,請(qǐng)求頭寫的是token。
- 原因 2:token 傳錯(cuò)了,或者 token 已經(jīng)過(guò)期 / 被退出登錄了。
- 原因 3:網(wǎng)關(guān)的exclude-path-patterns配置錯(cuò)了,把需要鑒權(quán)的路徑加進(jìn)去了。
- 解決:檢查請(qǐng)求頭 key 是否和sa-token.token-name一致,重新登錄獲取新 token,檢查 Nacos 的exclude-path-patterns配置。
4. 跨域問(wèn)題,前端調(diào)接口報(bào) “CORS policy” 錯(cuò)
- 原因:網(wǎng)關(guān)沒(méi)配置跨域過(guò)濾器,或者跨域配置不正確。
- 解決:參考前面的corsFilter配置,確保Access-Control-Allow-Origin、Access-Control-Allow-Headers、Access-Control-Allow-Methods都配置對(duì)了,預(yù)檢請(qǐng)求(OPTIONS)要返回 200。
5. Nacos 配置修改后不生效
- 原因 1:沒(méi)加spring-cloud-starter-alibaba-nacos-config依賴,或者依賴版本不對(duì)。
- 原因 2:bootstrap.yml里沒(méi)配置 Nacos 的server-addr,或者配置錯(cuò)了。
- 解決:檢查依賴是否正確,檢查bootstrap.yml的 Nacos 地址是否正確,配置發(fā)布后等 10 秒再測(cè)試(Nacos 有緩存)。
九、總結(jié):這組合為啥比 Spring Security 香?
咱花了這么多時(shí)間搭完這個(gè)鑒權(quán)系統(tǒng),最后來(lái)總結(jié)下:Sa-Token + Gateway + Nacos 這組合,到底比 Spring Security 好在哪?
1. 代碼量少到離譜
- Spring Security:實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 token 鑒權(quán),要寫WebSecurityConfigurerAdapter、UserDetailsService、JwtTokenProvider等一堆類,配置文件還得寫半天。
- Sa-Token:登錄就一行StpUtil.login(userId),鑒權(quán)就一行StpUtil.checkLogin(),網(wǎng)關(guān)過(guò)濾器幾行代碼搞定,代碼量至少減少 80%。
2. 學(xué)習(xí)成本低
- Spring Security:要理解 “認(rèn)證流程”“授權(quán)流程”“過(guò)濾器鏈”“SecurityContext” 等一堆概念,新手入門至少得一周。
- Sa-Token:API 直觀到不用看文檔都能猜懂,StpUtil.hasRole()就是判斷角色,StpUtil.logout()就是退出登錄,新手半天就能上手。
3. 動(dòng)態(tài)配置更靈活
- Spring Security:要改個(gè)攔截路徑、token 有效期,得改代碼、重啟服務(wù),麻煩得很。
- Sa-Token + Nacos:直接在 Nacos 上改配置,10 秒生效,不用重啟服務(wù),運(yùn)維效率直接拉滿。
4. 報(bào)錯(cuò)信息更友好
- Spring Security:報(bào)個(gè)AccessDeniedException,你還得自己排查是 “沒(méi)登錄” 還是 “沒(méi)權(quán)限”。
- Sa-Token:直接報(bào) “未登錄,請(qǐng)先登錄!”“無(wú)此權(quán)限,請(qǐng)聯(lián)系管理員!”,連排查方向都給你指好了,調(diào)試效率高多了。





























