SataToken认证授权-快速入门

SaToken认证-快速入门

1.SaToken集成

1.1.微服务认证方案

遇到什么问题?
微服务架构下的第一个难题便是数据同步,单机版的Session在分布式环境下一般不能正常工作,为此我们需要对框架做一些特定的处理。
首先我们要明白,分布式环境下为什么Session会失效?因为用户在一个节点对会话做出的更改无法实时同步到其它的节点, 这就导致一个很严重的问题:如果用户在节点一上已经登录成功,那么当下一次的请求落在节点二上时,对节点二来讲,此用户仍然是未登录状态。
image.png
微服务认证方案?
要怎么解决这个问题呢?目前的主流方案有四种:

  1. Session同步:只要一个节点的数据发生了改变,就强制同步到其它所有节点

image.png

  1. Session粘滞:通过一定的算法,保证一个用户的所有请求都稳定的落在一个节点之上,对这个用户来讲,就好像还是在访问一个单机版的服务

image.png

  1. 建立会话中心:将Session存储在专业的缓存中间件上,使每个节点都变成了无状态服务,例如:Redis共享会话,或者使用 SpringSession+Redis

image.png

  1. 颁发无状态token:放弃Session机制,将用户数据直接写入到令牌本身上,使会话数据做到令牌自解释(客户端Token,无状态),例如:jwt

image.png
之所以叫无状态:是因为这种方式是把用户会话封装到Token中(用户,权限等),服务端不用存储用户会话,拿到Token就可以进行鉴权,无效再去获取用户会话,这种方式更加方便,另外少了获取会话的性能开销。
如何选择方案?
业界使用较多的是使用Redis建立分布式会话中心以及使用JWT客户端Token模式,2种模式各有优缺点,相比而言客户端Token方案是无状态的,服务端无效存储任何会话信息即可完成认证授权,更加方便,但是他的问题在于Token过长。这里我选择JWT客户端Token方案。下面用一个图来理解客户端Token

业界做登录授权的框架也有很多,比如:Shiro、SpringSecurity、Sa-Token都属于Java的权限框架。相比而言Sa-Token功能更为强大和简单,他主要解决:登录认证、权限认证、Session会话、单点登录、OAuth2.0 等一系列权限相关问题,相较于前两者,Sa-Token更适合于前后台分离架构,功能强大, 上手简单
Sa-Token:https://sa-token.cc/doc.html#/

1.2.Redis整合

第一步:导入redis的依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

第二步:配置redis

spring:
  redis:
    database: 2
    host: ${REDIS_HOST:redis.java.itsource.cn}
    port: ${REDIS_PORT:6380}
    password: 123456
    jedis:
      pool:
        max-wait: 2000ms
        min-idle: 10
        max-idle: 10

第三步:配置redis的序列化方式,自定义redisTemplate

@Resource
private RedisConnectionFactory factory;

//使用JSON进行序列化
@Bean
public RedisTemplate<String, Object> redisTemplate() {
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(factory);
    //JSON格式序列化
    GenericFastJsonRedisSerializer serializer = new GenericFastJsonRedisSerializer();
    //key的序列化
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    //value的序列化
    redisTemplate.setValueSerializer(serializer);
    //hash结构key的序列化
    redisTemplate.setHashKeySerializer(new StringRedisSerializer());
    //hash结构value的序列化
    redisTemplate.setHashValueSerializer(serializer);
    return redisTemplate;
}

第四步:使用redis,通过注入 RedisTemplate<String, Object> 即可。

1.3.SaToken+Redis认证

既然我们建立了统一认证中心,那么登录功能肯定交给UAA来完成,然后采用微信登录的方式

  1. 请求携带code请求UAA微信登录接口
  2. UAA执行微信登录后,加载登录对象和权限信息,使用Sa-Token登录,并设置好权限
  3. UAA执行完登录,返回Token到小程序端,小程序把Token存储起来
  4. 小程序在向其他资源发起请求的时,通过Header携带Token到资源服务器
  5. 资源服务器拿到Token进行校验,并对方法进行授权访问

