SpringBoot集成Sa-Token进行登录鉴权

SpringBoot集成Sa-token

Sa-Token是轻量级的认证框架,主要解决:登录认证,权限认证、单点登录、微服务网关鉴权,就不用自己手写认证过程,非常方便,像权限菜单,权限按钮Sa-Token都可以帮助我们去实现

Sa-Token文档:链接

Sa-Token开源代码:链接

1.1、基础导入

依赖导入

<!-- SpringBoot2.x -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>1.44.0</version>
</dependency>

<!-- SpringBoot3.x -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot3-starter</artifactId>
    <version>1.44.0</version>
</dependency>

application.yml

sa-token: 
    # token 名称(同时也是 cookie 名称)
    token-name: satoken
    # token 有效期(单位:秒) 默认30天,-1 代表永久有效
    timeout: 2592000
    # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
    active-timeout: -1
    # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
    is-concurrent: true
    # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
    is-share: false
    # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
    token-style: uuid
    # 是否输出操作日志 
    is-log: true

1.2、简易登录操作(单体架构)

controller层

这个可能有点复杂了,因为我直接用微服务中的登录代码,稍微解读一下

@PostMapping("/login")
public Result systemUserLogin(@RequestBody LoginDTO loginDTO) {

    // 在微服务中,在认证服务使用了OpenFeign远程调用login这个接口,如果在单体架构中,直接调用自己的查询用户接口就可以了,因为登录步骤一般都是:
    // 1、输入账户和密码
    // 2、根据账户判断用户是否存在
    // 3、存在就进行一系列的基础校验(检查完后,将用户信息返回给Controller层)
    // 稍微改造一下就是SystemUser systemUser = cloudSystemUserAPI.login(loginDTO.getUserName(), loginDTO.getPassword());这是单体架构中的一个示例,并不是当前的标准写法
    Result result = cloudSystemUserAPI.login(loginDTO.getUserName(), loginDTO.getPassword());

    // 获取返回的data数据,获取data中的id进行sa-token权限设置(这一段可以不看,微服务中的)
    LinkedHashMap<String, Object> data = (LinkedHashMap<String, Object>) result.getData();
    if (data == null) {
        String msg = result.getMsg();
        ResultCodeEnum code = getByMessage(msg);
        return Result.build(null, code);
    }
    // 直接看这里,我们根据改造后的写法就是Integer id = systemUser.getId();
    // 然后直接StpUtil.login(id);就可以了,然后sa-token就会返回如下格式:
    // 这时可以观察cookie中会携带tokenName,然后在其他操作中进行注解鉴权就可以了@SaCheckLogin,详见说明文档中的注解鉴权
    Integer id = (Integer) data.get("id");
    StpUtil.login(id);
    SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
    return Result.build(null, SUCCESS, tokenInfo);
}
返回格式:
"saTokenInfo": {
    "tokenName": "satoken", // token-name
    "tokenValue": "23cd50f1-6cfd-45c2-afde-6970265ac36f", // tokenValue
    "isLogin": true, // 是否登录
    "loginId": "1", // 登录的用户id
    "loginType": "login", // 登录类型
    "tokenTimeout": 55,
    "sessionTimeout": 55,
    "tokenSessionTimeout": -2,
    "tokenActiveTimeout": -1,
    "loginDeviceType": "DEF",
    "tag": null
}

image

1.3、多用户登录操作(微服务)

这个我自己也琢磨了很久,因为是第一次用这个框架进行多用户鉴权操作,踩坑很多,所以教大家如何避坑,如何准确去操作,有点复杂,但是又不复杂

微服务架构:

网关服务(进行统一鉴权):cloud-gateway-service

认证服务(进行登录操作,派发Token):cloud-auth-service

用户服务(系统用户,普通用户):cloud-user-service

公共服务(存放一些公共配置,远程接口,自定义DTO等):cloud-common-service

我的项目的依赖关系是:所有请求通过网关服务进行转发到其他服务中(除了公共服务),而其他服务依赖于公共服务

登录流程图

这里面集成了Redis,无伤大雅

登录流程图

关键代码

网关服务

config层

