前后端分离使用Sa-Token(超越官方文档)

imtoken官网地址 admin 2024-02-28 05:07 45 0

前后端分离使用Sa-Token(超越官方文档)

前言

tokenall中文官网

Sa-Token的官方文档链接在此。

tokenrank官网

事先声明,起一个这样的标题并不是我狂妄自大,而且Sa-Token的官方文档是我见过的少有的写的很好的官方文档(很多知名项目的官方文档可以说一言难尽)。但是,关于前后端分离的这部分,我感到Sa-Token官方文档实在有些过于简略。而我在学习Sa-Token的时候也遇到一个困境,就是关于前后端分离的文章在百度谷歌上基本找不来,大部分所谓原创都是对官方文档的复制粘贴,照搬。因此,在我掌握前后端分离对Sa-Token的使用后,我决定写这样一篇文章,方便后来者对Sa-Token的前后端分离的使用有一个参考。

tokenall官网

在前后端分离的权限验证,登录校验这条路上。我自学习编程以来,先后经历过传统token,jwt,单点登录,shiro, 等。这些技术,有的需要你书写大篇幅的拦截器,过滤器,资源类控制逻辑。可谓苦不堪言。而且官方文档一塌糊涂,使用步骤极度繁琐。最终,我找到了一个目前我认为最完美的技术:Sa-Token。当今国内企业,大部分项目都是前后端分离的,Sa-Token美中不足的地方在于,它的官方文档对前后端不分离的部分讲述的很清楚,对我们真正要经常用到前后端分离的使用讲述的却过于简略。这篇文档,我将从头到尾讲解如何使用Sa-Token做前后端分离的 Boot项目。

请注意,大部分代码需要你结合实际,甚至需要你稍加改动才能用。我会默认你已经掌握了jwttoken 权限管理·(中国)官方网站,,等知识。我将在每一段代码内写上注释,而且讲解我的写作逻辑,但这不意味着你可以不思考就能掌握这项技术。我已经将我的代码开源放在,你也可以拉取的代码在本地运行,更方便你理解这项技术。的链接在此。

步骤 1.创建一个 boot项目,引入如下两个依赖。


        <dependency>
            <groupId>cn.dev33groupId>
            <artifactId>sa-token-spring-boot-starterartifactId>
            <version>1.35.0.RCversion>
        dependency>
        
        <dependency>
            <groupId>cn.dev33groupId>
            <artifactId>sa-token-redis-jacksonartifactId>
            <version>1.35.0.RCversion>
        dependency>

讲解:第一个是sa-token的自动装配,无需多言。第二个,则会将我们的token自动管理在redis中,而不是在或者,这样就达到了最完美的前后端分离状态,无论你是重启前端还是后端,销毁还是,都不影响项目的token。如果你对单点登录,分布式登录有了解,你就会深深明白这么做的好处。在跨域的单点登录里,是失效的,前后端分离,分布式的项目里,一个请求根本不知道到来的请求在跟哪个打交道。我的代码只求最彻底,最激进,最完善的前后端分离。

2.配置你的.yml。

这里需要注意,在 boot data redis的2版本以后,已经弃用 jedis 改用 ,而官网仍在配置jedis连接池,这里要用连接池。

############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
  # token 名称(同时也是 cookie 名称)
  token-name: token
  # token 有效期(单位:秒) 默认30天,-1 代表永久有效
  timeout: -1
  # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
  active-timeout: -1
  # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
  is-concurrent: false
  # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
  token-style: simple-uuid
  # 是否输出操作日志
  is-log: true
  # 是否尝试从 cookie 里读取 Token,此值为 false 后,StpUtil.login(id) 登录时也不会再往前端注入Cookie
  isReadCookie: false
spring:
  # Redis配置
  redis:
    host: localhost
    port: 6379
    # 根据自己设置的密码决定
    password: 你的redis密码
    # 操作0号数据库,默认有16个数据库
    database: 0
    lettuce:
      pool:
        # 最大连接数
        max-active: 500
        # 连接池最大阻塞等待时间
        max-wait: 1000ms
        # 连接池中的最大空闲连接
        max-idle: 100
        # 连接池中的最小空闲连接
        min-idle: 0

讲解:如果你用过jwt,那么token-name: token就可以达到类似于jwt的效果,在你的加上token:token值,就可以达到登录的校验的效果,像jwt一样,完全抛弃了和,而jwt过于冗余,token-style: -uuid将保证token的精简,结合token的唯一性配合redis的使用,jwt携带信息的优点也将不复存在,因为redis可以存储一切。而redis配置你不必担心它影响 boot自带的redis自动装配依赖,结合实际你会发现sa-token就是封装的redis自动装配依赖。当你创造一个token,他也会自动写入redis,不需要你多写任何代码。所有的token,都将会自动管理。

3.注解鉴权

加sa-token拦截器。