image.png

第一步:导入依赖,下面是sa-token整合SpringBoot,基于Redis存储登录会话

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

第二步:配置Sa-Token


############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
  # token名称 (同时也是cookie名称)
  token-name: satoken
  # token有效期,单位s 默认30天, -1代表永不过期
  timeout: 2592000
  # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
  activity-timeout: -1
  # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
  is-concurrent: true
  # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
  is-share: false
  # token风格
  token-style: uuid
  # 是否输出操作日志
  is-log: false
  # jwt秘钥,密钥任意配置
  jwt-secret-key: itsource

第三步:执行登录逻辑这里是微信登录和satoken结合大致流程如下

  1. 用户采用微信登录,后台拿到微信OpenID
  2. 根据OpenId和用户类型查询Login对象,为空则提示注册
  3. 调用:StpUtil.login(login.getId()) 执行登录
  4. 封装用户信息如:头像,昵称等返回给前端
  5. 调用:StpUtil.getTokenInfo() 拿到Token,和用户信息一起,返回给前端
public LoginSuccess minProjectLogin(MinProjectLoginDTO dto) {
        //获取OpenId
        String openId = wechatTemplate.getOpenId(dto.getWechatCode());
        AssertUtil.isNotEmpty(openId,"微信ID不可为空");

        //根据OpenId查询用户
        Login login = super.getOne(new LambdaQueryWrapper<Login>().eq(Login::getOpenId, openId).eq(Login::getType,dto.getType()));
        AssertUtil.isNotNull(login,"请先注册");

        //执行登录
        StpUtil.login(login.getId());

        System.out.println(StpUtil.isLogin());

        //把手机号,头像等返回给前端展示
        LoginSuccess loginSuccess = LoginSuccess.builder()
                .phone(login.getPhone())
                .avatar(login.getAvatar())
                .nickName(login.getNickName()).build();

        //获取登录结果
        SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
        log.info("执行satoken 认证 {}" , tokenInfo);

    	//把Token返回给前端
        loginSuccess.setSatoken(tokenInfo.tokenValue);

        return loginSuccess;
    }
1.4.小程序存储token

第1步:小程序拿到token,调用 uni.setStorageSync 存储Token ,然后提示登录成功,跳转用户工作台workbench.vue 页面

uni.setStorageSync("satoken",data.satoken);
uni.setStorageSync("avatar",data.avatar);
uni.setStorageSync("nickName",data.nickName);
uni.setStorageSync("phone",data.phone);

uni.showToast({ icon:"success", title:"登录成功" })

//跳转工作台页面
setTimeout(()=>{
  uni.switchTab({
    url:"/pages/workbench/workbench"
  })
},2000)

第2步:请求头携带Token,在发往后台的请求的请求头中添加token

  • 因为我们把所有的请求都在main.js中进行了封装,所以只需要在main.js中的sendRequest方法中 uni.request 的请求头添加Token即可
//在发起ajax请求的地方,获取这个值,并塞到header里 
uni.request({
    url: '', 
    header: {
        "satoken": uni.getStorageSync('satoken')
    },
    success: (res) => {
        console.log(res.data);    
    }
});
1.4.配置放行

在所有功能中大部分功能可能都需要登录后才能访问,但是某些资源是不需要登录就能访问的,比如/register ; /login这种资源,所以我们需要对satoken的认证检查拦截器进行定制

  • 下面代码把登录路径 , 注册路径都放行了
  • SaInterceptor :satoken的token拦截器,以及权限校验拦截器
public class SaTokenConfigure implements WebMvcConfigurer {
    // 注册 Sa-Token 拦截器,打开注解式鉴权功能 
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
        registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/login/app/login",
                        "/driver/app/register",
                        "/customer/app/register"
                        );
    }
}

