Sa-Token:轻量级 Java 权限认证框架(快速上手)

Sa-Token框架主要解决登录认证、权限认证、单点登录等一系列权限相关问题。

官网: https://sa-token.cc/doc.html#/

本次演示环境为SpringBoot3.2.5、mybatis-plus3.5.6,权限模型采用RBAC模型,简单来说就是什么用户有什么角色,对应又有哪些权限。

数据库表

建表语句

create table user
(
    id       int auto_increment
        primary key,
    username varchar(20)  null comment '用户名',
    password varchar(255) null comment '密码'
);
create table rule
(
    id        int auto_increment
        primary key,
    rule_name varchar(20) null comment '角色名'
);
create table power
(
    id        int auto_increment
        primary key,
    authority varchar(255) null comment '权限'
);
# 用户和角色关联表
create table user_rule
(
    id      int auto_increment
        primary key,
    user_id int null comment '用户id',
    rule_id int null comment '角色id'
);
# 角色和权限关联表
create table rule_power
(
    id       int auto_increment
        primary key,
    rule_id  int null comment '角色id',
    power_id int null comment '权限id'
);

表数据

用户表
在这里插入图片描述

角色表
在这里插入图片描述

权限表
在这里插入图片描述
另外两个关系表就不展示了

依赖

<properties>
    <java.version>17</java.version>
    <mybatis.plus.version>3.5.6</mybatis.plus.version>
    <sa-token.version>1.34.0</sa-token.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <!-- mybatis-plus代码生成器 -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-generator</artifactId>
        <version>${mybatis.plus.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.velocity</groupId>
        <artifactId>velocity-engine-core</artifactId>
        <version>2.3</version>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>${mybatis.plus.version}</version>
        <exclusions>
            <exclusion>
                <artifactId>mybatis-spring</artifactId>
                <groupId>org.mybatis</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>3.0.3</version>
    </dependency>
    <dependency>
        <groupId>cn.dev33</groupId>
        <artifactId>sa-token-spring-boot3-starter</artifactId>
        <version>${sa-token.version}</version>
    </dependency>
    <!-- Sa-Token 整合 Redis (使用 jackson 序列化方式)-->
    <dependency>
        <groupId>cn.dev33</groupId>
        <artifactId>sa-token-dao-redis-jackson</artifactId>
        <version>${sa-token.version}</version>
    </dependency>
    <!-- 提供Redis连接池 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
    <!-- Sa-Token插件:权限缓存与业务缓存分离 -->
    <dependency>
        <groupId>cn.dev33</groupId>
        <artifactId>sa-token-alone-redis</artifactId>
        <version>${sa-token.version}</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.80</version>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.7.12</version>
    </dependency>
</dependencies>

注意版本sa-token相关的版本一致,也不要忘了commons-pool2这个依赖,不然就会报各种奇怪的错误。

常见问题报错可以在官网进行查看
快速访问: https://sa-token.cc/doc.html#/more/common-questions
在这里插入图片描述

配置文件

spring:
  application:
    name: sa-token-demo
  banner:
    location: classpath:banner.txt
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/your_scheme?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: 111111
  data:
    redis:
      # Redis数据库索引(默认为0)
      database: 0
      host: 127.0.0.1
      port: 6379
      password: redis001
      timeout: 10s
      lettuce:
        pool:
          # 最大连接数
          max-active: 100
          # 最大阻塞等待时间(使用负值表示没有限制)
          max-wait: -1ms
          # 最大空闲连接
          max-idle: 10
          # 最小空闲连接
          min-idle: 0

sa-token:
  # token 名称(同时也是 cookie 名称),发请求的时候请求头名称,如 satoken:Bearer b12e2f2157034ac0aa32a7cc4c97b3e6
  token-name: satoken
  # token前缀
  token-prefix: Bearer
  # token 有效期(单位:秒) 默认30天,-1 代表永久有效
  timeout: 2592000
  # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
  activity-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: simple-uuid
  # 是否输出操作日志
  is-log: true

mybatis-plus:
  configuration:
    #开启sql日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

注意token-name上面写的注释

代码

角色、权限的查询

写一个类继承StpInterface这个接口,重写getRoleList、getPermissionList这两个方法,后续权限校验都会走这两个方法

@Slf4j
@Component
public class UserAuthority implements StpInterface {

    @Autowired
    private UserRuleMapper userRuleMapper;

    /**
     * 获取角色列表
     *
     * @param o 用户id
     * @param s
     * @return List
     */
    @Override
    public List<String> getRoleList(Object o, String s) {
        List<Rule> rules = userRuleMapper.queryRuleByUserId(o);
        List<String> ruleNames = rules.stream().map(Rule::getRuleName).toList();

        log.info(">>>getRoleList");
        log.info("用户:" + o);
        log.info("角色:" + Arrays.toString(ruleNames.toArray()));
        return ruleNames;
    }

    /**
     * 获取权限列表
     *
     * @param o 用户id
     * @param s
     * @return List
     */
    @Override
    public List<String> getPermissionList(Object o, String s) {
        List<Power> powers = userRuleMapper.queryPowerByUserId(o);
        List<String> permissions = powers.stream().map(Power::getAuthority).toList();

        log.info(">>>getPermissionList");
        log.info("用户:" + o);
        log.info("权限:" + Arrays.toString(permissions.toArray()));
        return permissions;
    }
}

mapper

@Mapper
public interface UserRuleMapper extends BaseMapper<UserRule> {
    /**
     * 查询某个用户所拥有的角色
     *
     * @param userId 用户id
     * @return list
     */
    List<Rule> queryRuleByUserId(@Param("id") Object userId);

    /**
     * 查询某个用户所拥有的所有权限
     *
     * @param userId 用户id
     * @return list
     */
    List<Power> queryPowerByUserId(@Param("id") Object userId);
}

mapper xml

<select id="queryRuleByUserId" resultType="org.example.satokendemo.entity.Rule" parameterType="string">
    select rule.*
    from rule
             join user_rule on rule.id = user_rule.rule_id
    where user_rule.user_id = #{id};
</select>

<select id="queryPowerByUserId" resultType="org.example.satokendemo.entity.Power" parameterType="string">
    select power.*
    from user_rule
             join rule_power on user_rule.rule_id = rule_power.rule_id
             join power on rule_power.power_id = power.id
    where user_rule.user_id = #{id};
</select>

拦截器

@SpringBootConfiguration
@EnableWebMvc
public class RequestInterceptor implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        SaInterceptor saInterceptor = new SaInterceptor(
                handler -> {
                    // 检验登录
                    StpUtil.checkLogin();
                }
        );
        registry.addInterceptor(saInterceptor)
                // 拦截所有接口
                .addPathPatterns("/**")
                //不拦截注册、登录接口
                .excludePathPatterns("/user/register", "/user/doLogin");
    }
}

