Spring Gateway、Sa-Token、Nacos 認(rèn)證/鑒權(quán)方案,yyds!
之前進(jìn)行鑒權(quán)、授權(quán)都要寫一大堆代碼。如果使用像Spring Security這樣的框架,又要花好多時(shí)間學(xué)習(xí),拿過(guò)來(lái)一用,好多配置項(xiàng)也不知道是干嘛用的,又不想了解。要是不用Spring Security,token的生成、校驗(yàn)、刷新,權(quán)限的驗(yàn)證分配,又全要自己寫,想想都頭大。
Spring Security太重而且配置繁瑣。自己實(shí)現(xiàn)所有的點(diǎn)必須又要顧及到,更是麻煩。
最近看到一個(gè)權(quán)限認(rèn)證框架,真是夠簡(jiǎn)單高效。這里分享一個(gè)使用Sa-Token的gateway鑒權(quán)demo。
需求分析
圖片
結(jié)構(gòu)
圖片
認(rèn)證
sa-token模塊
我們首先編寫sa-token模塊進(jìn)行token生成和權(quán)限分配。
在sa-token的session模式下生成token非常方便,只需要調(diào)用
StpUtil.login(Object id);就可以為賬號(hào)生成 Token 憑證與 Session 會(huì)話了。
配置信息
server:
# 端口
port:8081
spring:
application:
name:weishuang-account
datasource:
driver-class-name:com.mysql.cj.jdbc.Driver
url:jdbc:mysql://127.0.0.1:3306/weishuang_account?useUnicode=true&characterEncoding=utf8&autoRecnotallow=true&allowMultiQueries=true&useSSL=false&serverTimeznotallow=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL
username:root
password:root
# redis配置
redis:
# Redis數(shù)據(jù)庫(kù)索引(默認(rèn)為0)
database:0
# Redis服務(wù)器地址
host:127.0.0.1
# Redis服務(wù)器連接端口
port:6379
# Redis服務(wù)器連接密碼(默認(rèn)為空)
# password:
# 連接超時(shí)時(shí)間
timeout:10s
lettuce:
pool:
# 連接池最大連接數(shù)
max-active:200
# 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒(méi)有限制)
max-wait:-1ms
# 連接池中的最大空閑連接
max-idle:10
# 連接池中的最小空閑連接
min-idle:0
############## Sa-Token 配置 (文檔: https://sa-token.cc) ##############
sa-token:
# token名稱 (同時(shí)也是cookie名稱)
token-name:weishuang-token
# token有效期,單位s 默認(rèn)30天, -1代表永不過(guò)期
timeout:2592000
# token臨時(shí)有效期 (指定時(shí)間內(nèi)無(wú)操作就視為token過(guò)期) 單位: 秒
activity-timeout:-1
# 是否允許同一賬號(hào)并發(fā)登錄 (為true時(shí)允許一起登錄, 為false時(shí)新登錄擠掉舊登錄)
is-concurrent:true
# 在多人登錄同一賬號(hào)時(shí),是否共用一個(gè)token (為true時(shí)所有登錄共用一個(gè)token, 為false時(shí)每次登錄新建一個(gè)token)
is-share:true
# token風(fēng)格
token-style:uuid
# 是否輸出操作日志
is-log:false
# token前綴
token-prefix:Bearer在sa-token的配置中,我使用了token-name來(lái)指定token的名稱,如果不指定那么就是默認(rèn)的satoken。
使用token-prefix來(lái)指定token的前綴,這樣前端在header里傳入token的時(shí)候就要加上Bearer了(注意有個(gè)空格),建議和前端商量一下需不需要這個(gè)前綴,如果不使用,直接傳token就好了。
現(xiàn)在調(diào)用接口時(shí)傳入的格式就是
weishuang-token = Bearer token123456sa-token的session模式需要redis來(lái)存儲(chǔ)session,在微服務(wù)中,各個(gè)服務(wù)的session也需要redis來(lái)同步。
當(dāng)然sa-token也支持jwt來(lái)生成無(wú)狀態(tài)的token,這樣就不需要在服務(wù)中引入redis了。本文使用session模式(jwt的刷新token等機(jī)制還要自己實(shí)現(xiàn),session的刷新sa-token都幫我們做好了,使用默認(rèn)的模式更加方便,而且功能更多)
我們來(lái)編寫一個(gè)登錄接口
User
@Data
public class User {
/**
* id
*/
private String id;
/**
* 賬號(hào)
*/
private String userName;
/**
* 密碼
*/
private String password;
}UserController
@RestController
@RequestMapping("/account/user/")
public class UserController {
@Autowired
private UserManager userManager;
@PostMapping("doLogin")
public SaResult doLogin(@RequestBody AccountUserLoginDTO req) {
userManager.login(req);
return SaResult.ok("登錄成功");
}
}UserManager
@Component
publicclass UserManagerImpl implements UserManager {
@Autowired
private UserService userService;
@Override
public void login(AccountUserLoginDTO req) {
//生成密碼
String password = PasswordUtil.generatePassword(req.getPassword());
//調(diào)用數(shù)據(jù)庫(kù)校驗(yàn)是否存在用戶
User user = userService.getOne(req.getUserName(), password);
if (user == null) {
thrownew RuntimeException("賬號(hào)或密碼錯(cuò)誤");
}
//為賬號(hào)生成Token憑證與Session會(huì)話
StpUtil.login(user.getId());
//為該用戶的session存儲(chǔ)更多信息
//這里為了方便直接把user實(shí)體存進(jìn)去了,也包括了密碼,自己實(shí)現(xiàn)時(shí)不建議這樣做。
StpUtil.getSession().set("USER_DATA", user);
}
}UserService
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
private UserMapper userMapper;
public User getOne(String username, String password){
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username)
.eq(User::getPassword,password);
return userMapper.selectOne(queryWrapper);
}
}gateway模塊
依賴
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- 引入gateway網(wǎng)關(guān) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- Sa-Token 權(quán)限認(rèn)證(Reactor響應(yīng)式集成), 在線文檔:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot-starter</artifactId>
<version>1.34.0</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.34.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>配置
server:
port:9000
spring:
application:
name:weishuang-gateway
cloud:
loadbalancer:
ribbon:
enabled:false
nacos:
discovery:
username:nacos
password:nacos
server-addr:localhost:8848
gateway:
routes:
-id:account
uri:lb://weishuang-account
order:1
predicates:
-Path=/account/**
# redis配置
redis:
# Redis數(shù)據(jù)庫(kù)索引(默認(rèn)為0)
database:0
# Redis服務(wù)器地址
host:127.0.0.1
# Redis服務(wù)器連接端口
port:6379
# Redis服務(wù)器連接密碼(默認(rèn)為空)
# password:
# 連接超時(shí)時(shí)間
timeout:10s
lettuce:
pool:
# 連接池最大連接數(shù)
max-active:200
# 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒(méi)有限制)
max-wait:-1ms
# 連接池中的最大空閑連接
max-idle:10
# 連接池中的最小空閑連接
min-idle:0
############## Sa-Token 配置 (文檔: https://sa-token.cc) ##############
sa-token:
# token名稱 (同時(shí)也是cookie名稱)
token-name:weishuang-token
# token有效期,單位s 默認(rèn)30天, -1代表永不過(guò)期
timeout:2592000
# token臨時(shí)有效期 (指定時(shí)間內(nèi)無(wú)操作就視為token過(guò)期) 單位: 秒
activity-timeout:-1
# 是否允許同一賬號(hào)并發(fā)登錄 (為true時(shí)允許一起登錄, 為false時(shí)新登錄擠掉舊登錄)
is-concurrent:true
# 在多人登錄同一賬號(hào)時(shí),是否共用一個(gè)token (為true時(shí)所有登錄共用一個(gè)token, 為false時(shí)每次登錄新建一個(gè)token)
is-share:true
# token風(fēng)格
token-style:uuid
# 是否輸出操作日志
is-log:false
# token前綴
token-prefix:Bearer同樣的,在gateway中也需要配置sa-token和redis,注意和在account服務(wù)中配置的要一致,否則在redis中獲取信息的時(shí)候找不到。
gateway我們也注冊(cè)到nacos中。
攔截認(rèn)證
package com.weishuang.gateway.gateway.config;
import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Configuration
publicclass SaTokenConfigure {
// 注冊(cè) Sa-Token全局過(guò)濾器
@Bean
public SaReactorFilter getSaReactorFilter() {
returnnew SaReactorFilter()
// 攔截地址
.addInclude("/**") /* 攔截全部path */
// 開放地址
.addExclude("/favicon.ico")
// 鑒權(quán)方法:每次訪問(wèn)進(jìn)入
.setAuth(obj -> {
// 登錄校驗(yàn) -- 攔截所有路由,并排除/account/user/doLogin用于開放登錄
SaRouter.match("/**", "/account/user/doLogin", r -> StpUtil.checkLogin());
// // 權(quán)限認(rèn)證 -- 不同模塊, 校驗(yàn)不同權(quán)限
// SaRouter.match("/account/**", r -> StpUtil.checkRole("user"));
// SaRouter.match("/admin/**", r -> StpUtil.checkRole("admin"));
// SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
// SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
// 更多匹配 ... */
})
// 異常處理方法:每次setAuth函數(shù)出現(xiàn)異常時(shí)進(jìn)入
.setError(e -> {
return SaResult.error(e.getMessage());
})
;
}
}只需要在gateway中添加一個(gè)全局過(guò)濾器進(jìn)行鑒權(quán)操作就可以實(shí)現(xiàn)認(rèn)證/鑒權(quán)操作了。
這里我們對(duì)**全部路徑進(jìn)行攔截,但不要忘記把我們的登錄接口釋放出來(lái),允許訪問(wèn)。
到這里簡(jiǎn)單的認(rèn)證操作就實(shí)現(xiàn)了。我們僅僅使用了sa-token的一個(gè)StpUtil.login(Object id)方法,其他事情sa-token都幫我們完成了,更無(wú)需復(fù)雜的配置和多到爆炸的Bean。
鑒權(quán)
有時(shí)候一個(gè)token認(rèn)證并不能讓我們區(qū)分用戶能不能訪問(wèn)這個(gè)資源,使用那個(gè)菜單,我們需要更細(xì)粒度的鑒權(quán)。
在經(jīng)典的RBAC模型里,用戶會(huì)擁有多個(gè)角色,不同的角色又會(huì)有不同的權(quán)限。
這里我們使用五個(gè)表來(lái)表示用戶、角色、權(quán)限之間的關(guān)系。
圖片
很顯然,我們想判斷用戶有沒(méi)有權(quán)限訪問(wèn)一個(gè)path,需要判斷用戶是否還有該權(quán)限。
在sa-token中想要實(shí)現(xiàn)這個(gè)功能,只需要實(shí)現(xiàn)StpInterface接口即可。
/**
* 自定義權(quán)限驗(yàn)證接口擴(kuò)展
*/
@Component
publicclass StpInterfaceImpl implements StpInterface {
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 返回此 loginId 擁有的權(quán)限列表
return ...;
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 返回此 loginId 擁有的角色列表
return ...;
}
}我們?cè)趃ateway實(shí)現(xiàn)這個(gè)接口,為用戶賦予權(quán)限,再進(jìn)行權(quán)限校驗(yàn),就可以精確到path了。
我們使用先從Redis中獲取緩存數(shù)據(jù),獲取不到時(shí)走RPC調(diào)用account服務(wù)獲取。
為了更方便的使用gateway調(diào)用account服務(wù),我們使用nacos進(jìn)行服務(wù)發(fā)現(xiàn),用feign調(diào)用。
在account和gateway服務(wù)中配置nacos
配置nacos
在兩個(gè)服務(wù)中加入nacos的配置
spring:
cloud:
nacos:
discovery:
username:nacos
password:nacos
server-addr:localhost:8848
datasource:
driver-class-name:com.mysql.cj.jdbc.Driver
url:jdbc:mysql://127.0.0.1:3306/weishuang_account?useUnicode=true&characterEncoding=utf8&autoRecnotallow=true&allowMultiQueries=true&useSSL=false&serverTimeznotallow=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL
username:root
password:root配置gateway
需要注意的是,gateway是基于WebFlux的一個(gè)響應(yīng)式組件,HttpMessageConverters不會(huì)像Spring Mvc一樣自動(dòng)注入,需要我們手動(dòng)配置。
package com.weishuang.gateway.gateway.config;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import java.util.stream.Collectors;
@Configuration
publicclass HttpMessageConvertersConfigure {
@Bean
@ConditionalOnMissingBean
public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
returnnew HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
}
}實(shí)現(xiàn)獲取角色、權(quán)限接口
在account中實(shí)現(xiàn)通過(guò)用戶獲取角色、獲取權(quán)限的接口
RoleController、PermissionController
@RestController
@RequestMapping("/account/role/")
publicclass RoleController {
@Autowired
private RoleManager roleManager;
@PostMapping("/getRoles")
public List<RoleDTO> getRoles(@RequestParam String userId) {
return roleManager.getRoles(userId);
}
}
@RestController
@RequestMapping("/account/permission/")
publicclass PermissionController {
@Autowired
private PermissionManager permissionManager;
@PostMapping("/getPermissions")
public List<PermissionDTO> getPermissions(@RequestParam String userId) {
return permissionManager.getPermissions(userId);
}
}RoleManager
@Component
publicclass RoleManagerImpl implements RoleManager {
@Autowired
private RoleService roleService;
@Autowired
private UserRoleService userRoleService;
@Autowired
private Role2RoleDTOCovert role2RoleDTOCovert;
@Override
public List<RoleDTO> getRoles(String userId) {
List<UserRole> userRoles = userRoleService.getByUserId(userId);
Set<String> roleIds = userRoles.stream().map(UserRole::getRoleId).collect(Collectors.toSet());
List<RoleDTO> roleDTOS = role2RoleDTOCovert.covertTargetList2SourceList(roleService.getByIds(roleIds));
//服務(wù)不對(duì)外暴露,網(wǎng)關(guān)不傳token到子服務(wù),這里通過(guò)userId獲取session,并設(shè)置角色。
String tokenValue = StpUtil.getTokenValueByLoginId(userId);
//為這個(gè)token在redis中設(shè)置角色,使網(wǎng)關(guān)獲取更方便
if(StringUtils.isNotEmpty(tokenValue)){
if(CollectionUtils.isEmpty(roleDTOS)){
StpUtil.getTokenSessionByToken(tokenValue).set("ROLES", "");
}else{
List<String> roleNames = roleDTOS.stream().map(RoleDTO::getRoleName).collect(Collectors.toList());
StpUtil.getTokenSessionByToken(tokenValue).set("ROLES", ListUtil.list2String(roleNames));
}
}
return roleDTOS;
}
}PermissionManager
@Component
publicclass PermissionManagerImpl implements PermissionManager {
@Autowired
private PermissionService permissionService;
@Autowired
private RolePermService rolePermService;
@Autowired
private UserRoleService userRoleService;
@Autowired
private Permission2PermissionDTOCovert permissionDTOCovert;
@Override
public List<PermissionDTO> getPermissions(String userId) {
//獲取用戶的角色
List<UserRole> roles = userRoleService.getByUserId(userId);
if (CollectionUtils.isEmpty(roles)) {
handleUserPermSession(userId, null);
}
Set<String> roleIds = roles.stream().map(UserRole::getRoleId).collect(Collectors.toSet());
List<RolePerm> rolePerms = rolePermService.getByRoleIds(roleIds);
if (CollectionUtils.isEmpty(rolePerms)) {
handleUserPermSession(userId, null);
}
Set<String> permIds = rolePerms.stream().map(RolePerm::getPermId).collect(Collectors.toSet());
List<PermissionDTO> perms = permissionDTOCovert.covertTargetList2SourceList(permissionService.getByIds(permIds));
handleUserPermSession(userId, perms);
return perms;
}
private void handleUserPermSession(String userId, List<PermissionDTO> perms) {
//通過(guò)userId獲取session,并設(shè)置權(quán)限
String tokenValue = StpUtil.getTokenValueByLoginId(userId);
if (StringUtils.isNotEmpty(tokenValue)) {
//為了防止沒(méi)有權(quán)限的用戶多次進(jìn)入到該接口,沒(méi)權(quán)限的用戶在redis中存入空字符串
if (CollectionUtils.isEmpty(perms)) {
StpUtil.getTokenSessionByToken(tokenValue).set("PERMS", "");
} else {
List<String> paths = perms.stream().map(PermissionDTO::getPath).collect(Collectors.toList());
StpUtil.getTokenSessionByToken(tokenValue).set("PERMS", ListUtil.list2String(paths));
}
}
}
}gateway獲取角色、權(quán)限
方式一:
官方寫的實(shí)現(xiàn)StpInterfaceImpl中的方法
作為一個(gè)異步組件,gateway中不允許使用引起阻塞的同步調(diào)用,若使用feign進(jìn)行調(diào)用就會(huì)發(fā)生錯(cuò)誤,我們使用CompletableFuture來(lái)將同步調(diào)用轉(zhuǎn)換成異步操作,但使用CompletableFuture我們需要指定線程池,否則將會(huì)使用默認(rèn)的ForkJoinPool
這里我們創(chuàng)建一個(gè)線程池,用于權(quán)限獲取使用
package com.weishuang.gateway.gateway.config;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
@Configuration
publicclass ThreadPollConfig {
privatefinal BlockingQueue<Runnable> asyncSenderThreadPoolQueue = new LinkedBlockingQueue<Runnable>(50000);
publicfinal ExecutorService USER_ROLE_PERM_THREAD_POOL = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(),
Runtime.getRuntime().availableProcessors(),
1000 * 60,
TimeUnit.MILLISECONDS,
this.asyncSenderThreadPoolQueue,
new ThreadFactory() {
privatefinal AtomicInteger threadIndex = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
returnnew Thread(r, "RolePermExecutor_" + this.threadIndex.incrementAndGet());
}
});
}StpInterfaceImpl
@Component
publicclass StpInterfaceImpl implements StpInterface {
@Autowired
private RoleFacade roleFacade;
@Autowired
private PermissionFacade permissionFacade;
@Autowired
private ThreadPollConfig threadPollConfig;
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
Object res = StpUtil.getTokenSession().get("PERMS");
if (res == null) {
CompletableFuture<List<String>> permFuture = CompletableFuture.supplyAsync(() -> {
// 返回此 loginId 擁有的權(quán)限列表
List<PermissionDTO> permissions = permissionFacade.getPermissions((String) loginId);
return permissions.stream().map(PermissionDTO::getPath).collect(Collectors.toList());
}, threadPollConfig.USER_ROLE_PERM_THREAD_POOL);
try {
return permFuture.get();
} catch (InterruptedException | ExecutionException e) {
thrownew RuntimeException(e);
}
}
String paths = (String) res;
System.out.println(paths);
return ListUtil.string2List(paths);
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
Object res = StpUtil.getTokenSession().get("ROLES");
if (res == null) {
CompletableFuture<List<String>> roleFuture = CompletableFuture.supplyAsync(() -> {
// 返回此 loginId 擁有的權(quán)限列表
List<RoleDTO> roles = roleFacade.getRoles((String) loginId);
return roles.stream().map(RoleDTO::getRoleName).collect(Collectors.toList());
}, threadPollConfig.USER_ROLE_PERM_THREAD_POOL);
try {
return roleFuture.get();
} catch (InterruptedException | ExecutionException e) {
thrownew RuntimeException(e);
}
}
String roleNames = (String) res;
System.out.println(roleNames);
return ListUtil.string2List(roleNames);
}
}gateway配置過(guò)濾器,實(shí)現(xiàn)鑒權(quán)
@Component
publicclass ForwardAuthFilter implements WebFilter {
static Set<String> whitePaths = new HashSet<>();
static {
whitePaths.add("/account/user/doLogin");
whitePaths.add("/account/user/logout");
whitePaths.add("/account/user/register");
}
@Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
ServerHttpRequest serverHttpRequest = serverWebExchange.getRequest();
String path = serverHttpRequest.getPath().toString();
//需要校驗(yàn)權(quán)限
if(!whitePaths.contains(path)){
//判斷用戶是否有該權(quán)限
if(!StpUtil.hasPermission(path)){
thrownew NotPermissionException(path);
}
}
return webFilterChain.filter(serverWebExchange);
}
}方式二:
如果您覺(jué)得一定要使用響應(yīng)式才行,那么無(wú)需實(shí)現(xiàn)StpInterfaceImpl
/**
* 全局過(guò)濾器
*/
@Component
publicclass ForwardAuthFilter implements WebFilter {
@Autowired
private WebClient.Builder webClientBuilder;
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
String path = serverHttpRequest.getPath().toString();
// /api開頭的都要鑒權(quán)
if (StringUtils.isNotEmpty(path) && path.startsWith("/api")) {
Mono<List<String>> permissionList = getPermissionList();
return permissionList.flatMap(list -> {
if (!StpUtil.stpLogic.hasElement(list, path)) {
return Mono.error(new NotPermissionException(path));
}
return chain.filter(exchange);
});
}
return chain.filter(exchange);
}
@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
private Mono<List<String>> getPermissionList() {
String userId = (String) StpUtil.getLoginId();
Mono<List<PermissionDTO>> listMono = webClientBuilder.build()
.post()
.uri("http://weishuang-account/account/permission/getPermissions")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData("userId", userId))
.retrieve()
.bodyToFlux(PermissionDTO.class)
.collectList();
return listMono.map(permissions -> permissions.stream().map(PermissionDTO::getPath).collect(Collectors.toList()));
}
}修改sa-token的配置
@Configuration
publicclass SaTokenConfigure {
// 注冊(cè) Sa-Token全局過(guò)濾器
@Bean
public SaReactorFilter getSaReactorFilter() {
returnnew SaReactorFilter()
// 攔截地址
.addInclude("/**") /* 攔截全部path */
// 開放地址
.addExclude("/favicon.ico")
// 鑒權(quán)方法:每次訪問(wèn)進(jìn)入
.setAuth(obj -> {
// 登錄校驗(yàn) -- 攔截所有路由,排除白名單
SaRouter.match("/**")
.notMatch(new ArrayList<>(WhitePath.whitePaths))
.check(r -> StpUtil.checkLogin());
})
// 異常處理方法:每次setAuth函數(shù)出現(xiàn)異常時(shí)進(jìn)入
.setError(e -> {
return SaResult.error(e.getMessage());
});
}
}白名單
public class WhitePath {
static Set<String> whitePaths = new HashSet<>();
static {
whitePaths.add("/account/user/doLogin");
whitePaths.add("/account/user/logout");
whitePaths.add("/account/user/register");
}
}































