搭建基于sa-token 的网关权限管理系统

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

image.png

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,如果不写取不到当前请求的信息

总结

总结记录下,希望能帮到有缘人

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

香菜+

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值