个人技术分享

说明:Sa-Token是一个轻量级java权限认证框架(官方语),所谓权限认证框架,就是登录框架,像Shiro、Spring Security。本文介绍Sa-Token框架的入门使用,基于Spring Boot环境。

准备工作

首先,创建一个简单的Spring Boot项目,pom.xml文件如下:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!--spring boot-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.12</version>
        <relativePath/>
    </parent>

    <groupId>com.hezy</groupId>
    <artifactId>satoken_demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>Archetype - satoken_demo</name>
    <url>http://maven.apache.org</url>

    <dependencies>
        <!--web依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!--数据库连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.8</version>
        </dependency>

        <!--mybatis依赖-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

        <!--数据库驱动-->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
</project>

application.yml配置文件如下

server:
  port: 8080

# 1.数据库配置
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://IP地址:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: admin
    password: MySQL@3306

# 2.mybatis配置
mybatis:
  configuration:
    # 显示SQL日志配置
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # 驼峰命名配置
    map-underscore-to-camel-case: true
  # 设置mapper.xml文件所在的路径
  mapper-locations: classpath:mapper/*.xml

在编码之前,需要介绍一下RBAC(Role-Based Access Control,基于角色的访问控制模型),简单来说,是用户 ⇒ 角色 ⇒ 权限,一个用户有多个角色,一个角色有多个权限。权限对应的是对系统资源的控制,通过角色深度绑定,再将角色分配给用户,非常灵活。对应的数据库设计,就是以下五张表:

  • i_users(用户表);

  • i_roles(角色表);

  • i_permissions(权限表);

  • i_user_roles_mapping(用户角色表);

  • i_role_permissions_mapping(角色权限表);

创建以上五张表,如下:

(i_users,用户表)

在这里插入图片描述

(i_roles,角色表)

在这里插入图片描述

(i_permissions,权限表)

在这里插入图片描述

(i_user_roles_mapping,用户角色表)

以下表示:

  • zhangsan(id为1)的角色是admin、admin-super,拥有这两个角色的权限;

  • lisi(id为2)的角色是admin,拥有admin这个角色的权限;

在这里插入图片描述

(i_role_permissions_mapping,角色权限表)

以下表示:

  • admin角色拥有user.add、user.update、user.delete、user.select权限;

  • admin-super角色拥有user.*权限;

  • user角色拥有user.select权限;

在这里插入图片描述

下面加入Sa-Token依赖和配置,如下:

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

配置文件

############## 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

启动项目,控制台可以看到Sa-Token相关信息

在这里插入图片描述

基础使用

使用前,先创建一个全局异常处理器,用来显示Sa-Token返回的错误信息;

import cn.dev33.satoken.util.SaResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {
    // 全局异常拦截 
    @ExceptionHandler
    public SaResult handlerException(Exception e) {
        e.printStackTrace(); 
        return SaResult.error(e.getMessage());
    }
}

(1)登录认证

Sa-Token登录认证功能里,只需调用API即可完成登录,发放Token、校验Token都不需要手动编码。如下:

  • StpUtil.login():登录,设置当前用户id;

  • StpUtil.isLogin():判断当前用户是否已登录;

  • StpUtil.getTokenInfo():返回当前用户的Token信息;

  • StpUtil.logout():退出登录;

  • StpUtil.getLoginId():获取当前会话账号id, 如果未登录,则抛出异常:NotLoginException

  • StpUtil.getLoginIdDefaultNull():获取当前会话账号id, 如果未登录,则返回 null;

import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import com.hezy.mapper.UserMapper;
import com.hezy.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 登录测试 
 */
@RestController
@RequestMapping("/acc/")
public class LoginController {
    
    @Autowired
    private UserMapper userMapper;

    /**
     * 登录
     * @param username
     * @param password
     * @return
     */
    @RequestMapping("doLogin")
    public SaResult doLogin(String username, String password) {
        // 查询用户
        User userDO = userMapper.getUserByUsername(username);

        // 密码校验
        if(userDO.getPassword().equals(password)) {
            StpUtil.login(userDO.getId());
            return SaResult.ok("登录成功");
        }
        return SaResult.error("登录失败");
    }

    /**
     * 判断是否登录
     * @return
     */
    @RequestMapping("isLogin")
    public SaResult isLogin() {
        return SaResult.ok("是否登录:" + StpUtil.isLogin());
    }

    /**
     * 获取当前登录Token信息
     * @return
     */
    @RequestMapping("tokenInfo")
    public SaResult tokenInfo() {
        return SaResult.data(StpUtil.getTokenInfo());
    }

    /**
     * 退出登录
     * @return
     */
    @RequestMapping("logout")
    public SaResult logout() {
        StpUtil.logout();
        return SaResult.ok();
    }
}

重启项目,用Apifox测试一下;

(登录)

在这里插入图片描述


(当前用户是否已登录)

在这里插入图片描述

