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





