@Configuration
// 必须配置,参考文档中的多用户认证
public class SaTokenConfig {

    // 注册sa-token全局过滤器
    @Bean
    public SaReactorFilter getSaReactorFilter() {
        return new SaReactorFilter()
                // 拦截地址
                .addInclude("/**")
                // 开放地址
                .addExclude("/favicon.ico")
                .addExclude("/swagger-ui/**") // 开放Swagger文档
                .addExclude("/v3/api-docs/**") // 开放OpenAPI文档
                .addExclude("/auth-service/login") // 开放认证服务中的登录接口
                .addExclude("/user-service/user") // 开发用户服务中的根据用户名进行查询
                .addExclude("/auth-service/common-user/login") // 开放认证服务中的普通用户登录接口
                .addExclude("/common-service/common-user/query") // 开发用户服务中的根据普通用户名进行查询
                // 鉴权方法:每次访问进入
                .setAuth(obj -> {
                    // 登录校验(系统用户)
                    SaRouter.match("/user-service/**").check(StpUtil::checkLogin);
                    SaRouter.match("/post-service/**").check(StpUtil::checkLogin);
                    // 登录校验(普通用户)
                    SaRouter.match("/common-service/**").check(StpUserUtil::checkLogin);
                }).setError(e -> {
                    return SaResult.error(e.getMessage());
                });
    }
}
// 这里我就没截全了,因为很多,可以参考代码:https://gitee.com/dromara/sa-token/blob/master/sa-token-demo/sa-token-demo-case/src/main/java/com/pj/satoken/StpUserUtil.java
// 这个代码主要就是普通用户登录,因为系统用户登录Type默认是login,所以不需要管,只需要设置普通用户就可以了,文档中提供了两种方式:一种就是对原生的StpUtil进行改造,另一种就是Kit模式
@Component
public class StpUserUtil {
    /**
     * 多账号体系下的类型标识
     */
    public static final String TYPE = "user";
    
    /**
     * 底层使用的 StpLogic 对象,这里源代码中没做修改,
       但是这里必须做修改,因为在多用户认证中,使用的tokenName默认为satoken,
       如果系统用户登录之后,普通用户登录的话,会把系统用户挤下线,所以这是我们需要重写方法,可以参考文档:多账户认证 -> 同端多登录,这是我的踩坑地方
     */
    public static StpLogic stpLogic = new StpLogic(TYPE) {
        @Override
        public String splicingKeyTokenName() {
            return super.splicingKeyTokenName() + "-user";
        }
    };
    ...
}

application.yml配置

spring:
  data:
    redis:
      port: 6379
      host: 192.168.10.105
      lettuce:
        pool:
          max-active: 200
          max-wait: -1ms
          max-idle: 10
          min-idle: 0
  application:
    name: cloud-gateway-service
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowed-origins:
              - "http://localhost:5173"
            allowed-methods:
              - GET
              - PUT
              - POST
              - DELETE
              - OPTIONS
            allowed-headers:
              - "Content-Type"
              - "Authorization"
              - "X-Requested-With"
            allow-credentials: true
            max-age: 7200
      routes:
        # 岗位服务
        - id: cloud-post-service    # 路由id,没有固定规则,但唯一建议与服务名对应
          uri: lb://cloud-post-service   # 此处不需要配置uri,因为此处是转发给注册中心,由注册中心进行转发
          predicates:   # 以下是断言条件,必选全部符合条件
            - Path=/post-service/**
          # - Method=POST  表示只有post请求才会进行路由转发,但是建议不这么做

        # 用户服务(系统用户和普通用户)关键点,不要将系统用户和普通用户的路径统一写user-service,分开写,
        # 一个为user-service,一个为common-service,但是都指向同一个服务,这是我踩坑的地方,
        # 如果写一样,那么出现的问题就是只登录系统用户没问题,但是只登录普通用户就出现普通用户操作读取不到token,只有登录系统用户才会读取到token
        - id: cloud-user-service
          uri: lb://cloud-user-service
          predicates:
            - Path=/user-service/**

        - id: cloud-common-service
          uri: lb://cloud-user-service
          predicates:
            - Path=/common-service/**

        # 认证服务
        - id: cloud-auth-service
          uri: lb://cloud-auth-service
          predicates:
            - Path=/auth-service/**
    nacos:
      discovery:
        server-addr: 192.168.10.105:8848
        username: nacos
        password: nacos
        namespace: public #发布服务到指定的 命名空间,默认为public
        group: DEFAULT_GROUP #发布服务到指定的 group ,默认为DEFAULT_GROUP
server:
  port: 9090
sa-token:
  # token 有效期(单位:秒) 默认30天,-1 代表永久有效
  timeout: 60
  # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
  active-timeout: -1
  # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
  is-concurrent: true
  # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
  is-share: false
  # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
  token-style: uuid
  # 是否输出操作日志
  is-log: true

认证服务

/**
 * 系统用户登录,这个就根据个人的代码来更改,因为我是自定义了一个响应结构体,思路不变就可以了
 */