可以看到,我什么参数都没有传,但是Sa-Token判断已登录,这是因为Apifox有个Cookie管理的功能,会将前面登录返回Token保存下来,后面发的请求自动携带。

在这里插入图片描述


(获取当前用户的Token信息,包括了Token的名称、有限期等)

在这里插入图片描述


(退出登录)

在这里插入图片描述

退出登录后,Apifox里存的Cookie被清空,再访问下判断当前用户是否登录的接口,如下:

在这里插入图片描述


(控制台信息)

在这里插入图片描述

看下来,是不是非常简单?

(2)权限认证

前面我们创建了一个RBAC模型,可以通过下面的方式来使用。首先,实现Sa-Token的StpInterface接口 ;

import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpUtil;
import com.hezy.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;

/**
 * 自定义权限加载接口实现类
 */
@Component
public class StpInterfaceImpl implements StpInterface {

    @Autowired
    private UserMapper userMapper;

    /**
     * 返回一个账号所拥有的权限码集合 
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        return userMapper.getPermissionListByLoginId(StpUtil.getLoginIdAsInt());
    }

    /**
     * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        return userMapper.getRoleListByLoginId(StpUtil.getLoginIdAsInt());
    }
}

对应的Mapper方法

    /**
     * 根据用户id查询用户的角色
     */
    List<String> getRoleListByLoginId(int loginIdAsInt);

    /**
     * 根据用户id查询该用户的权限
     */
    List<String> getPermissionListByLoginId(int loginIdAsInt);

Mapper.xml

    <select id="getPermissionListByLoginId" resultType="java.lang.String" parameterType="int">
        select t3.name
        from i_role_permissions_mapping t1
                 inner join i_user_roles_mapping t2 on t1.role_name = t2.role_name
                 inner join i_permissions t3 on t3.id = t1.permission_name
        where t2.user_name = #{loginIdAsInt}
    </select>

    <select id="getRoleListByLoginId" resultType="java.lang.String" parameterType="int">
        select t2.name
        from i_user_roles_mapping t1
                 inner join i_roles t2 on t2.id = t1.role_name
        where t1.user_name = #{loginIdAsInt}
    </select>

(查询当前用户所拥有的角色,及权限)

package com.hezy.controller;

import java.util.List;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;

/**
 * 权限认证
 */
@RestController
@RequestMapping("/jur/")
public class JurAuthController {

	/**
	 * 查询当前用户的角色、权限信息
	 */
	@RequestMapping("getPermission")
	public SaResult getPermission() {
		// 查询权限信息 ,如果当前会话未登录,会返回一个空集合 
		List<String> permissionList = StpUtil.getPermissionList();
		System.out.println("当前登录账号拥有的所有权限:" + permissionList);
		
		// 查询角色信息 ,如果当前会话未登录,会返回一个空集合 
		List<String> roleList = StpUtil.getRoleList();
		System.out.println("当前登录账号拥有的所有角色:" + roleList);
		
		// 返回给前端 
		return SaResult.ok()
				.set("roleList", roleList)
				.set("permissionList", permissionList);
	}
}

在这里插入图片描述


这样,就可以使用Sa-Token对应的API,来返回当前用户是否拥有某个权限、某个角色,相关API如下:

  • StpUtil.getPermissionList():获取当前用户的所有权限列表;

  • StpUtil.getRoleList():获取当前用户的所有角色列表;


  • StpUtil.hasPermission(“user.add”):判断当前用户是否有“user.add”权限;

  • StpUtil.hasPermissionAnd(“user.add”, “user.delete”, “user.select”):判断当前用户是否有这三个权限,需全部满足;

  • StpUtil.hasPermissionOr(“user.add”, “user.delete”, “user.select”):判断当前用户是否有这三个其中一个权限;

  • StpUtil.checkPermission(“user.add”):校验当前用户是否有“user.add”权限;

  • StpUtil.checkPermissionAnd(“user.add”, “user.delete”, “user.select”);:校验当前用户是否有这三个权限,需全部满足;

  • StpUtil.checkPermissionOr(“user.add”, “user.delete”, “user.select”):校验当前用户是否有这三个其中一个权限;


  • StpUtil.hasRole(“admin”):判断当前用户是否有“admin”角色;

  • StpUtil.hasRoleAnd(“admin”, “ceo”, “cfo”):判断当前用户是否有这三个角色,需全部满足;

  • StpUtil.hasRoleOr(“admin”, “ceo”, “cfo”):判断当前用户是否有这三个其中一个角色;

  • StpUtil.checkRole(“admin”):校验当前用户是否有“admin”角色;

  • StpUtil.checkRoleAnd(“admin”, “ceo”, “cfo”):校验当前用户是否有这三个角色,需全部满足;