全局异常处理

@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 未登录异常
     *
     * @param e NotLoginException
     * @return Result
     */
    @ExceptionHandler(NotLoginException.class)
    public Result handlerNotLoginException(NotLoginException e) {
        e.printStackTrace();
        String message = "";
        if (e.getType().equals(NotLoginException.NOT_TOKEN)) {
            message = "未能读取到有效 token";
        } else if (e.getType().equals(NotLoginException.INVALID_TOKEN)) {
            message = "token 无效";
        } else if (e.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
            message = "token 已过期";
        } else if (e.getType().equals(NotLoginException.BE_REPLACED)) {
            message = "token 已被顶下线";
        } else if (e.getType().equals(NotLoginException.KICK_OUT)) {
            message = "token 已被踢下线";
        } else {
            message = "当前会话未登录";
        }
        return Result.error(message);
    }

    /**
     * 无角色异常
     *
     * @param e NotRoleException
     * @return Result
     */
    @ExceptionHandler(NotRoleException.class)
    public Result handlerNotRoleException(Exception e) {
        e.printStackTrace();
        return Result.error("当前账号角色不符合!");
    }

    /**
     * 无权限异常
     *
     * @param e NotPermissionException
     * @return Result
     */
    @ExceptionHandler(NotPermissionException.class)
    public Result handlerNotPermissionException(Exception e) {
        e.printStackTrace();
        return Result.error("当前角色无权限!");
    }

    /**
     * 系统异常
     *
     * @param e Exception
     * @return Result
     */
    @ExceptionHandler(Exception.class)
    public Result handlerException(Exception e) {
        e.printStackTrace();
        return Result.error("系统异常");
    }

}

