当前位置:   article > 正文

分布式权限认证-Sa-token,So Easy!_satoken 分布式鉴权

satoken 分布式鉴权

1、何为鉴权?为何鉴权?

鉴权也被称为权限控制,是一种在计算机网络中保护用户对特定资源的使用,防止未经授权的访问的技术手段。
它是在信息系统、网络或应用中,确定用户的身份是否合法,并根据用户的身份和权限,决定其在系统中能够访问哪些资源或执行哪些操作的过程。这个过程通常包括身份验证和权限验证两个阶段。

  • 身份验证是确定用户是否是系统中的合法用户,常见的鉴权方式包括用户名密码认证、数字证书认证、生物特征认证等。一旦身份验证通过,用户就被视为已经获得了对某些资源的访问权限。
  • 授权阶段系统会根据用户的身份和权限,授予其访问和操作特定资源的能力。这种授权通常是由系统管理员或第三方服务进行的,以确保资源的合理分配和使用。

鉴权是一种重要的安全机制,通过合理的身份验证和授权策略,以及采用多种安全措施,可以有效地防止网络攻击和滥用行为,保护网络中的资源不被未经授权的用户使用。

2、权限框架

在Java开发环境非常成熟的当下,框架的产生可谓是日新月异,在近十年的Java开发中,有如下权限框架非常火爆。

2.1、Shior

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。

2.1.1、基本概念

三个核心组件:Subject、 SecurityManager 、 Realms。

  1. Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。
  2. SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
  3. Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。

2.1.2、Shior架构图

shior系统图

2.2、Spring Security

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。
Spring Security本质上就是一系列的过滤器。

它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

Spring Security对Web安全性的支持大量地依赖于Servlet过滤器。这些过滤器拦截进入请求,并且在应用程序处理该请求之前进行某些安全处理。 Spring Security提供有若干个过滤器,它们能够拦截Servlet请求,并将这些请求转给认证和访问决策管理器处理,从而增强安全性。根据自己的需要,可以使用适当的过滤器来保护自己的应用程序。

2.2.1、基本概念

1·AuthenticationManager

用于处理用户认证。它是一个身份验证管理器,负责协调和执行身份验证过程。AuthenticationManager接受一个Authentication对象作为输入,并返回一个已经填充了身份验证信息的完整的Authentication对象。

2.DaoAuthenticationProvider

是AuthenticationProvider接口的一个实现类。DaoAuthenticationProvider使用一个UserDetailsService来获取用户的详细信息,并使用PasswordEncoder对用户提供的凭据进行验证。它的主要作用是从数据库或其 他持久化存储中获取用户信息,并进行身份验证。

3.AuthenticationProvider

AuthenticationProvider是一个接口,定义了身份验证提供者的通用行为。它是一个抽象的身份验证机制,可以有多个实现类。DaoAuthenticationProvider就是AuthenticationProvider接口的一个实现类之一。通过实现AuthenticationProvider接口,可以自定义身份验证逻辑。

2.2.2、三者的关系

  1. AuthenticationManager是一个身份验证管理器,负责协调和执行身份验证过程。
  2. DaoAuthenticationProvider是AuthenticationProvider接口的一个实现类,用于从数据库或其他持久化存储中获取用户信息并进行身份验证。
  3. AuthenticationProvider是一个接口,定义了身份验证提供者的通用行为,可以有多个实现类。

2.2.3、SpringSecurity架构图

在这里插入图片描述

2.3、Sa-Token

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

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

2.3.1、集成方式

SpringBoot 2.x

<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>1.37.0</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

SpringBoot 3.x

<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot3-starter</artifactId>
    <version>1.37.0</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

分布式

<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-reactor-spring-boot-starter</artifactId>
    <version>1.37.0</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

2.3.2、基本配置

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

2.3.3、分布式下的鉴权方案

本文主要记录Sa-Token在分布式Gateway环境下的鉴权方式,想进一步了解Sa-Token请看官网。

2.3.3.1、数据库的设计

用户表

CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',
  `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '手机号',
  `sex` int NOT NULL DEFAULT '0' COMMENT '0男,1女,2其他',
  `avatar` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '头像',
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '签名',
  `identify_card` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '身份证号',
  `status` bit(1) DEFAULT NULL COMMENT '当前用户是否被冻结',
  `travel_official` bit(1) DEFAULT b'0' COMMENT '是否进行了官方认证',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `PHONE_INDEX` (`phone`) USING BTREE,
  UNIQUE KEY `IDENTIFY_CARD_INDEX` (`identify_card`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=91706 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

角色表

CREATE TABLE `roles` (
  `id` int NOT NULL AUTO_INCREMENT,
  `role_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `description` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

