Sa-Token登录验证

在很多情况下,我们的功能应该是必须登录之后才能访问,比如说访问我的订单接口,如何实现这个功能呢?我们通常的做法是在处理请求之前,增加一层接口校验:

  • 如果校验通过(已登录),则:正常返回数据。
  • 如果校验未通过(未登录),则:抛出异常,告知其需要先进行登录。

接下来, 我们来分析一下这个过程。

登录验证过程

既然是登录校验,所以还得包含登录,完整的过程如下:

  1. 用户提交 name + password 参数,调用登录接口。
  2. 登录成功,返回这个用户的 Token 会话凭证。
  3. 用户后续的每次请求,都携带上这个 Token。
  4. 服务器根据 Token 判断此会话是否登录成功。

如果要实现以上的登录验证过程,我们需要做什么呢?如果大家能想到cookie,session,那么实现起来就很简单,因为cookie,session属于JAVAEE规范的内容,浏览器和Web服务器都支持,因此我们只需要完成如下工作:

  1. 用户提交 name + password 参数,调用登录接口。
  2. 如果登录成功,则获取Session,并向session中放入所需数据返回即可,返回JessionId

之后就是Web服务器和浏览器自动完成的工作了:

  1. 在登录接口返回响应时,web服务器会自动给浏览器返回包含JSESSIONID的Cookie。
  2. 之后在请求的时候,浏览器会自动携带包含JSESSIONID的Cookie,
  3. 服务器自动获取Cookie中的JSESSIONID, 通过JSESSIONID查询服务器中是否有对应的Session,以此判断用户是否登录。

Cookie-Session局限性

经过以上分析,基于Cookie-Session进行登录身份认证确实简单,但是它也存在诸多局限性:

  • 缺乏精确控制:由于Servlet API的限制,后端体统无法获取web服务器中所有的Session,也无法查询某个用户的session,无法给Session设置过期时间。因此像踢人下线,账号封禁这样的功能,仅靠原生的Cookie-Session是无法实现的
  • 登录校验方式不灵活: 使用Cookie-Session的方式进行登录校验,正常只通过Cookie来校验,如果用户禁用Cookie呢?就只能通过额外的处理方式来解决。
  • 不支持多账号系统: 在我们的一个系统中,可能会存在不同的账号体系比如后台管理员,后台运营人员和普通用户。如果使用Cookie-Session的方式来实现登录验证,就必须得区分当前登录的用户,怎么区分呢?只能靠我们自己向登录用户的Session中添加标志位来区分不同类型的用户,而且可能在每个获取session数据的地方都要判断用户类型(因为不同类型的用户的数据是不同的)
  • 只能实现有状态的会话管理: 基于Cookie-Session实现登录校验用户的登录会话数据是存储在服务器内存,即服务器内存保存了用户的登录状态,因此我们说它实现的是有状态的会话管理。相反,如果我们将用户登录之后的会话数据不存在服务器,而是以某种形式返回给浏览器(或者其他客户端),之后每次请求后端都在请求中携带登录会话数据,同样可以校验用户是否登录。

Sa-Token

以上分析的基于Cookie-Session实现登录校验的局限性,都可以通过一个专门的登录框架Sa-Token来解决。它可以对会话数据做精确控制,校验方式不仅仅只限于cookie,支持多账号系统,既可以实现有状态的会话管理,也能实现无状态的会话管理。

Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证权限认证单点登录OAuth2.0分布式Session会话微服务网关鉴权 等一系列权限相关问题。

这里需要注意的是,Sa-Token模式实现的有状态会话管理,与JAVAEE中的Cookie-Session机制无关。它自己实现了一整套的会话管理系统!

Sa-Token会话模型

在传统的登录会话实现中,登录会话是以登录用户为单位,一个登录用户对应一个独立的会话。但是Sa-Token的会话模型更为强大和灵活。他分为Account-Session,Token-Session,Custom-Session。

  • Account-Session: 指的是框架为每个 账号id 分配的 Session
  • Token-Session: 指的是框架为每个 token 分配的 Session
  • Custom-Session(了解): 指的是以一个 特定的值作为SessionId,来分配的 Session