除此之外我们还可以通过注解进行控制是否放行

  • @SaIgnore : 无需登录就能访问
  • @SaCheckLogin :需要检查登录

注意:无需登录就能访问的资源一定要放行,否则会出现未登录的情况

2.服务器鉴权

2.1.SaToken注解授权

小程序携带satoken请求某一个微服务,微服务需要对token进行校验,以及对功能进行授权,即:某些安全性比较高的方法需要有指定的权限才可以访问。

在Satoken中我们可以使用注解鉴权,在controller的方法上贴注解即可,优雅的将鉴权与业务代码分离!

  • @SaCheckLogin: 登录校验 —— 只有登录之后才能进入该方法。
  • @SaCheckRole(“admin”): 角色校验 —— 必须具有指定角色标识才能进入该方法。
  • @SaCheckPermission(“user:add”): 权限校验 —— 必须具有指定权限才能进入该方法。
  • @SaCheckSafe: 二级认证校验 —— 必须二级认证之后才能进入该方法。
  • @SaCheckBasic: HttpBasic校验 —— 只有通过 Basic 认证后才能进入该方法。
  • @SaIgnore:忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。
  • @SaCheckDisable(“comment”):账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。

案例:用户需要有 customer:list 权限才可以访问该方法

@SaCheckPermission("customer:list")
public JSONResult list(@RequestBody PageQueryWrapper<Customer> query){ ... }

那么当用户访问某个功能,Satoken就会帮我们检查用户是否拥有访问权限了。

2.2.加载用户的权限和角色

那么问题来了,satoken怎么知道当前用户是否有访问权限。Satoken通过 StpInterface 接口,他提供了加载权限列表的方法,来为satoken加载用户的权限,而用户拥有什么权限始终是在数据库中关联的,所以我们可以这样做。

  1. 在用户登录成功后,从数据库查询出用户的权限列表
  2. 把用户的权限列表缓存到Redis中方便获取
  3. 编写类实现StpInterface接口,从redis中获取权限返回

第一步:在用户登录成功后,为用户加载权限列表,为了方便获取,我们直接存储到Redis中,修改登录逻辑代码:根据用户ID查询权限列表

//使用loginId拼接一个redis的key用来存储权限
String permissionKeyForRedis = String.format(Constants.Redis.KEY_PERMISSIONS, login.getId());

//把用户的权限缓存到Redis
List<String> permissions = permissionService.selectByLoginId(login.getId());
if(!permissions.isEmpty()){
    redisTemplate.opsForSet().add(permissionKeyForRedis,permissions.toArray());
}

第二步:创建一个权限加载接口实现类

  • 复写:getPermissionList方法
  • 拼接redis中权限的key,获取权限列表
/**
 * 自定义权限加载接口实现类
 */
@Slf4j
@Component
public class StpInterfaceImpl implements StpInterface {

    @Autowired  private RedisTemplate<String,Object> redisTemplate;
    
    /**
     * 返回一个账号所拥有的权限码集合 
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        //拼接redis的key
        String permissionKeyForRedis = String.format(Constants.Redis.KEY_PERMISSIONS, loginId);
        Set<Object> permissionsObj = redisTemplate.opsForSet().members(permissionKeyForRedis);
        //拿到权限列表
        List<String>  permissions = permissionsObj.stream().map(p -> p.toString() ).collect(Collectors.toList());
        log.info("从Redis 加载用户权限 {}",permissions);
        return new ArrayList<String>(permissions);
    }
    /**
     * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        return new ArrayList<String>();
    }
}

2.2.方法授权

第一步:在controller方法上通过注解授权 : @SaCheckPermission,并指定权限编号,注意:权限编号一定要和数据库中的权限编号对应

//需要权限 wallet:save 才可以访问
@SaCheckPermission("wallet:save")
@PostMapping
public JsonResult save(@RequestBody @Valid DriverWallet driverWallet){
    return JsonResult.success(driverWalletService.save(driverWallet));
}

第二步:配置JWT和权限拦截器
1.StpLogicJwtForMixin是对JWT支持
2.SaInterceptor是用来做权限检查的,他回去拦截方法上的权限注解,比如:@SaCheckPermission(“”)

@Configuration
public class SaTokenConfig implements WebMvcConfigurer {

    // Sa-Token 整合 jwt (Simple 简单模式)
    @Bean
    public StpLogic getStpLogicJwt() {
        return new StpLogicJwtForMixin();
    }

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

第三步:定义权限校验接口,返回用户权限。JWT的权限拦截器会调用SaInterceptor接口拿到用户的权限,我们需要解析Token,拿到Token中的权限返回给SaInterceptor。

  • 注意:从请求头获取Token的名字,必须和小程序指定的名字一样:satoken
  • 从Token中获取权限的名字必须和登录时放入Token中的权限名字一样:permissions
/**
 * 自定义权限验证接口扩展
 */
@Component
public class StpInterfaceImpl implements StpInterface {