角色用户映射表

CREATE TABLE `user_role_relationship` (
  `user_id` int NOT NULL,
  `role_id` int NOT NULL,
  PRIMARY KEY (`user_id`,`role_id`) USING BTREE,
  UNIQUE KEY `USER_ROLE_INDEX` (`user_id`,`role_id`) USING BTREE,
  KEY `ROLE_ID` (`role_id`) USING BTREE,
  CONSTRAINT `ROLE_ID` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
  CONSTRAINT `USER_ID` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

菜单表

CREATE TABLE `menu` (
  `id` int NOT NULL AUTO_INCREMENT,
  `menu_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `menu_path` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `need_login` bit(1) NOT NULL DEFAULT b'0',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

菜单角色映射表

CREATE TABLE `role_menu_relationship` (
  `roles_id` int NOT NULL,
  `menu_id` int NOT NULL,
  PRIMARY KEY (`roles_id`,`menu_id`) USING BTREE,
  UNIQUE KEY `ROLE_MENU_INDEX` (`roles_id`,`menu_id`) USING BTREE,
  KEY `MENU_INDEX` (`menu_id`) USING BTREE,
  CONSTRAINT `MENU_INDEX` FOREIGN KEY (`menu_id`) REFERENCES `menu` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
  CONSTRAINT `ROLE_INDEX` FOREIGN KEY (`roles_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
2.3.3.2、Gateway配置

在系统中需要Sa-Token整合Redis实现登录持久化记录,所以需要引入:sa-token-redis-jacksoncommons-pool2两个依赖。
官网解释:https://sa-token.cc/doc.html#/up/integ-redis

<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-reactor-spring-boot-starter</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
</dependency>


<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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
2.3.3.3、Yaml配置
spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    timeout: 10s
    lettuce:
      pool:
        # 连接池最大连接数
        max-active: 200
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms
        # 连接池中的最大空闲连接
        max-idle: 10
        # 连接池中的最小空闲连接
        min-idle: 0

sa-token:
  token-name: authentication
  timeout: 604800
  token-style: simple-uuid
  is-share: true
  is-concurrent: true
  is-log: false
  is-read-cookie: false
  is-read-head: true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
2.3.3.4、内置的Token生成规则
  1. uuid:uuid风格 (默认风格)

623368f0-ae5e-4475-a53f-93e4225f16ae

  1. simple-uuid:同上,uuid风格, 只不过去掉了中划线

6fd4221395024b5f87edd34bc3258ee8

  1. random-32 :随机32位字符串

qEjyPsEA1Bkc9dr8YP6okFr5umCZNR6W

  1. random-64:随机64位字符串

v4ueNLEpPwMtmOPMBtOOeIQsvP8z9gkMgIVibTUVjkrNrlfra5CGwQkViDjO8jcc

  1. random-128:随机128位字符串

nojYPmcEtrFEaN0Otpssa8I8jpk8FO53UcMZkCP9qyoHaDbKS6dxoRPky9c6QlftQ0pdzxRGXsKZmUSrPeZBOD6kJFfmfgiRyUmYWcj4WU4SSP2ilakWN1HYnIuX0Olj

  1. tik:tik风格

gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__

2.3.3.5、自定义SaReactorFilter
@Configuration
@RequiredArgsConstructor
public class SaTokenConfig {
    @Lazy
    @Resource
    private UserFeign userFeign;
    private final RedisTemplate<String, Object> redisTemplate;

    @Bean
    public SaReactorFilter getSaReactorFilter() {
        return new SaReactorFilter()
                .addInclude("/**")
                .setAuth(obj -> {
                    String travelId = SaHolder.getRequest().getHeader("authentication");
                    Object cache = redisTemplate.opsForValue()
                            .get(SystemProperties.USER_INFO_PREFIX + StpUtil.getLoginIdByToken(travelId));
                    if (travelId != null && cache == null) {
                        throw new RuntimeException();
                    }
                    executorMenuPath();
                })
                .setError(e -> {
                    e.printStackTrace();
                    ServerWebExchange exchange = SaReactorSyncHolder.getContext();
                    exchange.getResponse().getHeaders().set("Content-Type", "application/json; charset=utf-8");
                    return SaResult.get(HttpStatus.BAD_REQUEST.value(), "请先进行登录", null);
                })
                // 跨域信息
                .setBeforeAuth(obj -> {
                    SaHolder.getResponse()
                            .setHeader("Access-Control-Allow-Origin", "*")
                            .setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
                            .setHeader("Access-Control-Max-Age", "3600")
                            .setHeader("Access-Control-Allow-Headers", "*");
                    SaRouter.match(SaHttpMethod.OPTIONS).back();
                });
    }


	// 进行权限判断
    private void executorMenuPath(){
        Result<Map<String, List<Menu>>> systemMenu = userFeign.allMenu();
        if(systemMenu.getCode() != HttpStatus.OK.value()) return;

        Map<String, Menu> path = new HashMap<>();
        systemMenu.getBody().forEach((key, value) -> {
            value.forEach(menu -> path.put(menu.getMenuPath(), menu));
        });
        Menu menu = path.get(SaHolder.getRequest().getRequestPath());
        if(menu.isNeedLogin()) StpUtil.checkLogin();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
2.3.3.6、实现StpInterface接口

StpInterface接口是用来获取用户所拥有的权限和可访问路径的接口。
Sa-Token的鉴权方式就是会调用这个接口的实现类。

@Component
public class StpInterceptorImpl implements StpInterface {

    @Resource
    @Lazy
    private UserFeign userFeign;

    // 获取当前用户可以访问的路径
    @Override
    public List<String> getPermissionList(Object o, String s) {
        Result<List<Menu>> response = userFeign.userAccessMenuByRoles();
        if(response.getCode() == HttpStatus.OK.value()){
            return response.getBody().stream().map(Menu::getMenuPath).toList();
        }
        return Collections.emptyList();
    }

    // 获取当前用户的所有角色
    @Override
    public List<String> getRoleList(Object o, String s) {
        Result<List<Role>> response = userFeign.roleList();
        if(response.getCode() == HttpStatus.OK.value()){
            return response.getBody().stream().map(Role::getRoleName).toList();
        }
        return Collections.emptyList();
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
2.3.3.7、注意事项
  1. 其实Sa-Token和Security很像,本质上都是一系列过滤器和调用链,如果在Gateway上使用Sa-Token,那么一定要配置跨域,因为Sa-Token会在Gateway之前对请求进行拦截,放行后才会到达Gateway。
  2. StpUtil.checkLogin()StpUtil.checkRole()方法都是有顺序的,不能直接对系统中的所有路径进行配置,而是需要对当前请求的路径做配置。
  3. 在Gateway启动的时候不能做阻塞的任务,不然Gateway无法启动。

3、三大框架的比较

3.1、Shior

Shior正在逐步退出国内市场,原因有如下几个:

  1. Shior的登录地址默认是login.jsp,这在前后端分离的模式下需要重写许多类,这在分布式项目中可能会带来一些麻烦。
  2. 对于保持用户登录状态等需求,Shior也并不适合处理。
  3. 在分布式项目中,如电商项目,通常不需要明确的权限划分,使用Shior进行权限管理可能会过于复杂。同时,Shior对于SpringCloud等各种分布式框架来说,可能也是一场“灾难”,每个子系统可能都需要编写一些Shior的东西,这会逐渐变得繁琐并增加开发难度。

3.2、SpringSecurity

Spring Security 并没有逐渐退出国内市场,在国内外市场,Spring Security 的使用仍然非常广泛。许多大型企业和关键基础设施都在使用 Spring Security 来保护其应用程序和数据。同时,Spring Security 也在不断进行更新和改进,以适应不断变化的安全威胁和需求。

3.3、Sa-Token

阿里开源的轻量级权限框架,伴随简便的开发模式在国内越来越流行。

使用Sa-Token的理由主要有以下几点:

  1. 轻量级框架:Sa-Token是一个轻量级的Java权限认证框架,相对于其他框架来说,它更加简洁、易用,并且可以快速地实现用户身份验证、权限控制、会话管理等功能。
  2. 集成简单:Sa-Token的集成非常简单,只需要在项目中添加相关的依赖,就可以快速地实现权限认证功能。
  3. API设计优雅:Sa-Token的API设计非常优雅,它提供了简单易用的接口,使得开发者可以非常方便地实现权限认证功能。
  4. 丰富的功能:Sa-Token不仅提供了基本的权限认证功能,还支持单点登录、OAuth2.0、微服务网关鉴权等一系列权限相关问题。
  5. 高性能:Sa-Token采用了高效的认证和授权机制,可以快速地处理大量的用户认证和授权请求,提高了系统的性能。
  6. 广泛的应用场景:Sa-Token可以应用于各种需要权限认证的场景,如企业应用、Web应用、移动应用等。

最为后端开发者我个人是非常喜欢Sa-Token的简单开发模式。 本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】

推荐阅读
相关标签