import cn.dev33.satoken.interceptor.SaInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
 * @author Peter Cheung
 * 2023/7/19 13:52
 */
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
    // 注册 Sa-Token 拦截器,打开注解式鉴权功能
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册 Sa-Token 拦截器,打开注解式鉴权功能
        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/api/**");
    }
}

讲解:/api最好写上,然后把你的接口都带上/apiimToken钱包,因为/**什么都扫描,效率低下。如果你只做登录校验,这么点代码就已经足够。

如果你还想做权限,角色的校验,加上下面的代码。

import cn.dev33.satoken.stp.StpInterface;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
 * @author Peter Cheung
 * 2023/7/19 16:55
 */
@Service
public class StpInterfaceImpl implements StpInterface {
    /**
     * 返回一个账号所拥有的权限码集合
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        // 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询权限
        List<String> list = new ArrayList<String>();
        list.add("101");
        list.add("user.add");
        list.add("user.update");
        list.add("user.get");
        // list.add("user.delete");
        list.add("art.*");
        return list;
    }
    /**
     * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        // 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询角色
        List<String> list = new ArrayList<String>();
        if ("root".equals(loginId)) {
            list.add("admin");
            list.add("super-admin");
        }
        return list;
    }
}

讲解:具体用法我不再赘述,如果你学过shiro,那你理解这段代码轻而易举,无非是把一个角色数组或者权限数据,跟账号做了一个绑定。如果你真的想理解掌握,然后超越,去改造这段代码,需要你不得不去学习一下shiro的五表思想去分配角色和权限资源。才能让你真正在实际工作里拿捏对权限角色的分配管理。

4.加入。

这和sa-token无关,只是为了让你的redis能支持中文存储。

import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
 * Redis配置类,更换默认序列化器
 *
 * @author Peter Cheung
 * @since 2023-07-28 13:51:55
 */
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    /**
     * 创建 RedisTemplate Bean,用于操作 Redis 数据库。
     *
     * @param connectionFactory Redis 连接工厂
     * @return RedisTemplate 实例
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // 设置 RedisTemplate 的连接工厂
        redisTemplate.setConnectionFactory(connectionFactory);
        // 创建 StringRedisSerializer 实例,用于序列化和反序列化 Redis 的键和哈希键
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // 创建 GenericJackson2JsonRedisSerializer 实例,用于序列化和反序列化 Redis 的值和哈希值
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        // 设置默认序列化器为 StringRedisSerializer
        redisTemplate.setDefaultSerializer(stringRedisSerializer);
        // 设置 RedisTemplate 的键序列化器为 StringRedisSerializer
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // 设置 RedisTemplate 的哈希键序列化器为 StringRedisSerializer
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // 设置 RedisTemplate 的值序列化器为 GenericJackson2JsonRedisSerializer
        redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
        // 设置 RedisTemplate 的哈希值序列化器为 GenericJackson2JsonRedisSerializer
        redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
        return redisTemplate;
    }
}

5.异常统一处理

如果你的Java知识不足以理解这些代码,这些也可以不要,下面的代码只是为了让你错误处理更加优雅。

import javax.validation.ValidationException;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import static com.learn.learnsatoken.config.constant.Constant.PACKAGE_NAME;
/**
 * 全局异常统一处理
 *
 * @author Peter Cheung
 * @since 2023-07-19 11:37:24
 */
@Slf4j
@ControllerAdvice
@ResponseBody
public class AllExceptionHandle {
    /**
     * 登录权限校验
     */
    @ExceptionHandler({NotLoginException.class, NotRoleException.class})
    public ResponseEntity<R> unauthorized(Exception e) {
        return R.deal(R.unauthorized().data(e(e)));
    }
    /**
     * 校验传参
     */
    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<R> handleBadRequest(Exception e) {
        return R.deal(R.badRequest().data(e(e)));
    }
    /**
     * 全局异常处理
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<R> exception(Exception e) {
        return R.deal(R.exp().data(e(e)));
    }
    /**
     * 异常信息处理主体方法
     *
     * @param e 异常对象
     * @return 异常解析信息
     */
    private String e(Exception e) {
        ByteArrayOutputStream printStackTrace = new ByteArrayOutputStream();
        e.printStackTrace(new PrintStream(printStackTrace));
        log.error(String.valueOf(printStackTrace));
        //错误信息
        StringBuilder errorMessage = new StringBuilder();
        errorMessage.append(e);
        if (StringUtils.isBlank(e.getMessage())) {
            //处理
            log.error(String.valueOf(errorMessage));
            return String.valueOf(errorMessage);
        }
        StackTraceElement[] stackTrace = e.getStackTrace();
        for (StackTraceElement stackTraceElement : stackTrace) {
            String className = stackTraceElement.getClassName();
            if (className.startsWith(PACKAGE_NAME)) {
                String errorName = ";" + stackTraceElement.getClassName();
                errorMessage.append(errorName);
                String errorLineNumber = ":" + stackTraceElement.getLineNumber();
                errorMessage.append(errorLineNumber);
                //处理
                log.error(String.valueOf(errorMessage));
                return String.valueOf(errorMessage);
            }
        }
        //处理
        log.error(String.valueOf(errorMessage));
        return String.valueOf(errorMessage);
    }
}