一、前言
互联网安全的理念这些年逐渐开始深入人心,以java流行的微服务技术体系来说,安全可以说是构筑微服务系统项目的基石,所以在微服务的技术选型和技术组件中,随处可见安全的影子,比如在springcloud体系中,gateway网关就承载了所有进入微服务接口的安全防线作用,再比如,所有访问系统资源的用户或请求,必须经过登录认证等,但是这些还远远不够,对于一个系统来说,必须从设计和技术选型阶段,就应该结合当前的现状将系统安全作为一个重要的因素考虑进去,从而避免给后续带来麻烦。
二、认证与授权介绍
2.1 什么是认证
在微服务架构中,认证(authentication)是指确认用户身份的过程。认证是安全领域的一个核心概念,在微服务架构中尤其重要,因为微服务通常通过网络进行交互,这意味着需要确保每个服务请求都是经过适当身份验证。
2.1.1 认证的目的
认证通常具有下面作用:
- 验证用户身份:
- 确保请求是由授权用户发出的;
- 保护资源安全:
- 限制对敏感数据或操作的访问;
- 通常是为了限制对系统服务api的访问;
- 跟踪和审计:
- 记录谁访问了哪些资源,以及进行了什么操作;
2.1.2 认证基本步骤
尽管不同的微服务系统在技术选型上略有差异,但是认证的基本流程是类似的,下面是一个相对通用的认证流程:
- 用户提交凭证
- 用户在尝试访问受保护资源之前,需提交某种形式的凭证,如用户名/密码、api 密钥、令牌等;
- 验证凭证
- 服务端验证用户提供的凭证是否有效;
- 颁发凭证
- 如果凭证有效,则颁发一个新的凭证(如访问令牌)给用户,以便后续请求使用;
- 使用凭证访问资源
- 用户使用颁发的凭证来访问受保护资源;
事实上,在现在很多微服务项目中,集成了第三方安全框架之后,上面的这些基本的认证流程在安全框架中已经替我们实现了,开发人员只需要集成并按照规范使用即可。
2.2 什么是授权
很多初学者很容易把认证与授权这两个概念混为一谈,这样理解肯定是错误的,授权(authorization)是指确定已经通过认证的用户或服务,能够访问哪些资源或执行哪些操作的过程。授权是在认证之后发生的,通常用来控制访问级别和权限。为什么要授权,主要出于下面的考虑:
- 访问控制:确定用户可以访问哪些资源或执行哪些操作;
- 安全性:防止未经授权的访问,保护敏感数据和功能;
- 角色管理:基于用户的角色来授予不同的权限;
- 审计和合规性:确保系统符合法规要求,如 gdpr 或 hipaa;
2.2.1 常用的授权模型
在项目开发中,授权模型的选择至关重要,这将关系到系统中涉及到权限控制的方方面面,下面介绍几种常用的授权模型:
- 基于角色的访问控制 (rbac):
- 简介:根据用户的角色来决定他们可以访问哪些资源或执行哪些操作。
- 优点:易于管理和维护,可以轻松地为角色添加或移除权限。
- 应用场景:rbac是经典的授权模型,适用于大多数企业级的应用。
- 基于属性的访问控制 (abac):
- 简介:基于用户相关属性(如部门、地理位置等)及环境条件(如时间、设备等)来决定访问权限。
- 优点:高度灵活,可以适应复杂的业务需求。
- 应用场景:适用于需要精细控制访问权限的场景。
- 强制访问控制 (mac):
- 简介:基于安全标签来决定访问权限,通常用于政务类、军工类相关的项目。
- 优点:提供最高安全级别的安全性。
- 应用场景:适用于需要严格安全控制的环境。
基本上来说,rbac如果设计和使用得当,就能满足大多数的项目了,配合一些安全框架的使用,既能达到一定的安全等级,如果项目中还需要对更细粒度的权限进行控制,可以基于rbac作为基础模型进行数据表的扩展即可。
三、微服务中常用的认证安全框架
在微服务开发中,可以说越来越少的项目纯粹靠手写安全组件来维护整个系统的权限体系了,多少是借助第三方安全框架集成到项目中进行使用,下面介绍几种微服务中常用的涉及安全认证的技术框架。
3.1 spring security
spring security 是 spring 生态体系中最成熟、最全面的安全框架,提供了丰富的认证和授权功能,得到了众多公司项目的使用和认可。它提供丰富的认证机制,如表单认证、oauth2、jwt 等。
3.1.1 spring security 特点
spring security具有如下特点:
- 灵活性好
- 高度可配置:spring security 提供了高度可配置的 api,允许开发者根据自己的需求定制认证和授权逻辑;
- 支持多种认证机制:支持表单认证、http basic 认证、oauth2、openid connect、jwt 等多种认证机制;
- 可扩展性:可以轻松地扩展 spring security 的功能,例如自定义认证处理器、密码编码器等。
- 安全功能完善
- 认证:提供了多种认证方式,包括基于表单、基于 http basic、基于 oauth2 等;
- 授权:支持基于角色的访问控制 (rbac) 和基于属性的访问控制 (abac);
- 会话管理:可以管理用户的会话,包括会话固定保护、会话超时等;
- csrf 保护:提供了针对跨站请求伪造 (csrf) 攻击的保护机制;
- 跨站脚本防护:提供了针对跨站脚本 (xss) 攻击的防护措施;
- 异常处理:可以配置异常处理逻辑,以优雅地处理认证失败或授权失败的情况。
- 与其他组件集成能力强
- 与 spring 生态系统无缝集成:spring security 是 spring 生态系统的一部分,可以轻松地与其他 spring 框架集成,如 spring mvc、spring boot 等;
- 支持多种数据源:可以使用 jdbc、ldap、active directory 等多种数据源进行用户认证和授权;
- 与其他框架的集成:可以与其他非 spring 框架(如 jax-rs、vaadin 等)集成。
- 社区活跃
- 广泛的社区支持:spring security 有着庞大的开发者社区,提供了丰富的文档、教程和示例;
- 活跃的开发团队:spring security 由 pivotal(现 vmware)维护,定期发布新版本和安全更新。
- 文档和资源丰富
- 详细的文档:spring security 提供了详细的官方文档,覆盖了框架的所有方面;
- 丰富的教程和示例:有许多教程和示例可供参考,帮助开发者快速上手。
3.2 jwt (json web tokens)
jwt 是一种轻量级的令牌标准,用于在各方之间安全地传输信息。在微服务架构中,jwt 常被用作认证机制的一部分,特别是在无状态服务中。
3.2.1 jwt特点
jwt具有如下特点:
- 无状态
- 特点:jwt 是一个自包含的令牌,不需要服务器保存会话状态或查询数据库来验证用户身份;
- 优点:减少了服务器的负担,提高了可伸缩性和性能。
- 自包含
- 特点:jwt 包含了所有必要的信息,如用户标识、权限等,这些信息在令牌本身中编码;
- 优点:不需要额外的数据库查询来验证用户信息,使得 jwt 成为一种快速的认证机制。
- 轻量级
- 特点:jwt 是一个轻量级的 json 对象,内容体积小,传输效率高;
- 优点:降低了网络带宽的消耗,提高了系统的响应速度。
- 安全性好
- 特点:jwt 可以使用 hmac 算法或 rsa 的公私钥对进行签名,确保了数据的完整性和安全性;
- 优点:可以防止令牌被篡改,确保了数据传输的安全性。
- 支持多种签名算法
- 特点:jwt 支持多种签名算法,包括 hmac sha256、rsa/ecdsa 等;
- 优点:可以根据安全需求选择合适的签名算法。
- 扩展性好
- 特点:jwt 可以包含任意数量的自定义声明,允许在令牌中携带更多的信息;
- 优点:可以根据应用需求灵活地扩展令牌的内容。
- 解析简单
- 特点:jwt 的结构清晰,可以很容易地被客户端和服务端解析;
- 优点:简化了开发过程,提高了开发效率。
3.3 其他认证安全框架
这里再补充其他几种框架作为知识的扩展;
- apache shiro
- apache shiro 是一个强大且易用的 java 安全框架,提供了认证、授权、加密和会话管理等功能。
- 提供了丰富的认证和授权功能。
- 支持多种认证机制,如表单认证、ldap 等。
- 可以与 spring 框架很好地集成。
- 提供了详细的文档和支持社区。
- apache shiro 是一个强大且易用的 java 安全框架,提供了认证、授权、加密和会话管理等功能。
- spring cloud security
- spring cloud security 是 spring cloud 的一部分,它为 spring cloud 应用提供了一种简单的方法来实现安全性和认证。
- 集成了 spring security,提供了微服务安全性的最佳实践。
- 支持 oauth2 和 jwt。
- 适用于 spring cloud 的服务发现和配置中心。
- spring cloud security 是 spring cloud 的一部分,它为 spring cloud 应用提供了一种简单的方法来实现安全性和认证。
- oauth2
- oauth2 是一个开放标准,用于授权第三方应用访问用户的资源,而无需共享用户的凭证。
- 支持多种授权模式,如密码模式、授权码模式、客户端凭证模式等。
- 可以与其他认证框架(如 spring security)结合使用。
- 适用于服务间通信的认证和授权。
- oauth2 是一个开放标准,用于授权第三方应用访问用户的资源,而无需共享用户的凭证。
- keycloak
- keycloak 是一个开源的身份和访问管理解决方案,提供了一个全面的认证和授权平台。
- 支持多种认证协议,如 oauth2、openid connect、saml 等。
- 可以作为独立的服务部署,也可以集成到现有的应用中。
- 提供了用户管理、角色管理等功能。
- 支持多租户,便于管理多个不同的应用。
- keycloak 是一个开源的身份和访问管理解决方案,提供了一个全面的认证和授权平台。
四、sa-token介绍
4.1 sa-token是什么
sa-token(simple authentication token)是一个轻量级的 java 认证框架,旨在提供简单易用的认证和授权功能。sa-token 的设计目标是简单、高效,并且易于集成到现有的 java 项目中,特别是 spring boot 的微服务项目。官网:sa-token,文档地址:sa-token
4.2 sa-token特点
sa-token具有如下特点:
- 轻量级
- sa-token 的设计初衷是为了提供一个轻量级的认证框架,相比于 spring security 等重量级框架,sa-token 的使用更为简单直接;
- 上手简单
- 提供了简洁的 api 接口,使得开发者可以快速上手;
- 支持多种认证方式,如基于 token 的认证、基于 session 的认证等。
- 性能高
- 采用内存缓存来存储认证信息,减少了数据库的访问频率,提高了系统的性能。
- 安全性高
- 支持多种加密算法,如 sha256、hmac-sha256 等,确保了令牌的安全性;
- 支持令牌的过期时间设置,增强了安全性。
- 可扩展性好
- 提供了丰富的事件监听器,可以方便地扩展认证逻辑;
- 支持自定义认证处理器,可以根据需要定制认证流程。
- 社区支持
- 拥有一定的社区支持,可以通过官方文档、github 仓库等渠道获得帮助。
4.3 sa-token使用场景
sa-token基于自身的特点,具有丰富的使用场景,下面列举了一些常用的场景:
- 单体服务应用
- 对于一般的单体应用,sa-token 可作为一个快速的认证解决方案,它可以提供基于 token 的认证,易于集成到现有的项目中。
- 微服务架构
- 在微服务架构中,sa-token 可以作为一个轻量级的认证框架,用于处理服务间的认证和授权。它可以提供高性能的认证服务,并且易于扩展。
- api 服务
- 对于 restful api 或 graphql 服务,sa-token 可以提供基于 token 的认证机制,确保 api 请求的安全性。
- 管理后台系统
- 在管理后台系统中,sa-token 可以提供用户认证和权限控制,确保只有经过认证的用户才能访问管理界面。
- 用户权限系统
- sa-token 可以用于实现用户权限系统,支持角色和权限的管理。它可以提供基于角色的访问控制(rbac)功能,方便地管理用户权限。
sa-token 是一个轻量级的 java 认证框架,适用于需要简单认证逻辑的项目。它提供了易用的 api 和丰富的功能,可以轻松地集成到 java 项目中。如果你正在寻找一个简单、高效且易于使用的认证框架,sa-token 是一个不错的选择。
五、springboot集成sa-token
接下来,详细介绍下如何在springboot项目中集成sa-token
5.1 数据准备
5.1.1 创建认证和授权使用相关的数据表
为了后续模拟登录,认证和鉴权相关的功能,提前创建几张数据表并初始化一些数据,不难看出,这正是经典的rbac的权限模型,如下:
-- 用户表 create table `user` ( `user_id` int not null, `user_name` varchar(32) collate utf8mb4_bin default null, `password` varchar(32) collate utf8mb4_bin default null, `real_name` varchar(32) collate utf8mb4_bin default null, `dept_name` varchar(255) collate utf8mb4_bin default null, primary key (`user_id`) ) engine=innodb default charset=utf8mb4 collate=utf8mb4_bin; insert into `db_sky`.`user`(`user_id`, `user_name`, `password`, `real_name`, `dept_name`) values (1, 'zhangsan', '123456', '张三', '技术部'); insert into `db_sky`.`user`(`user_id`, `user_name`, `password`, `real_name`, `dept_name`) values (2, 'lishi', '123456', '李四', '测试部'); -- 角色表 create table `role` ( `role_id` int not null, `role_code` varchar(32) collate utf8mb4_bin not null, `role_name` varchar(32) collate utf8mb4_bin not null, primary key (`role_id`) ) engine=innodb default charset=utf8mb4 collate=utf8mb4_bin; insert into `db_sky`.`role`(`role_id`, `role_code`, `role_name`) values (1, 'admin', '管理员'); insert into `db_sky`.`role`(`role_id`, `role_code`, `role_name`) values (2, 'guest', '游客'); -- 权限表 create table `permission` ( `permission_id` int not null, `permission_code` varchar(32) collate utf8mb4_bin default null, primary key (`permission_id`) ) engine=innodb default charset=utf8mb4 collate=utf8mb4_bin; insert into `db_sky`.`permission`(`permission_id`, `permission_code`) values (1, 'user-read'); insert into `db_sky`.`permission`(`permission_id`, `permission_code`) values (2, 'user-write'); insert into `db_sky`.`permission`(`permission_id`, `permission_code`) values (3, 'product-read'); insert into `db_sky`.`permission`(`permission_id`, `permission_code`) values (4, 'product-write'); -- 用户角色关联表 create table `role_user` ( `id` int not null, `role_id` int default null, `user_id` int default null, primary key (`id`) ) engine=innodb default charset=utf8mb4 collate=utf8mb4_bin; insert into `db_sky`.`role_user`(`id`, `role_id`, `user_id`) values (1, 1, 1); insert into `db_sky`.`role_user`(`id`, `role_id`, `user_id`) values (2, 2, 1); insert into `db_sky`.`role_user`(`id`, `role_id`, `user_id`) values (3, 2, 2); -- 角色权限表 create table `role_permission` ( `id` int not null, `role_id` int default null, `permission_id` int default null, primary key (`id`) ) engine=innodb default charset=utf8mb4 collate=utf8mb4_bin; insert into `db_sky`.`role_permission`(`id`, `role_id`, `permission_id`) values (1, 1, 1); insert into `db_sky`.`role_permission`(`id`, `role_id`, `permission_id`) values (2, 1, 2); insert into `db_sky`.`role_permission`(`id`, `role_id`, `permission_id`) values (3, 1, 3); insert into `db_sky`.`role_permission`(`id`, `role_id`, `permission_id`) values (4, 1, 4); insert into `db_sky`.`role_permission`(`id`, `role_id`, `permission_id`) values (7, 2, 1); insert into `db_sky`.`role_permission`(`id`, `role_id`, `permission_id`) values (8, 2, 3);
5.2 搭建springboot工程
5.2.1 导入基础依赖
添加如下依赖,由于需要操作数据库,本例使用mybatis+mysql的方式,其他依赖可根据自身情况添加
<parent> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-parent</artifactid> <version>2.5.5</version> <relativepath/> </parent> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceencoding>utf-8</project.build.sourceencoding> </properties> <dependencies> <dependency> <groupid>com.alibaba</groupid> <artifactid>druid-spring-boot-starter</artifactid> <version>1.1.17</version> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-web</artifactid> </dependency> <!-- mybatis starter --> <dependency> <groupid>org.mybatis.spring.boot</groupid> <artifactid>mybatis-spring-boot-starter</artifactid> <version>2.2.2</version> </dependency> <dependency> <groupid>org.projectlombok</groupid> <artifactid>lombok</artifactid> <scope>provided</scope> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-jdbc</artifactid> </dependency> <dependency> <groupid>mysql</groupid> <artifactid>mysql-connector-java</artifactid> </dependency> <dependency> <groupid>cn.dev33</groupid> <artifactid>sa-token-spring-boot-starter</artifactid> <version>1.38.0</version> </dependency> </dependencies>
5.2.2 添加配置文件
yml配置文件中添加如下的配置内容,关于sa-token部分的配置文件,给出了详细的解释说明,更多的配置,可以参阅官网,根据自身的需要进行添加或参数的调整,可以参考:sa-token
server: port: 8081 spring: application: name: user-service datasource: url: jdbc:mysql://ip:3306/db_sky driverclassname: com.mysql.jdbc.driver username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml #目的是为了省略resulttype里的代码量 type-aliases-package: com.congge.entity configuration: log-impl: org.apache.ibatis.logging.stdout.stdoutimpl sa-token: #token的名称,也即cookie的名称 token-name: satoken #token的有效期(单位/秒),默认30天,-1代表永久有效 timeout: 2592000 #token最低活跃频率(单位/秒),如果token超过次时间没有访问系统就会被冻结,默认-1表示不限制 active-timeout: -1 #是否允许同一账号多地登录 (为true允许一起登录,为false表示新登录挤掉旧的) is-concurrent: true #在多人登录同一个账号的同时,是否共用一个token is-share: true #token的风格形式(可选值:uuid,simple-uuid,random-32,random-64,random-128,tik) token-style: uuid #是否输出日志 is-log: true
5.3 sa-token 登录认证原理
5.3.1 sa-token登录认证原理介绍
使用安全认证框架,一般来说主要是借助框架的能力做两点需求,认证,和授权,这也是实际做技术选型的核心考虑要素,认证,通常是通过登录的过程体现的,用户输入账号信息,sa-token介入这个过程,从而管理用户的会话生命周期,具体来说:
- 用户提交
name
+password
参数,调用登录接口; - 登录成功,返回这个用户的 token 会话凭证;
- 用户后续的每次请求,都携带上这个 token;
- 服务器根据 token 判断此会话是否登录成功;
所谓登录认证,指的就是服务器校验账号密码,为用户颁发 token 会话凭证的过程,这个 token 也是我们后续判断会话是否登录的关键所在。
5.3.2 sa-token登录登出核心api
在sa-token的api中,核心的登录api只有下面一句
// 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | string, 不可以传入复杂类型,如:user、admin 等等 stputil.login(object id);
只此一句代码,便可以使会话登录成功,实际上,sa-token 在背后做了大量的工作,包括但不限于:
- 检查此账号是否之前已有登录;
- 为账号生成
token
凭证与session
会话; - 记录 token 活跃时间;
- 通知全局侦听器,xx 账号登录成功;
- 将
token
注入到请求上下文; - 其他要处理的业务...
除了登录方法,在其api中,我们可能还需要下面的一些方法:
// 当前会话注销登录 stputil.logout(); // 获取当前会话是否已经登录,返回true=已登录,false=未登录 stputil.islogin(); // 检验当前会话是否已经登录, 如果未登录,则抛出异常:`notloginexception` stputil.checklogin();
5.4 sa-token登录认证代码演示
基于上面准备阶段的代码和配置,参考下面完整的登录认证流程。
5.4.1 增加token的配置类
该类用于拦截服务器api访问的请求,即系统中的接口均需要认证
import cn.dev33.satoken.filter.saservletfilter; import cn.dev33.satoken.interceptor.sainterceptor; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import org.springframework.web.servlet.config.annotation.interceptorregistry; import org.springframework.web.servlet.config.annotation.webmvcconfigurer; @configuration public class satokenconfigure implements webmvcconfigurer { /** * 注册sa-token 拦截器,打开注解鉴权的功能 * @param registry */ @override public void addinterceptors(interceptorregistry registry) { registry.addinterceptor(new sainterceptor()).addpathpatterns("/**"); } /** * 注册sa-token的全局过滤器 * @return */ @bean public saservletfilter getsaservletfilter(){ return new saservletfilter() .addinclude("/**"); } }
5.4.2 增加全局异常处理类
该类作为全局的异常处理器,这里主要是为了兜住与sa-token相关的业务异常
import cn.dev33.satoken.exception.disableserviceexception; import cn.dev33.satoken.exception.notloginexception; import cn.dev33.satoken.exception.notpermissionexception; import cn.dev33.satoken.exception.notroleexception; import org.springframework.http.httpstatus; import org.springframework.http.responseentity; import org.springframework.web.bind.annotation.controlleradvice; import org.springframework.web.bind.annotation.exceptionhandler; import org.springframework.web.bind.annotation.responsebody; import javax.servlet.http.httpservletrequest; import java.util.hashmap; import java.util.map; @controlleradvice public class globalbizexception { @responsebody @exceptionhandler public responseentity handleexception(exception e, httpservletrequest request) throws exception{ map<string,object> resultmap = new hashmap<>(); if(e instanceof notloginexception){ notloginexception exception = (notloginexception) e; resultmap.put("code",exception.getcode()); resultmap.put("message",exception.getmessage()); } else if(e instanceof notroleexception){ notroleexception exception = (notroleexception) e; resultmap.put("code",exception.getcode()); resultmap.put("message",exception.getmessage()); } else if(e instanceof notpermissionexception){ notpermissionexception exception = (notpermissionexception) e; resultmap.put("code",exception.getcode()); resultmap.put("message",exception.getmessage()); }else if(e instanceof disableserviceexception){ disableserviceexception exception = (disableserviceexception) e; resultmap.put("code",exception.getcode()); resultmap.put("message",exception.getmessage()); }else { resultmap.put("message",e.getmessage()); } return new responseentity<>(resultmap, httpstatus.internal_server_error); } }
5.4.3 登录登出接口
该类提供了3个接口,登录登出,以及获取token信息
import cn.dev33.satoken.stp.satokeninfo; import cn.dev33.satoken.stp.stputil; import com.congge.entity.user; import com.congge.service.userservice; import org.springframework.web.bind.annotation.getmapping; import org.springframework.web.bind.annotation.requestparam; import org.springframework.web.bind.annotation.restcontroller; import javax.annotation.resource; import java.util.objects; @restcontroller public class logincontroller { @resource private userservice userservice; //localhost:8081/login?name=zhangsan&pwd=123456 @getmapping("/login") public object login(@requestparam("name") string name, @requestparam("pwd") string pwd){ user dbuser = userservice.querybynameandpwd(name,pwd); if(objects.isnull(dbuser)){ throw new runtimeexception("用户名或密码错误"); } stputil.login(dbuser.getuserid()); return "login success"; } //localhost:8081/gettoken @getmapping("/gettoken") public object gettoken(){ satokeninfo tokeninfo = stputil.gettokeninfo(); return tokeninfo; } //localhost:8081/logout @getmapping("/logout") public object logout(){ stputil.logout(); return "logout success"; } }
5.4.4 获取用户信息接口
提供一个获取用户信息相关的接口,作为系统的api资源,后续验证对接口的访问必须要通过认证
import com.congge.entity.user; import com.congge.service.userservice; import org.springframework.beans.factory.annotation.autowired; import org.springframework.web.bind.annotation.getmapping; import org.springframework.web.bind.annotation.requestmapping; import org.springframework.web.bind.annotation.requestparam; import org.springframework.web.bind.annotation.restcontroller; @requestmapping("/user") @restcontroller public class usercontroller { @autowired private userservice userservice; //localhost:8081/user/detail?userid=1 @getmapping("/detail") public user getbyid(@requestparam integer userid){ return userservice.getbyid(userid); } }
5.4.5 接口测试与效果验证
1)登录接口测试
启动工程之后,首先调用登录接口:localhost:8081/login?name=zhangsan&pwd=123456,看到如下效果:
在上述的效果中,通过f12打开浏览器窗口,可以看到,登录成功之后,sa-token会通过接口响应向浏览器的cookie中写入一些会话相关的信息,比如:
- cookie中的token的名称;
- 会话中的token的过期时间;
2)获取用户信息接口测试
在登录的情况下,调用获取用户信息的接口,此时可以通过接口获取到用户的信息
f12查看请求信息时可以发现在本次的请求中,在请求的header中自动带上了cookie的信息,这样服务端就可以从请求中解析出token,进而做请求的拦截和解析等工作
3)登出接口测试
调用登出接口,可以看到,此时cookie的token就置为null
sa-token在控制台中,也输出了相关的登录登出的日志信息
关于登录认证相关的api使用,更详细的可以参考:sa-token
5.4.6 集成redis管理登录会话
事实上,在微服务的镇上的线上部署模式中,为了提升系统的并发和吞吐性能,某个服务可能会集群(多节点)部署,在这种情况下,如果不借助外部存储,会话信息就与当前的服务进程绑定了,想要扩展服务就成了问题,如何解决呢?业内通常是借助redis作为会话的存储容器,即不管部署了多少个微服务,大家会话信息的获取用的都是同一份存储在redis中的这个,这样就解决了这个问题,sa-token也是采用了这个思路,具体来说,和大家在日常项目开发中编码几乎类似,这里不再详细展开,提供官方的参考配置文档:sa-token
5.5 sa-token授权模型介绍
5.5.1 sa-token权限概述
一个功能体系完善的安全框架来说,认证和授权是必不可少的,在上面的操作演示中,体验了sa-token的认证功能,此外,sa-token还提供了鉴权,所谓鉴权,核心逻辑就是判断一个账号是否拥有指定权限:
- 有权限?就让你通过;
- 没有?那么禁止访问;
深入到底层数据结构,就是每个账号都会拥有一组权限码集合,框架来校验这个集合中是否包含指定的权限码。
例如:当前账号拥有权限码集合 ["user-add", "user-delete", "user-get"],这时候我来校验权限 "user-update",则其结果就是:验证失败,禁止访问。
5.5.2 sa-token权限核心api说明
了解了上面的理论,现在问题的核心就是两个:
- 如何获取一个账号所拥有的权限码集合?
- 本次操作需要验证的权限码是哪个?
因为不同的项目需求不同,权限设计的方式也千变万化,因此 [ 获取当前账号权限码集合 ] 这一操作不可能内置到框架中, 所以 sa-token 将此操作以接口的方式暴露给你,以方便你根据自己的业务逻辑进行重写。你需要做的就是新建一个类,实现 stpinterface接口,例如以下代码:
/** * 自定义权限加载接口实现类 */ @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; } }
核心参数说明:
- loginid:账号id,即你在调用 stputil.login(id) 时写入的标识值,比如在上文中,写入的是userid;
- logintype:账号体系标识,此处可以暂时忽略;
更详细的内容可以参阅文档:sa-token,下面通过案例的使用深入体会权限的使用。
5.6 sa-token授权代码演示
参考下面的步骤。
5.6.1 新增stpinterfaceimpl类
实现stpinterface接口后,该类会在项目启动时自动被框架识别并加入到容器中
import cn.dev33.satoken.stp.stpinterface; import com.congge.dao.usermapper; import org.springframework.stereotype.component; import javax.annotation.resource; import java.util.list; /** * 自定义权限加载接口实现类 * 保证此类被 springboot 扫描,完成 sa-token 的自定义权限验证扩展 */ @component public class stpinterfaceimpl implements stpinterface { @resource private usermapper usermapper; /** * 返回一个账号所拥有的权限码集合 */ @override public list<string> getpermissionlist(object loginid, string logintype) { system.out.println("获取权限列表..."); list<string> permissionlist = usermapper.getpermissionlist(integer.valueof(loginid.tostring())); return permissionlist; } /** * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验) */ @override public list<string> getrolelist(object loginid, string logintype) { // 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询角色 list<string> list = usermapper.getrolelist(integer.valueof(loginid.tostring())); return list; } }
代码中对应的两个获取权限和角色的sql如下:
<?xml version="1.0" encoding="utf-8"?> <!doctype mapper public "-//mybatis.org//dtd mapper 3.0//en" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.congge.dao.usermapper"> <resultmap id="userresultmap" type="com.congge.entity.user"> <id property="userid" column="user_id"/> <result property="username" column="user_name"/> <result property="password" column="password"/> <result property="realname" column="real_name"/> <result property="deptname" column="dept_name"/> </resultmap> <select id="getbyid" resultmap="userresultmap"> select * from `user` where user_id = #{userid} </select> <select id="querybynameandpwd" resultmap="userresultmap"> select * from `user` where user_name = #{name} and password = #{pwd} </select> <select id="getpermissionlist" resulttype="java.lang.string"> select distinct p.permission_code from role_user ru ,role_permission rp ,permission p where ru.role_id = rp.role_id and rp.permission_id = p.permission_id and ru.user_id = #{loginid} </select> <select id="getrolelist" resulttype="java.lang.string"> select distinct r.role_code from role r ,role_user ru where ru.role_id = r.role_id and ru.user_id =#{loginid} </select> </mapper>
5.6.2 用户接口改造
在本案例中提供了用户操作的接口类,通常来说,对于接口权限的控制,是要求登录的用户具备某些权限,或某种角色才允许访问,在sa-token中,提供了两个常用的用于控制用户访问权限的注解:
- @sacheckrole("admin"),校验接口访问的人必须具备的角色;
- @sacheckpermission("user:add"),权限校验登录的人必须具有指定权限才能访问;
事实上,sa-token提供的不仅有注解的方式,也有api的方式进行权限校验,更详细的可以参阅文档:sa-token
如下,是我们对用户操作的接口的权限改造
import cn.dev33.satoken.annotation.sacheckpermission; import cn.dev33.satoken.annotation.sacheckrole; import cn.dev33.satoken.annotation.samode; import com.congge.entity.user; import com.congge.service.userservice; import org.springframework.beans.factory.annotation.autowired; import org.springframework.web.bind.annotation.getmapping; import org.springframework.web.bind.annotation.requestmapping; import org.springframework.web.bind.annotation.requestparam; import org.springframework.web.bind.annotation.restcontroller; @requestmapping("/user") @restcontroller public class usercontroller { @autowired private userservice userservice; //localhost:8081/user/detail?userid=1 /** * 查看用户详情时,只需要拥有 user-read 这个权限即可 * @param userid * @return */ @getmapping("/detail") @sacheckpermission("user-read") public user getbyid(@requestparam integer userid){ return userservice.getbyid(userid); } //localhost:8081/user/create /** * 创建用户时,需要在user-write 和 write中任意拥有一个即可 * @return */ @getmapping("/create") @sacheckpermission(value = {"user-write","write"},mode = samode.or) public object createuser(){ return "create user success"; } //localhost:8081/user/delete /** * 删除用户时,必须拥有admin这个角色的才行 * @return */ @sacheckrole("admin") @getmapping("/delete") public object deleteuser(){ return "delete user success"; } }
接口说明:
- 获取用户详情接口,只需要拥有 user-read 这个权限即可访问;
- 创建用户接口,需要在user-write 和 write中任意拥有一个即可;
- 删除用户接口,必须具备admin的角色才能删除;
5.6.3 接口权限效果测试
首先,在数据库中,zhangsan这和用户具有admin的角色,先以这个账号进行测试
1)登录接口
未登录效果
调用登录接口
2)再次获取用户详情接口
3)调用删除接口
zhangsan这个用户拥有admin的角色,理论上可以删除,调用接口,可以删除成功
4)退出当前登录使用另一个账户登录
使用lishi这个账户登录
5)调用删除接口
由于lishi这个账户没有admin的角色,理论上会删除失败,调用接口后可以看到下面的效果
六、写在文末
本文详细介绍了sa-token这款安全框架的使用,并结合实际操作演示了如何集成到springboot项目中,可以说作为最近几年慢慢火起来的安全框架,相对于其他的安全框架,sa-token的优势已经凸显出来了,作为一个技术选型,在今后的项目开发或架构设计中也是一个不错的选择,本篇到此结束,感谢观看。
到此这篇关于springboot 整合 sa-token 使用详解的文章就介绍到这了,更多相关springboot 整合 sa-token内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论