一、SaToken简介
Sa-Token是一个轻量级Java权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题。SaToken官方文档非常详尽,大家可以按照手册的指引可以很轻松的把SaToken整合到SpringBoot项目中。
二、认证与授权
颁发令牌(Token)
SaToken框架只会在两种业务场景中用到:登陆与访问。先请大家来看一下登录场景是怎么使用SaToken框架的。当用户登陆的时候,经过Java项目对登陆用户名和密码的核对,允许用户登陆系统。这时候需要我们调用SaToken的工具类(StpUtil)创建令牌字符串(Token)。SaToken生成的令牌字符串会被缓存到Redis中,接下来Web方法会把这个令牌字符串写到Http响应返回给客户端。
Token是一个加密的字符串,里面包含了userId、过期时间和随机数等等,它是用户登陆成功的凭证。如果客户端的令牌被别人盗取了,那么其他人就可以冒充该用户了。为了避免令牌在网络传输过程中被盗取,我们可以采用HTTPS协议,这样别人就无法窃取数据了。后续我们会为Java项目配置HTTPS,大家尽可放心。
在SaToken框架中,颁发令牌非常简单,只需要调用StpUtil
工具类的方法即可。我们需要向SaToken会话对象提供当前用户的userId,然后SaToken才可以生成Token令牌。换而言之,如果将来我们拿到用户的令牌,SaToken可以反向解析出用户的userId,我们就能知道是哪个用户访问的Web方法。
//向当前SaToken会话对象传递用户ID,只有提供了用户ID才能生成令牌。
StpUtil.login(userId);
//生成令牌字符串
String token = StpUtil.getTokenValue();
验证令牌
用户成功登陆系统之后,客户端每次访问Web方法的时候,必须要上传Token令牌。如果不上传令牌,Java项目可以认定用户没有登陆系统,所以拒绝客户端访问Web方法。即便客户端提交了令牌,SaToken还是需要认真检查令牌的真伪。如果无法从令牌中反向解析出用户ID和令牌过期时间,那么就可以认定令牌是伪造的,SaToken则拒绝客户端访问Web方法。
假如遇上厉害的黑客破解了SaToken生成令牌的算法,他用这套算法自己生成了一个令牌,是不是能骗过SaToken系统呢?也不行。因为SaToken生成的每个令牌在Redis中都有存档,所以黑客手上的令牌,SaToken是不认的。这也就是为什么上图中第三个步骤需要调用Redis的原因。
其实也不是所有的Web方法或者HTML页面都需要用户登陆之后才能访问,比如登陆页面和对应的后端Web方法。但是有些Web方法必须用户登陆之后才能访问,我们可以给Web方法添加@SaCheckLogin
注解。这个注解会拦截Web方法的请求,让SaToken验证客户端提交的Token令牌。如果令牌合法就允许调用Web方法,反之就拒绝HTTP请求,返回401状态码。
@RestController
@RequestMapping("/mis/user")
public class UserController {
……
@GetMapping("/searchUserSummary")
@SaCheckLogin
public R searchUserSummary() {
……
}
}
权限验证
对于业务端来说,所有登陆的用户都是客户角色,身份上完全一致,所以业务端对应的Web方法只需要验证用户是否登陆即可,不需要做权限判定。然而使用登录的用户,角色上并不统一,有的是超级管理员,有的是部门经理,甚至还有普通员工。登录的WEB方法既要验证用户是否登陆,还要判定是否拥有特定的权限。如果不具备相关的权限,SaToken就拒绝客户端访问Web方法。
三、编写鉴权类
鉴权类是需要我们自己实现的,必须要扩展StpInterface
接口才可以。在这里博主做个演示,创建StpInterfaceImpl.java
类。
package com.example.his.api.config.sa_token;
import cn.dev33.satoken.stp.StpInterface;
import com.example.his.api.db.dao.UserDao;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;@Component
public class StpInterfaceImpl implements StpInterface {
@Resource
private UserDao userDao;/**
* 返回一个用户所拥有的权限集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
List<String> list = new ArrayList<>();
int userId = Integer.parseInt(loginId.toString());
Set<String> set = userDao.searchUserPermissions(userId);
list.addAll(set);
return list;
}
/**
* 返回一个用户所拥有的角色标识集合
*/
@Override
public List<String> getRoleList(Object loginId, String loginKey) {
ArrayList<String> list = new ArrayList();
return list;}
}
四、为什么不通过角色鉴权?
由于登录系统中的权限是固定的,而角色是可以动态增减的。如果我们用@SaCheckRole
注解,规定访问Web方法的用户必须具备什么角色。如果将来该角色被删除掉,难道我们还要修改大量的Web方法注解吗?就像下面的Web方法,如果角色1被删除,难不成还要让程序员重新修改代码,重新打包程序,重新发布项目吗?
@RestController("MisAppointmentController")
@RequestMapping("/mis/appointment")
public class AppointmentController {
@Resource
private AppointmentService appointmentService;@PostMapping("/searchByPage")
@SaCheckRole(value = {"超级管理员", "部门经理", "角色1"}, mode = SaMode.OR)
public R searchByPage(@RequestBody @Valid SearchAppointmentByPageForm form) {
……
}
}
这么看下来,鉴权类中的getRoleList()
函数返回空的ArrayList对象就能说的通了。我们根本不需要验证用户的角色,干嘛要去查询用户的角色呢?
五、为何要对令牌续期?
只要用户成功登陆系统就会拿到Token令牌,但是令牌并不是永久有效的,它存在过期时间。假设令牌的过期时间是30天,用户在这30天内,都可以免于登陆直接使用登录系统。不管用户在有效期内使用登录系统多少次,只要过了30天,用户就必须要登陆一次,拿到新的令牌才可以继续使用登录系统。例如163邮箱和慕课网都是这种令牌过期时间的策略,过期就必须重新登陆。
从用户的角度来看,我一直使用登录系统,令牌就应该一直续期,除非我长期不用,令牌才可以过期。为了满足用户的需求,提升网站的满意度,我们可以给令牌自动续期,恰好SaToken也具备这个功能。
六、令牌如何续期?
SaToken令牌续期的过程比较复杂,所以我使用推演的方式,咱们逐步理解令牌续期的完整过程。
有效期和临时有效期
介绍令牌续期原理之前,我们先确定两个时间概念:有效期(timeout)和临时有效期(activity-timeout)。临时有效期很容易解释,只要用户访问登录系统的间隔时间不超过临时有效期,令牌就一直有效;有效期指的是即便用户不经常访问登录系统,只要在有效期以内,令牌就依然有效。
使用令牌的间隔时间
还记得SaToken生成的Token会缓存到Redis里面一份吗?令牌续期跟这个Redis缓存有很大的关系。缓存技术用的实在太巧妙了。执行@SaCheckLogin
和@SaCheckPermission
注解都会向Redis中记录使用的时刻。在向Redis中保存当前时刻之前,SaToken框架会用当前时刻减去Redis中保存的时刻,这就是该Token令牌被使用的间隔时间。大家记住这个间隔时间,非常关键。
何时做令牌续期?
如果当前时刻与有效期之间的差距小于临时有效期,SaToken就会查看Token令牌前后间隔时间是不是小于临时有效期。如果是,那就对令牌续期。如果不是,就不对令牌续期。上面这段话不太容易理解,我举个例子。比如说令牌有效期是30天,临时有效期是3天。从用户登陆成功开始算起,现在是第25天,用户访问了登录系统,我们完全不用考虑续期的问题。因为临时有效期是3天,在第27天之前,即便SaToken给令牌续期,过期时间依然也没有超过第30天,你觉得有必要续期吗?当然没必要。在第27天~30天之间,如果用户访问了登录系统,SaToken就必须考虑令牌续期的问题了。检查一下Token使用的间隔时间,在超过了临时有效期之内,就可以给令牌续期3天;反之就不续期。
续期产生新令牌吗?
Token续期既可以生成新令牌,也可以不生成新令牌,这个要看认证与授权框架是怎么设计的。比如SaToken的自动续期是不会产生新令牌的。也许有的同学还记得我说过令牌字符串中包含了加密过的过期时间。如果SaToken对令牌续期,应该生成新的令牌字符串,才能体现新的过期时间,原有的令牌怎么能体现是续过期的呢?答案很简单,SaToken对Redis缓存做了手脚:延长了Redis销毁该令牌的过期时间。
按照上图的流程,即便SaToken发现令牌中的过期时间早就到了,但是有可能这个令牌是续过期的。于是SaToken必须到Redis中检查该令牌是否存在缓存记录。如果还存在,说明该令牌是续过期的,而且还没有超期,令牌是有效的。
应该续期多少天
刚才我举例子的时候说到了,SaToken对令牌续期3天,而不是30天。那么为什么不续期30天呢?其实很简单,用户长期持有固定的令牌不安全。如果续期令牌,还是续期3天为妙,你觉得呢?
七、配置YML文件
在application.yml
文件中,填入SaToken的配置信息,如下:
sa-token:
#HTTP请求头中哪个属性用来上传令牌
token-name: token
#过期时间(秒),设置为30天
timeout: 2592000
#临时有效期,设置为3天
activity-timeout: 259200
#不允许相同账号同时在线,新登陆的账号会挤掉原来登陆的账号
allow-concurrent-login: false
#在多人登陆相同账号的时候,是否使用相同的Token
is-share: false
token-style: uuid
#是否读取Cookie中的令牌
isReadCookie: false
#同端互斥
isConcurrent: false
#SaToken缓存令牌用其他的逻辑库,避免业务数据和令牌数据共用相同的Redis逻辑库
alone-redis:
database: 1
host: localhost
port: 6379
password: abc123456
timeout: 10s
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: 10s
# 连接池中的最大空闲连接
max-idle: 16
# 连接池中的最小空闲连接
min-idle: 8
八、创建自签名数字证书
也许大家觉得前后端项目都部署在同一台服务器主机上面,前后端项目之间不需要使用HTTPS协议,其实不然。如果前后端项目都部署在同一台主机上面,该主机的负载会偏大,所以通常的做法是把前后端项目部署在不同主机上面。这种情况下,局域网之内的数据传输也应该加密,所以给后端项目配置HTTPS协议是必要的。
开通HTTPS协议需要使用数字证书,然而申请数字证书需要有公网域名。目前对于一些处在开发阶段的项目,我们可以自己创建一个数字证书,开通HTTPS协议。将来我们的项目上线的时候,再去申请正式的数字证书就行了。
命令就不在这里演示啦,大家想要学习的话,就发私信联系博主!博主带领大家一步一步在开发项目中实现https数字证书来保证安全性问题。