赞
踩
Sa-Token 是一个轻量级 Java 权限认证框架
主要解决:登录认证
、权限认证
、Session会话
、单点登录
、OAuth2.0
、微服务网关鉴权
等一系列权限相关问题。
Sa-Token 特点是以简单、优雅的方式完成系统的权限认证部分,如果你学过SpringSecurity 、Shiro之后,你就会明白,相对于这些传统老牌框架,Sa-Token 的 API 设计是多么的简单、优雅!
在Sa-Token中,绝大多数功能都能只用一行代码解决,例如:
登录认证:
StpUtil.login(Account)
踢人下线
StpUtil.kickout(Account)
权限认证
@SaCheckPermission("user:add")
public String insert(SysUser user) {
return "用户增加";
}
Sa-Token 目前主要五大功能模块:登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。
pom依赖:
<!-- Sa-Token 权限认证 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.34.0</version>
</dependency>
更多内测版本了解:Sa-Token最新版本
配置文件,这里只推荐yml风格,其他风格访问 在 SpringBoot 环境集成 (sa-token.cc) 查看:
server: # 端口 port: 8081 sa-token: # token名称 (同时也是cookie名称) token-name: satoken # token有效期,单位s 默认30天, -1代表永不过期 timeout: 2592000 # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒 activity-timeout: -1 # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) is-share: true # token风格 token-style: uuid # 是否输出操作日志 is-log: false
过程:拦截器 ==> 登录认证 ==> 权限验证 ==>
接下来我会正常的构建一个登录:
一般项目中都会有这样一个需求,在没有登陆的情况下,仅允许访问登录的接口,只有在登陆之后才可以访问其他的接口,那么此时就需要用到路由拦截了
注册Sa-Token拦截器
新建一个配置类,实现 WebMvcConfigurer:
// 注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
//拦截的路径
.addPathPatterns("/**")
//开放的登录接口,除此之外的接口都需要登陆才可以访问
.excludePathPatterns("/login");
}
这里注册了一个基于
StpUtil.checkLogin()
的登录校验拦截器,进行测试就会发现只有通过login接口登录成功后,才能访问其他接口那么如果我们想自定义一个认证规则怎么写:
@Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器,定义详细认证规则 registry.addInterceptor(new SaInterceptor(handler -> { // 指定一条 match 规则 SaRouter .match("/**") // 拦截的 path 列表,可以写多个 */ .notMatch("/login") // 排除掉的 path 列表,可以写多个 .check(r -> StpUtil.checkLogin()); // 要执行的校验动作,可以写完整的 lambda 表达式 // 根据路由划分模块,不同模块不同鉴权 //SaRouter.match() 匹配函数有两个参数 参数一:要匹配的path路由 // 参数二:要执行的校验函数。 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.cc)
额… 看了一圈,好像是没有密码校验这一功能的(也可能是没找到),但是有密码加密,所以这里的代码就自己写一下校验了
@Autowired private LoginService loginService; @RequestMapping("/login") protected String login(String acc,String pwd) { //查询数据库中的账号 User user = loginService.getUser(acc); //判断账号是否存在 //密码是否相同(加密后) if (user != null && SaSecureUtil.md5(pwd).equals(user.getPwd()) ){ StpUtil.login(acc); return "登录成功"; } return "登录失败"; }
常用的密码加密,更多密码加密方式 密码加密 (sa-token.cc) :
// md5加密
SaSecureUtil.md5(“123456”);// sha1加密
SaSecureUtil.sha1(“123456”);// sha256加密
SaSecureUtil.sha256(“123456”);
这里我们再写一个 查看登陆状态 的接口测试一下我们拦截器和登录的效果:
@RequestMapping("/isLogin")
public String isLogin() {
return "当前会话是否登录:" + StpUtil.isLogin();
}
此时我们访问
isLogin
接口:HTTP Status 500 – Internal Server Error.访问
login
接口,并携带 acc 和 pwd:登录成功.再访问
isLogin
接口:当前会话是否登录:true.
简单分析一下登录访问流程:
name
+ password
参数,调用登录接口。我们暂时不需要完整的了解整个登录过程,只需要记住关键一点:Sa-Token 为这个账号创建了一个Token凭证,且通过 Cookie 上下文返回给了前端
。
此处仅仅做了会话登录,但并没有主动向前端返回 Token 信息。 是因为不需要吗?严格来讲是需要的,只不过 StpUtil.login()
方法利用了 Cookie 自动注入的特性,省略了手写返回 Token 的代码。
至此,简单的登录认证就已经完成了,更多相关功能比如:退出登录、注销登录、踢人下线等可以访问 StpUtil-鉴权工具类 (sa-token.cc) 查看
首先我们要做的就是去数据库中,把当前用户的权限、角色数据给获取出来,这一步就使用 假数据来代替了
那么现在应该做的就是实现赋权:新建一个类,实现 StpInterface
接口,重写其中 获取权限列表
获取角色列表
两个方法
@Component public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合 */ @Override public List<String> getPermissionList(Object o, String s) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限 List<String> list = new ArrayList<String>(); list.add("101"); list.add("user.add"); list.add("user.update"); list.add("art.*"); return list; } /** * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验) */ @Override public List<String> getRoleList(Object o, String s) { // 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色 List<String> list = new ArrayList<String>(); list.add("admin"); list.add("super-admin"); return list; } }
写一个接口做简单测试:
@RequestMapping("/PermissionsCheck")
public void PermissionsCheck() {
//返回类型boolean
System.out.println("是否拥有权限user.add:" + StpUtil.hasPermission("user.add"));
//返回类型List
System.out.println("当前账号拥有的所有权限: " + StpUtil.getPermissionList());
System.out.println("是否拥有角色admin:" + StpUtil.hasRole("admin"));
System.out.println("当前账号拥有的所有角色: " + StpUtil.getRoleList());
}
学过 Shori 和 SpringSecurity 知道他们都可以使用注解鉴权,这里当然也是可以使用注解鉴权的
写一个接口,使用注解 @SaCheckRole("admin")
检验角色:
@RequestMapping("/role")
@SaCheckRole("admin")
public String role() {
return ("角色匹配,允许访问");
}
还有其他的注解,比如:
@SaIgnore
如果你的拦截拦截器里面写的是/**拦截所有,那么这个注解就能使被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权
@SaCheckPermission("user:add")
权限校验,不需要多解释
更多注解查看 注解鉴权 (sa-token.cc)
使用拦截器模式,只能在Controller层
进行注解鉴权,如需在任意层级使用注解鉴权,请参考:AOP注解鉴权
经过上面的测试,权限正确能够正常的访问接口,但是当权限不够时会报出 NotRoleException
,此时我们可以创建一个全局异常拦截器,统一返回给前端
@RestControllerAdvice public class GlobalException { // 拦截:未登录异常 @ExceptionHandler(NotLoginException.class) public SaResult handlerException(NotLoginException e) { // 打印堆栈,以供调试 e.printStackTrace(); // 返回给前端 return SaResult.error(e.getMessage()); } // 拦截:缺少权限异常 @ExceptionHandler(NotPermissionException.class) public SaResult handlerException(NotPermissionException e) { e.printStackTrace(); return SaResult.error("缺少权限:" + e.getPermission()); } // 拦截:缺少角色异常 @ExceptionHandler(NotRoleException.class) public SaResult handlerException(NotRoleException e) { e.printStackTrace(); return SaResult.error("缺少角色:" + e.getRole()); } // 拦截:二级认证校验失败异常 @ExceptionHandler(NotSafeException.class) public SaResult handlerException(NotSafeException e) { e.printStackTrace(); return SaResult.error("二级认证校验失败:" + e.getService()); } // 拦截:服务封禁异常 @ExceptionHandler(DisableServiceException.class) public SaResult handlerException(DisableServiceException e) { e.printStackTrace(); return SaResult.error("当前账号 " + e.getService() + " 服务已被封禁 (level=" + e.getLevel() + "):" + e.getDisableTime() + "秒后解封"); } // 拦截:Http Basic 校验失败异常 @ExceptionHandler(NotBasicAuthException.class) public SaResult handlerException(NotBasicAuthException e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } // 拦截:其它所有异常 @ExceptionHandler(Exception.class) public SaResult handlerException(Exception e) { e.printStackTrace(); return SaResult.error(e.getMessage()); } }
Sa-Token 提供了扩展接口,可以轻松将会话数据存储在 Redis
、Memcached
等专业的缓存中间件中, 做到重启数据不丢失,而且保证分布式环境下多节点的会话一致性。
依赖:
<!-- Sa-Token 整合 Redis (使用 jdk 默认序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis</artifactId>
<version>1.34.0</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
yml配置:
spring: # redis配置 redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: **.***.**.*** # Redis服务器连接端口 port: 6379 # Redis服务器连接密码(默认为空) password: ****** # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0
接下来…
没有接下来了,因为集成 Redis 后,框架自动保存。集成
Redis
只需要引入对应的pom依赖
即可,框架所有上层 API 保持不变。非常好用
从上面的认证我们可以得知,Sa-Token为当前账号创建了一个Token凭证,且通过 Cookie 上下文返回给了前端
其实这里sa-token做了两步操作:
- 后端控制写入 Cookie
- 每次请求自动携带 Cookie
那么如果在一些场景下后端没办法控制 Cookie 的写入了怎么办?
这些情况还是比较常见的,比如:
1. 小程序不支持 Cookie
1. App端支持 Cookie 但是不支持自动保持
1. 浏览器禁用了 Cookie 的情况
当使用非 H5 端协议(例如HTTP)时,服务器可以通过发送Set-Cookie头部来让客户端保存Cookie。然而,客户端不会自动将这些Cookie保存下来并自动发送回服务器。相反,客户端应该手动管理Cookie,并在需要时将它们添加到请求中发送给服务器。这意味着客户端需要使用编程语言和技术来处理和管理Cookie,而不是依赖于自动化的行为。
当出现这些情况时,我们就需要
继续在登录代码的基础上修改:
...
if (user != null && SaSecureUtil.md5(pwd).equals(user.getPwd())){
//登录
StpUtil.login(acc);
//取Token
String tokenValue = StpUtil.getTokenInfo().tokenValue
//这里直接返回token值
return tokenValue;
}
return "登录失败";
...
这里以 uni-app 举例
//将返回的 tokenValue 存起来,uni-app可以存到本地里:
uni.setStorageSync('tokenValue', tokenValue);
uni.request({
url: 'http://localhost:8081/Course/getData',
data: {},
header: {
'tokenValue': uni.getStorageSync('tokenValue')
},
success: (res) => {
}
});
将这些代码封装起来就简单又好用了
这里有一点需要注意的是: ‘tokenValue’ 这个名字,token 的键名必须要是这一个,否则sa-token没办法自动读取到 token 值,进行鉴权
键名从哪里得知,其实
StpUtil.getTokenInfo()
获取出来的是一个 SaTokenInfo 对象,看一下就知道了
官方文档
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。