这里需要解释一下Token-Session,为什么会有Token-Session呢?主要是考虑到同一个用户的多端登录的情况,比如PC端,手机APP端。很多时候,我们需要区分不同终端的数据,如果将同一个用户的登录会话数据都存储在Account-Session中,会比较麻烦。

分析一个场景,假设我们要实现,客户端超过两小时无操作就自动下线(退出登录),如果两小时内有操作,就再续期两小时,直到新的两小时无操作。

  • 思路很简单,就是在session中记录用户上次操作的时间,就可以判断出用户是否超过两小时登录无操作了
  • 但是如果PC端和APP端是共享的同一个 Account-Session ,PC端的时间和APP端的上次操作时间都记录在如果把数据放在Account-Session, 那就意味着,即使用户在PC端一直无操作,只要手机上用户还在不间断的操作,那PC端也不会过期!

所以即使是同一个用户,此时针对不同的终端就需要用到不同的Token-Session,分别存储不同终端的数据

简而言之:

  • Account-Session 以账号 id 为主,只要 token 指向的账号 id 一致,那么对应的Session对象就一致
  • Token-Session 以token为主,只要token不同,那么对应的Session对象就不同
  • Custom-Session 以特定的key为主,不同key对应不同的Session对象,同样的key指向同一个Session对象

Sa-Token登录配置

引入依赖

如果项目使用的是 Spring Boot 3,需要先引入 Sa-Token 的 starter 依赖:

<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot3-starter</artifactId>
    <version>1.40.0</version>
</dependency>

登录校验拦截器配置

在我们的项目中,可能不是所有的接口访问都需要做登录身份校验,比如登录接口,比如电商首页,这样的接口都是没登录就可以访问的,因此我们需要配置,到底那些接口的访问需要做登录校验,哪些不需要

/**
    配置除了/user/login请求路径之外的其他请求都需要做登录校验
*/
@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都可以多次链式调用,指定多个路径
                .addPathPatterns("/**")
                .excludePathPatterns("/user/doLogin");  
    }
}

基本配置

我们可以在springboot项目的application.yml文件中添加以下配置

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: false
    # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
    token-style: uuid
    # 是否输出操作日志 
    is-log: true

或者也可以以配置类的方式来配置

/**
 * Sa-Token 配置类
 */
@Configuration
public class SaTokenConfigure {
    // Sa-Token 参数配置,参考文档:https://sa-token.cc
    // 此配置会覆盖 application.yml 中的配置
    @Bean
    @Primary
    public SaTokenConfig getSaTokenConfigPrimary() {
        SaTokenConfig config = new SaTokenConfig();
         // token 名称(同时也是 cookie 名称)
        config.setTokenName("satoken");  
         // token 有效期(单位:秒),默认30天,-1代表永不过期 
        config.setTimeout(30 * 24 * 60 * 60);
        // token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
        config.setActiveTimeout(-1);
         // 是否允许同一账号多地同时登录(为 true 时允许一起登录,为 false 时新登录挤掉旧登录)
        config.setIsConcurrent(true);
         // 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token,为 false 时每次登录新建一个 token)
        config.setIsShare(false);
        // token 风格
        config.setTokenStyle("uuid");
         // 是否输出操作日志 
        config.setIsLog(false);                    
        return config;
    }
}

Sa-Token登录API

在我们的项目中,我们不考虑多端登录,因此只会使用Account-Session,接下来介绍一下登录相关的部分API

  • 登录相关:我们一般使用userId作为账号id登录即可
// 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等
StpUtil.login(Object id);   // account-session

// 当前会话注销登录
StpUtil.logout();

// 获取当前会话是否已经登录,返回true=已登录,false=未登录
StpUtil.isLogin();

// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.checkLogin();
  • 获取登录账号
// 获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException`
Object id = StpUtil.getLoginId();

// 类似查询API还有:
String id = StpUtil.getLoginIdAsString();    // 获取当前会话账号id, 并转化为`String`类型
Integer id = StpUtil.getLoginIdAsInt();       // 获取当前会话账号id, 并转化为`int`类型
Long id = StpUtil.getLoginIdAsLong();      // 获取当前会话账号id, 并转化为`long`类型
  • 获取登录会话
