1、需求
想要搭建一个基于spring cloud gateway ,sa-token的网关权限管理系统,系统支持多种登录方式,如密码,用户名登录,微信登录,手机登录。使用sa-token主要是使用他的token支持,通过token实现同统一登录,同时基于redis实现token共享。
项目基于springboot3.5,spring cloud gateway 2025.0.0
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2025.0.0</version> <!-- 与Spring Boot 3.5.0兼容 -->
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
数据表设计
权限设计是基于RBAC设计,基于角色设计,也就是角色拥有权限,用户拥有角色。
2.1 配置
先配置路由,路由的配置格式和spring cloud gateway一致
创建角色 ,根据业务随意定义角色,粒度可以小一些
接着给角色分配路由权限
用户注册后可以分配角色
2.2 路由查找过程
登录后获得登录id,然后查询当前用户有的角色,根据匹配的路由id,获得有当前路由权限的所有角色id,如果有交叉则代表当前用户有权限,否则无权限 。
登录服务器接入 登录服务器主要是颁发token,这里使用的是redis共享token,方便其他的服务可以获取,验证token。
pom.xml修改
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.44.0</version>
</dependency>
<!-- Sa-Token 整合 RedisTemplate -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-template</artifactId>
<version>1.44.0</version>
</dependency>
<!-- 提供 Redis 连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL JDBC 驱动(MySQL 8) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
登录接口
@PostMapping("pwd")
public ResponseEntity<SaResult> loginWithPwd(@RequestBody LoginPwdDTO loginPwdDto) {
SaResult login = pwdService.Login(loginPwdDto);
return ResponseEntity.ok(login);
}
登录实现,主要是去数据库验证用户名和密码,设置用户拥有的角色
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import com.tyjt.common.TYJT_Constants;
import com.tyjt.common.mysql.entity.UserInfoEntity;
import com.tyjt.common.mysql.entity.UserRoleInfoEntity;
import com.tyjt.common.mysql.repo.UserInfoRepository;
import com.tyjt.common.mysql.repo.UserRoleRepository;
import com.tyjt.loginsrv.auth.AbsLogin;
import com.tyjt.loginsrv.domain.dto.LoginPwdDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class PwdService extends AbsLogin<LoginPwdDTO> {
@Resource
UserInfoRepository userInfoRepository;
@Resource
UserRoleRepository userRoleRepository;
@Override
public SaResult doLogic(LoginPwdDTO loginPwdDto) {
UserInfoEntity userInfoEntity = userInfoRepository.findByNameAndPwd(loginPwdDto.getUserName(), loginPwdDto.getPwd());
// 第一步:比对前端提交的账号名称、密码
if (userInfoEntity != null) {
// 第二步:根据账号id,进行登录
StpUtil.login(userInfoEntity.getId());
SaSession session = StpUtil.getSession();
List<UserRoleInfoEntity> byRoleId = userRoleRepository.findByRoleId(userInfoEntity.getId());
Set<Integer> roleSet = byRoleId.stream().map(UserRoleInfoEntity::getRoleId).collect(Collectors.toSet());
session.set(TYJT_Constants.ROLE_KEY, roleSet);
return SaResult.ok("登录成功");
}
return SaResult.error("登录失败");
}
@Override
public boolean checkParameter(LoginPwdDTO loginPwdDto) {
returntrue;
}
}
session.set(TYJT_Constants.ROLE_KEY, roleSet);
数据库中会有用户的角色列表,如下图中的“1”代表角色1
3、网关服务器接入
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
</dependency>
<!-- Sa-Token 权限认证(Reactor响应式集成),在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
<version>1.44.0</version>
</dependency>
<!-- Sa-Token 整合 RedisTemplate -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-template</artifactId>
<version>1.44.0</version>
</dependency>
<!-- 提供 Redis 连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL JDBC 驱动(MySQL 8) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
配置sa-token过滤器,这里只做登录验证
@Configuration
public class SaTokenFilterConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE) // 最高优先级
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter()
.addInclude("/**")
.setAuth(obj -> {
// 登录校验
StpUtil.checkLogin();
})
.setError(e -> {
// 认证失败返回 JSON
return Mono.just(ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.contentType(MediaType.APPLICATION_JSON)
.body("{\"code\":401,\"msg\":\"" + e.getMessage() + "\"}"));
});
}
}
权限验证放在一个单独的filter内
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Resource
RoleRouteCache roleRouteCache;
@Override
public int getOrder() {
return -1; // 高优先级
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 手动初始化WebFlux上下文
SaReactorSyncHolder.setContext(exchange);
// 2. 获取会话对象(从Redis中读取)
SaSession session = StpUtil.getSession();
// 2. 路由校验
Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
String routeId = route.getId();
Set<Integer> roleSet = (Set<Integer>) session.get(TYJT_Constants.ROLE_KEY);
Set<Integer> configRoleSet = roleRouteCache.getCache().get(routeId);
if (configRoleSet != null){
boolean hasIntersection = roleSet.stream().anyMatch(configRoleSet::contains);
if (hasIntersection) {
return chain.filter(exchange);
}else {
return unauthorized(exchange, "没有权限");
}
}else {
return unauthorized(exchange, "没有权限");
}
}
private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
byte[] bytes = message.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
return exchange.getResponse().writeWith(Mono.just(buffer));
}
}
“注意:// 手动初始化WebFlux上下文SaReactorSyncHolder.setContext(exchange);
这句至关重要,主要是关联能取到session,如果不写取不到当前请求的信息
总结
总结记录下,希望能帮到有缘人