UserController

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;
    @Autowired
    private UserMapper userMapper;

    @RequestMapping(value = "register", method = RequestMethod.POST)
    public Result register(String username, String password) {
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUsername, username);
        User user = userService.getOne(queryWrapper);
        if (ObjectUtil.isNotNull(user)) {
            return Result.error("用户名已存在!");
        }
        User newUser = new User();
        newUser.setUsername(username);
        newUser.setPassword(SaSecureUtil.aesEncrypt(Constant.ENCODER_KEY, password));
        userMapper.insert(newUser);
        return Result.ok("注册成功!");
    }

    @RequestMapping("doLogin")
    public Result doLogin(String username, String password) {
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUsername, username);
        queryWrapper.eq(User::getPassword, SaSecureUtil.aesEncrypt(Constant.ENCODER_KEY, password));
        User user = userService.getOne(queryWrapper);
        if (ObjectUtil.isNotNull(user)) {
            StpUtil.login(user.getId());
            SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
            System.out.println(">>>tokenInfo = " + tokenInfo);
            return Result.ok("登录成功", tokenInfo);
        }
        return Result.error("用户名或者密码错误!");
    }

    /**
     * 查看当前账号是否登录
     */
    @RequestMapping("isLogin")
    public Result isLogin() {
        return StpUtil.isLogin() ? Result.ok("已登录") : Result.error("是否登录:" + StpUtil.isLogin());
    }

    /**
     * 查询 Token 信息
     */
    @RequestMapping("tokenInfo")
    public Result tokenInfo() {
        return Result.ok(StpUtil.getTokenInfo());
    }

    /**
     * 注销
     */
    @RequestMapping("logout")
    public Result logout() {
        StpUtil.logout();
        return Result.ok();
    }

}

TestController

@RestController
@RequestMapping("/test")
public class TestController {

    /**
     * 加@SaIgnore可以不检查权限
     */
    @SaIgnore
    @RequestMapping("/getList")
    public Result getList() {
        return Result.ok("无需登录接口,查询成功");
    }

    /**
     * 登录后可访问
     */
    @SaCheckLogin
    @RequestMapping("/select")
    public Result select() {
        return Result.ok("查询成功");
    }

    /**
     * 检查角色,有admin角色可访问
     */
    @RequestMapping("/checkRoleSelect")
    @SaCheckRole("admin")
    public Result checkRoleSelect() {
        return Result.ok("查询成功");
    }

    /**
     * 有admin、superAdmin其中一个角色可访问
     */
    @RequestMapping("/delete")
    @SaCheckRole(value = {"admin", "superAdmin"}, mode = SaMode.OR)
    public Result delete() {
        return Result.ok("删除成功");
    }

    /**
     * 检查权限,有其中一个权限就可以访问
     */
    @RequestMapping("/add")
    @SaCheckPermission(value = {"add", "update", "delete"}, mode = SaMode.OR)
    public Result add() {
        return Result.ok("添加成功");
    }

    /**
     * 有 更新/删除 权限   或者admin角色时可以访问
     */
    @RequestMapping("/update")
    @SaCheckPermission(value = {"update", "delete"}, orRole = "admin")
    public Result update() {
        return Result.ok("更新成功");
    }

}

测试

未登录时访问,访问失败
在这里插入图片描述
使用saToken-normal用户登录,该用户角色为normal
在这里插入图片描述
访问删除接口,normal只有查询权限,访问失败(注意请求头中的参数值,Bearer和tokenValue直接之间有个空格)
在这里插入图片描述
换个admin角色登录
在这里插入图片描述
接口访问成功
在这里插入图片描述
其他情况就不一一测试了,可以看TestController中方法的注释,也可以看官网的解释
链接: https://sa-token.cc/doc.html#/use/at-check?id=%e6%b3%a8%e8%a7%a3%e9%89%b4%e6%9d%83-1

计科和软工做毕设的同学狂喜啊
Alt
该框架的更多功能请查看官网~

  • 16
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

彩虹、

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

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

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

打赏作者

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

抵扣说明:

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

余额充值