// 获取当前账号 id 的 Account-Session (必须是登录后才能调用)
SaSession session = StpUtil.getSession();
// 获取当前账号 id 的 Account-Session, 并决定在 Session 尚未创建时,是否新建并返回
SaSession session = StpUtil.getSession(true);

// 获取账号 id 为 10001 的 Account-Session
SaSession session = StpUtil.getSessionByLoginId(10001);

// 获取账号 id 为 10001 的 Account-Session, 并决定在 Session 尚未创建时,是否新建并返回
SaSession session = StpUtil.getSessionByLoginId(10001, true);
  • 向Session对象中存取数据
// 写值 
session.set("name", "zhang"); 

// 取值
Object value = session.get("name");

Integer intalue = session.getInt("age");         // 取值 (转int类型)
Long longValue = session.getLong("age");        // 取值 (转long类型)
String stringVlaue = session.getString("name");     // 取值 (转String类型)
Double doubleValue = session.getDouble("result");   // 取值 (转double类型)
Float floatValue = session.getFloat("result");    // 取值 (转float类型)
Student student = session.getModel("key", Student.class);     // 取值 (指定转换类型)

// 是否含有某个key (返回 true 或 false)
session.has("key"); 

// 删值 
session.delete('name');          

// 清空所有值 
session.clear();
  • 获取session对应的标识(token)
// 获取当前会话的 token 值
StpUtil.getTokenValue();
  • 生成密码摘要(密文密码): 通常我们不会将原始密码存储到数据库中以防止数据泄漏,所以需要将原始密码做转化生成密文密码然后存入数据库。这里基于md5算法生成密文密码(hash算法)
String newPasswd =  SaSecureUtil.md5(原始密码字符串);

Sa-Token多账号系统登录

有的时候,我们会在一个项目中设计两套账号体系,比如一个电商系统的 user表(普通用户) 和 admin表(后台系统), 在这种场景下,如果两套账号我们都使用 StpUtil 类的API进行登录鉴权,那么势必会发生逻辑冲突。

假如说我们的 user表 和 admin表 都有一个 id=10001 的账号,它们对应的登录代码:StpUtil.login(10001) 是一样的, 那么问题来了:在StpUtil.getLoginId()获取到的账号id如何区分它是User用户,还是Admin用户?

实现方式

这就是多账号登录的问题,那么如何解决这个问题呢?观察前面介绍的api调用,都是经过 StpUtil 类的各种静态方法进行授权认证, 而如果我们深入它的源码,就会发现,此类并没有任何代码逻辑,唯一做的事就是对成员变量stpLogic的各个API包装了一下,也就是说实际的功能都是由StpLogic来完成的!

/**
 * Sa-Token 权限认证工具类
 *
 * @author click33
 * @since 1.0.0
 */
public class StpUtil {
	
	private StpUtil() {}
	
	/**
	 * 多账号体系下的类型标识
	 */
	public static final String TYPE = "login";
	
	/**
	 * 底层使用的 StpLogic 对象
	 */
	public static StpLogic stpLogic = new StpLogic(TYPE);

	/**
	 * 获取当前 StpLogic 的账号类型
	 *
	 * @return /
	 */
	public static String getLoginType(){
		return stpLogic.getLoginType();
	}

	/**
	 * 会话登录
	 *
	 * @param id 账号id,建议的类型:(long | int | String)
	 */
	public static void login(Object id) {
		stpLogic.login(id);
	}
	/**
	 * 判断当前会话是否已经登录
	 *
	 * @return 已登录返回 true,未登录返回 false
	 */
	public static boolean isLogin() {
		return stpLogic.isLogin();
	}
    
    /**
	 * 在当前客户端会话注销
	 */
	public static void logout() {
		stpLogic.logout();
	}
    
   /**
	 * 获取当前会话账号id,如果未登录,则抛出异常
	 *
	 * @return 账号id
	 */
	public static Object getLoginId() {
		return stpLogic.getLoginId();
	}
    
    /**
	 * 获取当前请求的 token 值
	 *
	 * @return 当前tokenValue
	 */
	public static String getTokenValue() {
		return stpLogic.getTokenValue();
	}
    