@PostMapping("/login")
public Result systemUserLogin(@RequestBody LoginDTO loginDTO) {

    Result result = cloudSystemUserAPI.login(loginDTO.getUserName(), loginDTO.getPassword());

    // 获取返回的data数据,获取data中的id进行sa-token权限设置
    LinkedHashMap<String, Object> data = (LinkedHashMap<String, Object>) result.getData();
    if (data == null) {
        String msg = result.getMsg();
        ResultCodeEnum code = getByMessage(msg);
        return Result.build(null, code);
    }
    Integer id = (Integer) data.get("id");
    StpUtil.login(id);
    SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
    return Result.build(null, SUCCESS, tokenInfo);
}


/**
 * 普通用户登录
 */
@PostMapping("/common-user/login")
public Result commonUserLogin(@RequestBody LoginDTO loginDTO) {
    Result result = cloudSystemUserAPI.queryCommonUserByName(loginDTO.getCommonName(), loginDTO.getPassword());
    // 获取返回的data数据,获取data中的id进行sa-token权限设置
    LinkedHashMap<String, Object> data = (LinkedHashMap<String, Object>) result.getData();
    if (data == null) {
        String msg = result.getMsg();
        ResultCodeEnum code = getByMessage(msg);
        return Result.build(null, code);
    }
    Integer id = (Integer) data.get("id");
    StpUserUtil.login(id);
    SaTokenInfo tokenInfo = StpUserUtil.getTokenInfo();
    return Result.build(null, SUCCESS, tokenInfo);
}
}

用户服务

/**
 * 根据员工id查询员工信息
 */
@SaCheckLogin // 关键点,判断用户是否登录,这里检查的是login类型:satoken
@GetMapping("/query/{id}")
public Result querySystemUserById(@PathVariable Integer id) {

    SystemUser systemUser = systemUserService.queryDataById(id);
    SaTokenInfo tokenInfo = StpUtil.getTokenInfo();

    Integer postId = systemUser.getPostId();
    Result result = cloudSystemPostAPI.querySystemPostById(postId);
    Object data = result.getData();
    if (data instanceof LinkedHashMap) {
        LinkedHashMap<String, Object> postData = (LinkedHashMap<String, Object>) data;
        String postName = (String) postData.get("postName");
        systemUser.setPostName(postName);
    }

    return Result.build(systemUser, ResultCodeEnum.SUCCESS, tokenInfo);
}
/**
 * 根据用户id查询用户信息
 */
@SaCheckLogin(type = StpUserUtil.TYPE) // 关键点,这里检查的是user类型:satoken-user
@GetMapping("/common-user/query/{id}")
public Result queryDataById(@PathVariable Integer id) {
    CommonUser commonUser = commonUserService.queryDataById(id);
    return Result.build(commonUser, ResultCodeEnum.SUCCESS, StpUserUtil.getTokenInfo());
}

1.4、总结

对于单用户登录会发现特别简单,但是对于微服务中,多用户就有点小麻烦了。从给出的代码发现,主要配置就在网关服务那里,只要网关服务鉴权那里搞定,其他都很简单了。代码目前我暂时不想全部给出来,因为我想把项目全部做完后再开源给大家一个小参考。

posted @ 2025-08-26 16:45  Meditation丶  阅读(82)  评论(0)    收藏  举报