本文章使用的版本:

SpringBoot:2.6.13

JDK:1.8

SpringCloud Alibaba:2021.0.5.0

Satoken:1.37.0

微服务架构下的鉴权一般分为两种:

  1. 每个服务各自鉴权

  2. 网关统一鉴权

方案一和传统单体鉴权差别不大,不再过多赘述,本篇介绍方案二的整合步骤(以及在下游微服务、服务与服务之间传递用户信息):

流程梳理

大致流程与下图相似只是将jwt替换为了satoken

项目架构

在网关中引入如下依赖和配置

依赖:

<!-- Sa-Token 权限认证(Reactor响应式集成),在线文档:https://sa-token.cc -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-reactor-spring-boot-starter</artifactId>
    <version>1.37.0</version>
</dependency>

配置:

package team.weyoung.gateway.config;

import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.util.SaResult;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * [Sa-Token 权限认证] 配置类
 *
 * @author Tunan
 */
@Configuration
public class SaTokenConfigure {
    // 注册 Sa-Token全局过滤器
    @Bean
    public SaReactorFilter getSaReactorFilter() {
        return new SaReactorFilter()
                // 拦截地址
                .addInclude("/**")    /* 拦截全部path */
                // 开放地址
                .addExclude("/favicon.ico")
                // 鉴权方法:每次访问进入
                .setAuth(obj -> {
                    // 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
                    // todo 这里的路由需要根据实际情况修改
                    //SaRouter.match("/**", "/user/doLogin", r -> StpUtil.checkLogin());
                })
                // 异常处理方法:每次setAuth函数出现异常时进入
                .setError(e -> {
                    return SaResult.error(e.getMessage());
                })
                ;
    }
}

这里还需要一个过滤器将参数转发到下游微服务

package team.weyoung.gateway.config;

import cn.dev33.satoken.stp.StpUtil;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;


import reactor.core.publisher.Mono;

/**
 * 全局过滤器,为请求添加 satoken 参数
 *
 * @author kong
 *
 */
@Component
public class SaIdTokenFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest newRequest = exchange
                .getRequest()
                .mutate()
                // 为请求追加 satoken 参数
                .header("satoken",StpUtil.getTokenValue())
                .build();
        ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
        return chain.filter(newExchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

在common模块引入以下依赖和配置(其他服务依赖于common模块)

依赖:

<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-reactor-spring-boot-starter</artifactId>
    <version>1.37.0</version>
</dependency>

配置(注册全局拦截器)

package team.weyoung.ojmodel.satoken;

import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Sa-Token 代码方式进行配置
 *
 * @author kong
 */
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {

    /**
     * 注册 Sa-Token 的拦截器,打开注解式鉴权功能
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
    }

    /**
     * 注册 Sa-Token全局过滤器,解决跨域问题
     */
    @Bean
    public SaServletFilter getSaServletFilter() {
        return new SaServletFilter()
                // 拦截与排除 path
                .addInclude("/**").addExclude("/favicon.ico")

                // 全局认证函数
                .setAuth(obj -> {
                    // 校验 Id-Token 身份凭证     —— 以下两句代码可简化为:SaIdUtil.checkCurrentRequestToken();
                    String token = SaHolder.getRequest().getHeader("satoken");
                    StpUtil.getTokenSessionByToken(token);
                })
                .setBeforeAuth(obj -> {
                })
                ;
    }

}

用户服务

引入依赖:

<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-redis-jackson</artifactId>
    <version>1.37.0</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

yml文件:

############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
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: true
  # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
  token-style: uuid
  # 是否输出操作日志
  is-log: true
  # 是否从cookie中读取token
  is-read-cookie: false

最后在我们的用户服务就可以使用StpUtil获取登录信息了,这里我使用的是无cookie模式将tokenvalue塞到返回值里面给到前端,前端可以使用pinia或者localstorage存储token并在每次请求前在请求头加上satoken即可完成鉴权

@Override
public LoginUserVO userLogin(String userAccount, String userPassword) {
    // 1. 校验
    if (StringUtils.isAnyBlank(userAccount, userPassword)) {
        throw new BusinessException(HttpCodeEnum.PARAMS_ERROR, "参数为空");
    }
    if (userAccount.length() < 4) {
        throw new BusinessException(HttpCodeEnum.PARAMS_ERROR, "账号错误");
    }
    if (userPassword.length() < 8) {
        throw new BusinessException(HttpCodeEnum.PARAMS_ERROR, "密码错误");
    }
    // 2. 加密
    String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
    // 查询用户是否存在
    QueryWrapper queryWrapper = QueryWrapper.create()
            .from(USER)
            .where(USER.USER_ACCOUNT.eq(userAccount))
            .where(USER.USER_PASSWORD.eq(encryptPassword));
    User user = userMapper.selectOneByQuery(queryWrapper);
    // 用户不存在
    if (user == null) {
        log.info("user login failed, userAccount cannot match userPassword");
        throw new BusinessException(HttpCodeEnum.PARAMS_ERROR, "用户不存在或密码错误");
    }
    // 3. 记录用户的登录态
    long userId = user.getId();
    StpUtil.login(userId);
    StpUtil.getSession().set(SaSession.USER, user);
    SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
    System.out.println(tokenInfo);
    LoginUserVO loginUserVO = this.getLoginUserVO(user);
    loginUserVO.setToken(tokenInfo);
    return loginUserVO;
}