    ...

}

注意观察以上代码,我们会发现,StpLogic的构造方法,有一个参数Type(在StpUtil类中,默认的Type值为”user”),这个Type其实就是用户标识,我们只需在创建StpLogic对象的时候,给它不同的Type,它就能针对不同类型的用户,也就是不同类型的账号!

因此,如何解决多账号登录校验的问题呢?

/**
 * StpLogic 门面类,管理项目中所有的 StpLogic 账号体系
 */
public class StpKit {

    /**
     * 默认原生会话对象
     */
    public static final StpLogic DEFAULT = StpUtil.stpLogic;

    /**
     * Admin 会话对象,管理 Admin 表所有账号的登录、权限认证
     */
    public static final StpLogic ADMIN = new StpLogic("admin");

    /**
     * User 会话对象,管理 User 表所有账号的登录、权限认证
     */
    public static final StpLogic USER = new StpLogic("user");

    /**
     * XX 会话对象,(项目中有多少套账号表,就声明几个 StpLogic 会话对象)
     */
    public static final StpLogic XXX = new StpLogic("xx");

}

然后针对不同类型的用户,使用不同类型的StpLogic对象,调用其login,logout,getLoginId方法即可。

// 后台用户开启登录会话 
StpKit.ADMIN.login(1001);
// 普通用户开启登录会话
StpKit.USER.login(1001);

// 获取后台用户的账号id(用户id)
StpKit.ADMIN.getLoginId();

// 获取普通用户的账号id(用户id)
StpKit.USER.getLoginId();

多账号配置

当然,我们还可以针对不同账号的登录校验做不同的配置,但是只能使用配置类的方式

多账号登录校验拦截

不同账号的登录校验,针对的可能是不同的接口,我们也可以在前面讲解的登录校验拦截器的基础上灵活指定

项目中的登录验证

在我们的项目中,登录校验又分成了后台登录校验和前台登录校验。

后台登录校验

首先,我们需要实现后台登录

// 1. 查询数据库,验证后台用户的用户名和密码

// 2. 使用sa-token的login方法开启会话(注意使用后台账号的StpLogic对象)

// 3. 更新员工登录时间,登录ip

// 4. 获取会话id(即会话对应的tokenValue),并返回

接着,在访问接口时校验登录状态

这里其实只需要指定登录拦截校验路径即可,如1.7.3所示的那样,Sa-Token就会自动完成登录校验了。

前台登录校验

前台登录校验的实现非常类似于后台登录校验,首先需要实现前台登录,当用户在APP上正确的输入了验证码,点击登录,就会访问前台登录接口了,此时,前台登录接口需要完成的工作如下:

// 1.  根据手机号,查询数据库(uer表)判断用户是否存在
// 2.  若存在,则可以登录成功。
//     a. 使用sa-token login方法开启会话(注意使用前台账号的StpLogic对象)
//     b. 更新用户登录时间,登录ip
//     c. 返回登录会话对应的tokenValue
// 3.  若不存在
//     a. 则首先添加用户信息及其他信息(相当于注册了)
//     b. login方法开启会话
//     c. 更新用户登录时间,登录ip
//     d. 返回登录会话对应的tokenValue