    /**
     * 返回一个账号所拥有的权限码集合 
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        //拿到请求头中的Token
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        String satoken = requestAttributes.getRequest().getHeader("satoken");
        //拿到Token中的权限列表,返回
        List<String> permissions = (List<String>) StpUtil.getExtra(satoken, "permissions");
        return permissions;
    }

    /**
     * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        //拿到请求头中的Token
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        String satoken = requestAttributes.getRequest().getHeader("satoken");
        //拿到Token中的权限列表,返回
        List<String> roles = (List<String>) StpUtil.getExtra(satoken, "roles");
        return roles;
    }
}

3.微服务Token转发

3.1.服务调用Token失效

假设我们客户端的请求头戴着Token访问某微服务的时候可以正常访问,但是如果该请求设计到另外一个微服务的调用时,就会出现Token失效的情况。原因是Feign发起的是一个新的Http请求,该请求头中是不可能有Token的,所以被调用的服务(生产者)会校验Token失败
image.png
如上案例中,解决方案是:通过Feign的拦截器,拿到客户端发往消费者服务的请求对象中的请求头中的Token,然后添加到Feign的请求头中即可(消费者发往生产者服务的请求)。如下图:
image.png

3.2.编写拦截器

因为很多服务都需要用到Token转发,建议把拦截器编写到一个公共的模块中,然后需要导入Feign的依赖

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-core</artifactId>
</dependency>

然后编写拦截器,Feign提过了RequestInterceptor接口让我们去实现,通过复写其apply,该方法中提供了requestTemplate,通过他我们可以对feign的请求头进行设置,案例如下:

@Slf4j
@Component
public class TokenInterceptor implements RequestInterceptor {

    @Value("sa-token.token-name")
    private String tokenName;

    @Override
    public void apply(RequestTemplate requestTemplate) {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        String tokenValue = requestAttributes.getRequest().getHeader(tokenName);
        if(StringUtils.isNotEmpty(tokenValue)){
            requestTemplate.header(tokenName,tokenValue);
            log.warn("token转发 - {}" , tokenValue);
        }else{
            log.warn("token转发 - 请求头中并没有Token");
        }

    }
}

4.Satoken扩展

4.1.JWT模式【扩展】

使用JWT的好处是:token是无状态的,也就是服务端不用存储用户信息,而且把用户信息封装到Token中,交由前端自己去存储,这种方式的好处是减轻了服务器的压力,坏处是Token会变得很长

第一步:导入依赖,下面是sa-token整合SpringBoot,以及整合JWT的依赖(注意:不再导入redis的依赖)

 <!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>1.34.0</version>
</dependency>
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-jwt</artifactId>
    <version>1.34.0</version>
</dependency>

第二步:配置Sa-Token


############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
  # token名称 (同时也是cookie名称)
  token-name: satoken
  # token有效期,单位s 默认30天, -1代表永不过期
  timeout: 2592000
  # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
  activity-timeout: -1
  # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
  is-concurrent: true
  # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
  is-share: false
  # token风格
  token-style: uuid
  # 是否输出操作日志
  is-log: false
  # jwt秘钥,密钥任意配置
  jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk

第三步:配置StpLogicJwtForMixin ,该组件使得Sa-Token做登录的时候会生成JWT的Token,切进行秘钥加密


@Configuration
public class SaTokenConfig{

    // Sa-Token 整合 jwt (Mixin 混入模式)  redis 和 jwt一起使用
    @Bean
    public StpLogic getStpLogicJwt() {
        return new StpLogicJwtForMixin();
    }
}

第四步:加载用户权限,SaToken通过 StpInterface 接口来加载权限,我们需要复写该接口

  • getPermissionList :获取权限,根据loginId查询权限列表
  • getRoleList :获取角色,根据loginId查询角色列表
@Component
@Slf4j
public class StpInterfaceImpl implements StpInterface {

    @Autowired
    private IPermissionService permissionService;

    @Autowired
    private IRoleService roleService;

    /**
     * 返回一个账号所拥有的权限码集合 
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        List<String> permissions = permissionService.selectPermissionSnByLoginId(Long.valueOf(loginId.toString()));
        log.info("加载登录用户的权限 {}",permissions);
        return permissions;
    }

    /**
     * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        List<String> roles = roleService.selectRoleSnByLoginId((Long)loginId);
        log.info("加载登录用户的角色 {}",roles);
        return roles;
    }

}

第五步:执行 StpUtil.login 完成用户登录,同时把用户相关信息写入Token,包括权限

  • StpUtil.login(登录ID,扩展参数) : 通过扩展参数传入用户信息
  • StpUtil.getPermissionList 方法会触发StpInterface接口的getPermissionList方法加载权限
//执行微信登录
@Override
public Object wechatLogin(String code) {
    //1.获取OpenId
    String openId = wechatLoginTemplate.getOpenId(code);
    //2.查询Login
    Login login = selectByOpenId(openId);
    AssertUtil.isNotNull(login,"微信未注册");
    //3.走SaToken登录流程
    SaTokenInfo saTokenInfo = saTokenLogin(login);

    log.info("登录成功,打印用户TokenInfo {}",saTokenInfo);

    return saTokenInfo;
}

//执行SaToken登录逻辑
public SaTokenInfo saTokenLogin(Login login){
    StpUtil.login(login.getId(), SaLoginConfig
          	//根据需要封装扩展参数
            .setExtra("username", login.getUsername())
            .setExtra("type", login.getType())
            .setExtra("avatar", login.getAvatar())
            .setExtra("admin", login.getAdmin())
            .setExtra("nickName", login.getNickName())
            //加载权限
            .setExtra("permissions", StpUtil.getPermissionList(login.getId()))
            .setExtra("roles", StpUtil.getRoleList(login.getId()))
            .setExtra("openId", login.getOpenId()));
    return StpUtil.getTokenInfo();
}

返回值:SaTokenInfo就包含JWT的Token值。通过JsonResult把SaTokenInfo返回给小程序端

其他部分就和redis方式一样了

4.2.自动扫描权限

我们思考一个问题:数据库中的权限是哪儿来的?通常有2中方式,一是通过后台管理器去手动添加,第二种是自动扫描方法上的注解权限,自动插入到数据库。 下面我们来讲一下第二种方式如何实现

我们需要在程序启动的时候就去扫描controller方法上的注解

  • 这里我定义了一个类,注入了RequestMappingHandlerMapping :它里面包括了所有的controller
  • 然后拿到每个HandlerMethod ,也就是方法,然后拿到方法上的权限注解
@Slf4j
@Component
public class PermissionInitializer{
    
	@Autowired RequestMappingHandlerMapping handlerMapping;


    //把扫描到的权限保存到uaa
    public void createPermission2uaa(RequestMappingHandlerMapping handlerMapping,PermissionApi permissionApi){
        //拿到所有的handlerMethods
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
        Set<Map.Entry<RequestMappingInfo, HandlerMethod>> entries = handlerMethods.entrySet();

        List<CreatePermissionDTO> permissionDTOS = new ArrayList<>();
        for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : entries) {
            //拿到权限注解
            HandlerMethod method = entry.getValue();
        	//拿到权限注解
            SaCheckPermission checkPermission = method.getMethodAnnotation(SaCheckPermission.class);
            if(checkPermission != null){
                //权限表达式
                String[] value = checkPermission.value();
                //资源路径
                String url = entry.getKey().getPatternsCondition().getPatterns().stream().findFirst().get();

                //拿到功能名字
                Operation operation = method.getMethodAnnotation(Operation.class);

                String name = null;
                if(operation != null){
                    name = operation.summary();
                }else{
                    name = method.getMethod().getName();
                }
                //拼接一个唯一标识
                log.info("功能名字 {} - 资源路径 {} - 权限表达表达式 {}",name ,url , value[0]);
                CreatePermissionDTO permissionDTO= CreatePermissionDTO.builder().sn(value[0]).resource(url).name(name).build();
                permissionDTOS.add(permissionDTO);
            }
            
            //TODO 远程保存权限列表到uaa的权限表中 ,应该更换为 mq方式
            
        }
    }
}

5.网关统一鉴权

业界还有一个种比较流程的鉴权方式,就是通过网关统计鉴权,这种方式是在网关配置好需要放行的url,然后通过gateway的filter拦截请求,去检查请求是否需要做登录检查。如果需要做登录检查就从请求头中获取Token进行校验。

5.1.网关整合Satoken

首先需要把SaToken相关的依赖导入到网关,如果整合了jwt还需要导入JWT的依赖

<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
  <groupId>cn.dev33</groupId>
  <artifactId>sa-token-spring-boot-starter</artifactId>
  <version>1.37.0</version>
</dependency>
5.2.编写Globle Filter
//登录检查
@Component
@Slf4j
public class CheckLoginFilter implements GlobalFilter , Ordered {

    @Autowired
    private LoginProperties loginProperties;

    private AntPathMatcher pathMatcher = new AntPathMatcher();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取请求path
        String path = exchange.getRequest().getURI().getPath();
        if(isSkipCheckLogin(path)){
            log.info("请求Path : {} 跳过登录检查");
            return chain.filter(exchange);
        }

        //检查请求头
        List<String> headers = exchange.getRequest().getHeaders().get("satoken");
        if(headers == null || headers.size() == 0){
            log.info("请求头中没有satoken 提示去登录");
            return toLogin(exchange);
        }
        //校验Token
        String saToken = headers.get(0);
        StpUtil.setTokenValue(saToken);
        if(!StpUtil.isLogin()){
            log.info("请求头中没有satoken 提示去登录");
            return toLogin(exchange);
        }

        return chain.filter(exchange);
    }

    private boolean isSkipCheckLogin(String path){
        //匹配访问的path是否在放行的url当中
        for (String skipUrl : loginProperties.getUrls()) {
            if(pathMatcher.match(path,skipUrl))return true;

        }
        return false;
    }

    //返回提示登录错误信息
    private  Mono<Void> toLogin(ServerWebExchange exchange){
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");

        //错误信息封装
        JSONResult jsonResult = JSONResult.error("检查到未登录,请先登录");
        byte[] body = JSON.toJSONString(jsonResult).getBytes(StandardCharsets.UTF_8);
        //返回结果
        return response.writeWith(Mono.just(response.bufferFactory().wrap(body)));
    }

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

5.3.配置对象
@Component
@ConfigurationProperties("login.uncheck")
@Data
public class LoginProperties {
    private List<String> urls;
}

yaml配置

login:
  uncheck:
    urls:
      - /kds/order/register
      - /kds/order/login
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小Ti客栈

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

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

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

打赏作者

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

抵扣说明:

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

余额充值