  • StpUtil.checkRoleOr(“admin”, “ceo”, “cfo”):校验当前用户是否有这三个其中一个角色;


判断会返回true或false,校验当不满足时,会抛出NotPermissionException(没有权限)或 NotRoleException(没有角色)的异常。另外,Sa-Token还支持用通配符来表示,如下,

在这里插入图片描述

我们给admin-super角色分配了一个user.*的权限,当当前用户拥有admin-super角色时,等于拥有所有的user操作权限,以下API均能通过;

StpUtil.hasPermission("user.add");

StpUtil.hasPermissionAnd("user.add", "user.delete", "user.select");

StpUtil.hasPermissionOr("user.add", "user.delete", "user.select");

StpUtil.checkPermission("user.add");

StpUtil.checkPermissionAnd("user.add", "user.delete", "user.select");

StpUtil.checkPermissionOr("user.add", "user.delete", "user.select");

角色校验也能使用通配符,另外通配符还能放在中间,如:system.*.user,表示任意以system开头,以user结尾的权限都能通过校验;

(3)踢人下线

踢人下线相关API如下,可指定需要下线的loginId、token值,或使用loginId+登录类型;

StpUtil.kickout(10001);                    // 将指定账号踢下线 
StpUtil.kickout(10001, "PC");              // 将指定账号指定端踢下线
StpUtil.kickoutByTokenValue("token");      // 将指定 Token 踢下线

测试一下

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;

/**
 * Sa-Token 权限认证示例 
 */
@RestController
@RequestMapping("/kickout/")
public class KickoutController {

	// 将指定账号踢下线
	@RequestMapping("kickout")
	public SaResult kickout(long userId) {
		// 踢人下线不会清除Token信息,而是将其打上特定标记,再次访问会提示:Token已被踢下线。
		StpUtil.kickout(userId);
		
		// 返回
		return SaResult.ok();
	}
}

(踢人下线)

在这里插入图片描述

(再次访问查询该用户角色、权限接口,提示被踢下线)

在这里插入图片描述

(4)注解鉴权

Sa-Token也能像Spring Security那样,使用注解来对接口进行权限校验。需创建一个Sa-Token拦截器,如下:

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

然后,可以在需要校验的接口上打上相应的注解,如下:

  • @SaCheckLogin:需要登录才能进入;

  • @SaCheckPermission(“user.add”):需要有“user.add”权限才能进入;

  • @SaCheckPermission(value = {“user.add”, “user.delete”, “user.update”}, mode = SaMode.AND):需要满足这三个权限才能进入;

  • @SaCheckPermission(value = {“user.add”, “user.delete”, “user.update”}, mode = SaMode.OR):需要满足其中一个权限才能进入;

  • @SaCheckRole(“super-admin”):需要拥有“super-admin”角色才能进入;

  • @SaCheckPermission(value = “user.add”, orRole = “admin”):需要具备 "user.add"权限 或者 "admin"角色才能进入

  • @SaIgnore:忽略校验,可直进入;

另外,可使用组合校验,官网的开发文档中提供了一个样例,如下:

// 在 `@SaCheckOr` 中可以指定多个注解,只要当前会话满足其中一个注解即可通过验证,进入方法。
@SaCheckOr(
        login = @SaCheckLogin,
        role = @SaCheckRole("admin"),
        permission = @SaCheckPermission("user.add"),
        safe = @SaCheckSafe("update-password"),
        basic = @SaCheckBasic(account = "sa:123456"),
        disable = @SaCheckDisable("submit-orders")
)

(5)路由拦截鉴权

当我们需要根据接口地址来进行拦截时,可在前面的Sa-Token拦截器中设置,如下:

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
    // 注册拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
        registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
                .addPathPatterns("/**")
                .excludePathPatterns("/user/doLogin"); 
    }
}

表示,排除地址为“/user/doLogin”接口,其他接口都拦截。另外还可在这里写丰富的拦截逻辑,如下:

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册 Sa-Token 拦截器,定义详细认证规则 
        registry.addInterceptor(new SaInterceptor(handler -> {
            // 指定一条 match 规则
            SaRouter
                .match("/**")    // 拦截的 path 列表,可以写多个 */
                .notMatch("/user/doLogin")        // 排除掉的 path 列表,可以写多个 
                .check(r -> StpUtil.checkLogin());        // 要执行的校验动作,可以写完整的 lambda 表达式
                
            // 根据路由划分模块,不同模块不同鉴权 
            SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
            SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
            SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
            SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
            SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice"));
            SaRouter.match("/comment/**", r -> StpUtil.checkPermission("comment"));
        })).addPathPatterns("/**");
    }
}

小结

以上是Sa-Token框架的入门使用,我们完全可以基于此框架,将登录业务代码填充到这些API之间,或者将这些API插入到我们开发完成的登录代码中,完成系统的登录功能,可以说非常方便啦。

总结

本文介绍了Sa-Token框架的入门使用,对应Sa-Token开发文档中的基础部分,详细的功能强烈建议参看Sa-Tokan文档

在这里插入图片描述