其中关于,添加用户信息及其他信息到底有哪些信息呢?如下所示

   userDO = UserDO.builder()
                    .thirdPartyName(command.getDisplayName())
                    .email(command.getEmail())
                    .headImg(command.getPhotoUrl())
                    .phoneNumber(command.getPhoneNumber())
                    .lastLoginIp(ipAddr)
                    .nickName(getName())
                    .lastLoginTime(LocalDateTime.now())
                    // 默认等级
                    .levelName(CommonConstant.REGISTER_LEVEL_NAME)
                    .levelValue(CommonConstant.REGISTER_LEVEL_VALUE)
                    .levelDate(LocalDate.now())
                    // 默认单词上限
                    // 默认规划的每天单词量
                    .vocCountOfDay(CommonConstant.REGISTER_VOC_OF_DAY)
                    .build();
            // 保存用户信息
            userMapper.insert(userDO);

            // 添加用户单词上限统计数据(vocBoundStatistics)
            UserVocBoundStatisticsDO userVocBoundStatisticsDO = UserVocBoundStatisticsDO.builder()
                    .userId(userDO.getId())
                     // 新用户会送单词上限
                    .total(CommonConstant.REGISTER_VOC_BOUND)
                     // 新用户的总单词上限就是可用单词上限
                    .available(CommonConstant.REGISTER_VOC_BOUND)
                    .pay(0)
                     // 免费获取的单词上限一开始就是新用户送的单词上限
                    .free(CommonConstant.REGISTER_VOC_BOUND)
                     // 已使用的单词上限为0
                    .occupied(0)
                     // 已购买的单词上限为0
                    .exchange(0)
                    .build();
            userVocBoundStatisticsMapper.insert(userVocBoundStatisticsDO);

            // 添加用户的签到统计数据(userCheckinStatistics)
            UserCheckinStatisticsDO userCheckinStatisticsDO = UserCheckinStatisticsDO
                    .builder()
                    .userId(userDO.getId())
                     // 一开始总签到天数为0
                    .totalDays(0)
                     // 当前连续签到天数为0
                    .curContinuousDays(0)
                     // 最大连续签到天数为0
                    .maxContinuousDays(0)
                    .build();
            userCheckinStatisticsMapper.insert(userCheckinStatisticsDO);

            // 添加用户的学习提醒(user_remind)
            UserRemindDO userRemindDO = UserRemindDO.builder()
                    .phoneNumber(userDO.getPhoneNumber())
                    .userId(userDO.getId())
                    .userName(userDO.getNickName())
                     // 设置默认的打开状态
                    .messageStatus(CommonConstant.DEFAULT_REMIND_MESSAGE_STATUS)
                	 // 设置默认的提醒时间
                    .remindTime(LocalTime.parse(CommonConstant.DEFAULT_REMIND_TIME))
                    .build();
            userRemindMapper.insert(userRemindDO);


            // 添加一条用户的单词上限的变化日志(vocBoundLog)
            UserVocBoundLogDO userVocBoundLogDO = UserVocBoundLogDO.builder()
                     // 日期
                    .logDate(LocalDate.now())
                     // 用户id
                    .userId(userDO.getId())
                     // 变化数量
                    .count(CommonConstant.REGISTER_VOC_BOUND)
                     // 变化类型(这里是新用户注册)
                    .type(CommonEnum.USER_VOC_BOUND_LOG_REGISTER.getValue())
                     // 变化原因说明
                    .description(CommonEnum.USER_VOC_BOUND_LOG_REGISTER.getDescription())
                    .build();
            userVocBoundLogMapper.insert(userVocBoundLogDO);

所谓权限认证,核心逻辑就是判断一个账号是否拥有指定权限:

  • 有,就让你通过。
  • 没有?那么禁止访问!

深入到底层数据中,就是每个账号都会拥有一组权限码集合,框架来校验这个集合中是否包含指定的权限码。

例如:当前账号拥有权限码集合 ["user-add", "user-delete", "user-list"],这时候我来校验权限 "user-update",则其结果就是:验证失败,禁止访问

鉴权原理

每个用户都可以有权限,是否有某个权限决定了用户是否可以合法的访问某个接口。每个权限可以用一个字符串表示,因此一个用户的权限,其实就是一个字符集合。同时,我们可以指定访问哪个接口需要什么权限,这样一来,在访问某个接口的时候,我们就可以判断用户是否有权限访问接口,这就是所谓的鉴权。

所以要实现鉴权,就得解决两个问题:

  • 给不同的接口指定所需要的访问权限
  • 获取当前用户所具有的权限集合

第一个问题非常好解决,包括Sa-Token在内的很多登录鉴权框架都支持通过注解的方式来指定接口的访问权限。

第二个问题就不是框架可以自己解决的了,因为不同的系统权限设计,权限名称可能是不同的,不同用户所具有的权限也会因业务的不同有所差异,因此第二个问题即获取用户的权限集合的工作需要我们来告诉Sa-Token这样的框架,每个用户都有哪些权限,Sa-Token才能帮我们完成鉴权。

鉴权模型

今天的大部分系统,都是基于RBAC模型来实现鉴权,也就是访问权限控制。

RBAC(Role-Based Access Control,基于角色的访问控制)是一种广泛使用的访问控制机制,它通过将权限分配给角色而非直接分配给用户来管理系统访问权限。

RBAC的核心概念:

  1. 用户(User):系统的使用者,可以是人、程序或其他系统
  2. 角色(Role):代表一组权限的集合,如”管理员”、”普通用户”、”财务人员”等
  3. 权限(Permission):对系统资源的操作能力,如”读取文件”、”修改设置”等

他们之间的关系是,一个用户可以拥有多个角色,一个角色包含多个权限。

因为一个角色代表一个特定的权限集合(也代表权限),所以在鉴权的时候,我们也可以指定基于角色来鉴权:指定什么样的角色,可以访问什么样的接口,基于用户所具有的角色来决定用户是否可以访问接口。

当然,在我们的项目中,权限数据的设计是基于RBAC,但是在访问接口的时候判断的是权限而非角色。

Sa-Token鉴权功能实现

接下来,我们来学习在Sa-Token中如何使用鉴权注解指定访问接口所需的权限,以及如何告诉Sa-Token用户具有哪些权限。

鉴权注解

@SaCheckRole("admin"): 角色校验 —— 必须具有指定角色标识才能进入该方法。
@SaCheckPermission("user:add"): 权限校验 —— 必须具有指定权限才能进入该方法。

// 角色校验:必须具有指定角色才能进入该方法 
@SaCheckRole("super-admin")        
@RequestMapping("addr")
public String addWithRole() {
    return "用户增加";
}

// 权限校验:必须具有指定权限才能进入该方法 
@SaCheckPermission("user-add")        
@RequestMapping("addp")
public String addWithPermission() {
    return "用户增加";
}

当然,为了让注解生效,我们得添加拦截器(这个拦截器就是我们配置登录校验的哪个拦截器),只要注册了这个拦截器注解就可以生效,如果在实现登录校验的时候,已经配置过,无需重复配置

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

获取权限集合

因为每个项目的需求不同,其权限设计也千变万化,因此 [ 获取当前账号权限码集合 ] 这一操作不可能内置到框架中, 所以 Sa-Token 将此操作以接口的方式暴露给我们,以方便我们根据自己的业务逻辑进行重写。

public interface StpInterface {

	/**
	 * 返回指定账号id所拥有的权限码集合 
	 * 
	 * @param loginId  账号id
	 * @param loginType 账号类型
	 * @return 该账号id具有的权限码集合
	 */
	List<String> getPermissionList(Object loginId, String loginType);

	/**
	 * 返回指定账号id所拥有的角色标识集合 
	 * 
	 * @param loginId  账号id
	 * @param loginType 账号类型
	 * @return 该账号id具有的角色标识集合
	 */
	List<String> getRoleList(Object loginId, String loginType);

}

我们只需实现接口中的方法,查询指定用户的权限集合(getPermissionList方法)以及用户的角色集合(getRoleList)

/**
 * 自定义权限加载接口实现类
 */
@Component    // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展 
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>();    
        list.add("admin");
        list.add("super-admin");
        return list;
    }

}

getPermissionList方法返回的就是指定用户的权限集合,getRoleList方法返回的就是指定用户的角色集合。只要我们实现了以上两个方法,就相当于告诉了Sa-Token如何获取用户的角色和权限,在真正鉴权的时候,他就会自动获取了。

多账号鉴权

查询不同类型的账号和权限

关于多账号鉴权,其实就很简单了,因为无论是getPermissionList方法或者getRoleList方法都有loginType参数,这个参数代表的就是

public interface StpInterface {

	/**
	 * 返回指定账号id所拥有的权限码集合 
	 * 
	 * @param loginId  账号id
	 * @param loginType 账号类型
	 * @return 该账号id具有的权限码集合
	 */
	List<String> getPermissionList(Object loginId, String loginType);

	/**
	 * 返回指定账号id所拥有的角色标识集合 
	 * 
	 * @param loginId  账号id
	 * @param loginType 账号类型
	 * @return 该账号id具有的角色标识集合
	 */
	List<String> getRoleList(Object loginId, String loginType);

}

所以我们在实现的时就可以根据loginType区分不同的用户类型,正确的查询不同类型用户的权限和角色列表了。

@Component    // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展 
public class StpInterfaceImpl implements StpInterface {

    /**
     * 返回一个账号所拥有的权限码集合 
     */
    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
		if ("admin".equals(loginType)) {
            // 返回后台用户权限列表
              
        }
        
        if ("xxx".equals(loginType)) {
            // 返回其他用户权限列表
        }
    }

    /**
     * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
     */
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
       		if ("admin".equals(loginType)) {
            // 返回后台用户角色列表
              
        }
        
        if ("xxx".equals(loginType)) {
            // 返回其他用户角色列表
        }
    }

}

使用不同类型的鉴权注解

同时,在多账号的情况下,不同的账号系统,当然也是用不同的方式来鉴权,那么我们怎么让Sa-Token能够区分出当前的接口是针对哪种类型账号的鉴权呢?

此时我们就需要自定义注解了,官方文档告诉我们:

  • 我们只需要将原来@SaCheckPermission注解中的内容复制到我们自定义注解中
  • 在自定义注解上加上@SaCheckPermission注解,并制定鉴权账号类型,例如:@SaCheckPermission(type = “admin”)
@SaCheckPermission(type = "admin")
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE})
public @interface SaAdminCheckPermission {

    /**
     * 需要校验的权限码
     * @return 需要校验的权限码
     */
    @AliasFor(annotation = SaCheckPermission.class)
    String [] value() default {};

    /**
     * 验证模式:AND | OR,默认AND
     * @return 验证模式
     */
    @AliasFor(annotation = SaCheckPermission.class)
    SaMode mode() default SaMode.AND;

    /**
     * 在权限校验不通过时的次要选择,两者只要其一校验成功即可通过校验
     *
     * <p>
     * 	例1:@SaCheckPermission(value="user-add", orRole="admin"),
     * 	代表本次请求只要具有 user-add权限 或 admin角色 其一即可通过校验。
     * </p>
     *
     * <p>
     * 	例2: orRole = {"admin", "manager", "staff"},具有三个角色其一即可。 <br>
     * 	例3: orRole = {"admin, manager, staff"},必须三个角色同时具备。
     * </p>
     *
     * @return /
     */
    @AliasFor(annotation = SaCheckPermission.class)
    String[] orRole() default {};
}
@SaCheckPermission(type = "admin")
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE})
public @interface SaAdminCheckPermission {

    /**
     * 需要校验的权限码
     * @return 需要校验的权限码
     */
    @AliasFor(annotation = SaCheckPermission.class)
    String [] value() default {};

    /**
     * 验证模式:AND | OR,默认AND
     * @return 验证模式
     */
    @AliasFor(annotation = SaCheckPermission.class)
    SaMode mode() default SaMode.AND;

    /**
     * 在权限校验不通过时的次要选择,两者只要其一校验成功即可通过校验
     *
     * <p>
     * 	例1:@SaCheckPermission(value="user-add", orRole="admin"),
     * 	代表本次请求只要具有 user-add权限 或 admin角色 其一即可通过校验。
     * </p>
     *
     * <p>
     * 	例2: orRole = {"admin", "manager", "staff"},具有三个角色其一即可。 <br>
     * 	例3: orRole = {"admin, manager, staff"},必须三个角色同时具备。
     * </p>
     *
     * @return /
     */
    @AliasFor(annotation = SaCheckPermission.class)
    String[] orRole() default {};
}

最后一步,为了让自定义注解生效,我们需要在加上一个配置类即可:

@Configuration
public class SaTokenLogicConfigure {

    @PostConstruct
    public void rewriteSaStrategy() {
        // 重写Sa-Token的注解处理器,增加注解合并功能
        SaAnnotationStrategy.instance.getAnnotation = (element, annotationClass) -> {
            return AnnotatedElementUtils.getMergedAnnotation(element, annotationClass);
        };
    }

}

项目中的鉴权

关于鉴权,也分成后台鉴权和前台鉴权(移动端鉴权),当然完成鉴权的前提是要先实现后台的账号权限管理的所有功能!

image-20250428103010114

至于具体后台有哪些权限和角色,我们可以参考公网的后台页面:

image-20250428103454735
image-20250428103627920

后台鉴权

首先说明后台鉴权分成两个部分——页面鉴权和接口鉴权。页面鉴权由前端实现,页面鉴权的效果是决定页面后台用户显示,接口鉴权的效果是决定接口能不能被后台用户访问(以及页面中的组件是否对用户显示),如果不能访问则抛出异常,让用户访问不到接口。

后台用户具有不同的角色,每种角色包含不同的权限集合,因此每个用户都有其对应的权限集合,对于后台用户权限主要用两种表示方式权限码和权限别名。页面鉴权判断的是权限码,接口鉴权判断的是权限别名。权限码形如100,10001,10001001,权限别名形如: admin:voc:add(具有一定含义方便人理解的字符串)

后台页面鉴权

后台页面中的每一个部分都分配了一个权限码,形如100,10001,10001001这样的值,如下图所示,当且仅当后台用户所具有的权限码集合中包含了某个页面或者页面组件分配的权限码,这个页面或者页面中的组件才会显示。否则不显示

image-20250428094540675

那么,后端的页面怎么知道用户有哪些权限码集合呢?页面会请求后端的GET /admin/employee/info

image-20250428101448718

该接口的响应体中会包含用户的权限码集合

image-20250428101718045

该接口涉及到的表有:

  • employee 员工表
  • employee_role 员工角色表
  • role_permission 角色权限表
  • permission 权限表

后台接口鉴权

首先我们需要在某个后台接口的Controller方法上加上鉴权注解,并指定权限的别名(之所以指定别名是因为方便我们理解),比如:

image-20250428104104797

然后实现2.3.2中的获取权限的接口即可:

@Component
public class StpInterfaceImpl implements StpInterface {

    @Resource
    EmployeeService employeeService;

    @Resource
    UserService userService;

    @Override
    public List<String> getPermissionList(Object loginId, String loginType) {
        Long userId = Long.parseLong(loginId.toString());
        if(loginType.equals("admin")){

            // 返回后台用户所拥有的权限别名集合
        }
    }

    // 我们未使用角色鉴权,所以这个方法可以不用做具体实现
    @Override
    public List<String> getRoleList(Object loginId, String loginType) {
        return null;
    }

涉及到的表有:

  • employee 员工表
  • employee_role 员工角色表
  • role_permission 角色权限表
  • permission 权限表

前台鉴权

有同学可能会觉得疑惑,前台普通用户还能有不能访问的功能吗? 有的,因为在我们的背单词APP中,用户是有等级的,而每个等级会包含一些等级特权,没有达到相应等级就不能访问(使用)相对应的特权。具体到底有多少个等级,每个等级包含什么样的等级特权,参见公网服务器的后台页面。类似于后台鉴权,前台鉴权也分成了页面鉴权和接口鉴权,但是前台鉴权会稍微简单一些,因为无论是页面鉴权还是接口鉴权,都是基于等级特权码

image-20250428105746438
image-20250428105835622

前台页面鉴权

移动端APP的各个等级特权相关的组件,都指定了确定的特权码,当且仅当用户的等级特权码中包含某个组件的特权码,该组件才会显示,那么移动端页面怎么知道用户有哪些特权码呢?移动端会访问后端接口

image-20250428113018310

返回的结果中就包含用户当前的等级对应的特权码集合

image-20250428113110118

涉及到的表有:

  • user 用户表
  • level 等级表
  • level_privilege 等级特权表

前台鉴权接口鉴权,我们首先在访问等级特权的所有Controller方法上加上鉴权注解,比如

image-20250428113727695

前台接口鉴权

然后实现2.3.2中的获取权限的接口,返回用户的等级特权码集合即可

 Long userId = Long.parseLong(loginId.toString());
        if(loginType.equals("admin")){
            return 后台用户的权限别名集合;
        }

        // 否则说明是鉴权的用户类型为普通用户,返回普通用户的等级特权码集合即可
        return privilegeCodes;

所涉及到的表同前台页面鉴权

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