文章目录
框架介绍
轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题。
Demo 示例
创建一个 Spring 3 + 的 工程,引入依赖
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.41.0</version>
</dependency>
创建一个Controller
@RestController
@RequestMapping("/user/")
public class UserController {
// 测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456
@RequestMapping("doLogin")
public String doLogin(String username, String password) {
// 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
if("zhang".equals(username) && "123456".equals(password)) {
StpUtil.login(10001);
return "登录成功";
}
return "登录失败";
}
// 查询登录状态,浏览器访问: http://localhost:8081/user/isLogin
@RequestMapping("isLogin")
public String isLogin() {
return "当前会话是否登录:" + StpUtil.isLogin();
}
}
执行结果:


登录认证
登录与注销方法
// 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等
StpUtil.login(Object id);
官网说,这个函数的流程大概是:
- 检查账号是否已登录 (使用这个函数说明用户名密码比对正确)
- 生成Token 与 Session 会话
- 记录Token活跃时间
- 通知全局侦听器,xx账号登陆成功
- 将Token请求注入请求上下文等
但是我们不需要了解,只需要记住 : Sa-Token 为这个账号创建了Token,并且通过Cookie返回给前端。
登陆接口Demo
// 会话登录接口
@RequestMapping("doLogin")
public SaResult doLogin(String name, String pwd) {
// 第一步:比对前端提交的账号名称、密码
if("zhang".equals(name) && "123456".equals(pwd)) {
// 第二步:根据账号id,进行登录
StpUtil.login(10001);
return SaResult.ok("登录成功");
}
return SaResult.error("登录失败");
}
官网说,这里的login函数利用了 Cookie自动注入的特性,省略了手写token返回代码的过程。
然后给出了一个Cookie要点:
- Cookie 可以从后端向浏览器中写入 token 值
- Cookie 会在前端每次发起请求时 自动提交 token值

除此之外还有一些方法
// 当前会话注销登录
StpUtil.logout();
// 获取当前会话是否已经登录,返回true=已登录,false=未登录
StpUtil.isLogin();
// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.checkLogin();
会话查询
// 获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.getLoginId();
// 类似查询API还有:
StpUtil.getLoginIdAsString(); // 获取当前会话账号id, 并转化为`String`类型
StpUtil.getLoginIdAsInt(); // 获取当前会话账号id, 并转化为`int`类型
StpUtil.getLoginIdAsLong(); // 获取当前会话账号id, 并转化为`long`类型
// ---------- 指定未登录情形下返回的默认值 ----------
// 获取当前会话账号id, 如果未登录,则返回 null
StpUtil.getLoginIdDefaultNull();
// 获取当前会话账号id, 如果未登录,则返回默认值 (`defaultValue`可以为任意类型)
StpUtil.getLoginId(T defaultValue);
token查询
// 获取当前会话的 token 值
StpUtil.getTokenValue();
// 获取当前`StpLogic`的 token 名称
StpUtil.getTokenName();
// 获取指定 token 对应的账号id,如果未登录,则返回 null
StpUtil.getLoginIdByToken(String tokenValue);
// 获取当前会话剩余有效期(单位:s,返回-1代表永久有效)
StpUtil.getTokenTimeout();
// 获取当前会话的 token 信息参数
StpUtil.getTokenInfo();
// Sa-TokenInfo
{
"code": 200,
"msg": "ok",
"data": {
"tokenName": "satoken", // token名称
"tokenValue": "e67b99f1-3d7a-4a8d-bb2f-e888a0805633", // token值
"isLogin": true, // 此token是否已经登录
"loginId": "10001", // 此token对应的LoginId,未登录时为null
"loginType": "login", // 账号类型标识
"tokenTimeout": 2591977, // token剩余有效期 (单位: 秒)
"sessionTimeout": 2591977, // Account-Session剩余有效时间 (单位: 秒)
"tokenSessionTimeout": -2, // Token-Session剩余有效时间 (单位: 秒) (-2表示系统中不存在这个缓存)
"tokenActiveTimeout": -1, // token 距离被冻结还剩的时间 (单位: 秒)
"loginDevice": "DEF" // 登录设备类型
},
}
Demo
/**
* 登录测试
*/
@RestController
@RequestMapping("/acc/")
public class LoginController {
// 测试登录 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456
@RequestMapping("doLogin")
public SaResult doLogin(String name, String pwd) {
// 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
if("zhang".equals(name) && "123456".equals(pwd)) {
StpUtil.login(10001);
return SaResult.ok("登录成功");
}
return SaResult.error("登录失败");
}
// 查询登录状态 ---- http://localhost:8081/acc/isLogin
@RequestMapping("isLogin")
public SaResult isLogin() {
return SaResult.ok("是否登录:" + StpUtil.isLogin());
}
// 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo
@RequestMapping("tokenInfo")
public SaResult tokenInfo() {
return SaResult.data(StpUtil.getTokenInfo());
}
// 测试注销 ---- http://localhost:8081/acc/logout
@RequestMapping("logout")
public SaResult logout() {
StpUtil.logout();
return SaResult.ok();
}
}
权限认证
获取当前权限码集合
由于每个项目的权限设计不同,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;
}
}
权限校验
// 获取:当前账号所拥有的权限集合
StpUtil.getPermissionList();
// 判断:当前账号是否含有指定权限, 返回 true 或 false
StpUtil.hasPermission("user.add");
// 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException
StpUtil.checkPermission("user.add");
// 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过]
StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get");
// 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
StpUtil.checkPermissionOr("user.add", "user.delete", "user.get");
NotPermissionException 对象可通过 getLoginType() 方法获取具体是哪个 StpLogic 抛出的异常
角色校验
// 获取:当前账号所拥有的角色集合
StpUtil.getRoleList();
// 判断:当前账号是否拥有指定角色, 返回 true 或 false
StpUtil.hasRole("super-admin");
// 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
StpUtil.checkRole("super-admin");
// 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
StpUtil.checkRoleAnd("super-admin", "shop-admin");
// 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可]
StpUtil.checkRoleOr("super-admin", "shop-admin");
NotRoleException 对象可通过 getLoginType() 方法获取具体是哪个 StpLogic 抛出的异常
拦截全局异常
SpringMVC中的全局异常处理器 Demo
@RestControllerAdvice
public class GlobalExceptionHandler {
// 全局异常拦截
@ExceptionHandler
public SaResult handlerException(Exception e) {
e.printStackTrace(); //获取异常信息
return SaResult.error(e.getMessage()); // 返回结果
}
}
SaResult 返回结果封装类源码
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.util;
import cn.dev33.satoken.SaManager;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 对请求接口返回 Json 格式数据的简易封装。
*
* <p>
* 所有预留字段:<br>
* code = 状态码 <br>
* msg = 描述信息 <br>
* data = 携带对象 <br>
* </p>
*
* @author click33
* @since 1.22.0
*/
public class SaResult extends LinkedHashMap<String, Object> implements Serializable{
// 序列化版本号
private static final long serialVersionUID = 1L;
// 预定的状态码
public static final int CODE_SUCCESS = 200;
public static final int CODE_ERROR = 500;
/**
* 构建
*/
public SaResult() {
}
/**
* 构建
* @param code 状态码
* @param msg 信息
* @param data 数据
*/
public SaResult(int code, String msg, Object data) {
this.setCode(code);
this.setMsg(msg);
this.setData(data);
}
/**
* 根据 Map 快速构建
* @param map /
*/
public SaResult(Map<String, ?> map) {
this.setMap(map);
}
/**
* 获取code
* @return code
*/
public Integer getCode() {
return (Integer)this.get("code");
}
/**
* 获取msg
* @return msg
*/
public String getMsg() {
return (String)this.get("msg");
}
/**
* 获取data
* @return data
*/
public Object getData() {
return this.get("data");
}
/**
* 给code赋值,连缀风格
* @param code code
* @return 对象自身
*/
public SaResult setCode(int code) {
this.put("code", code);
return this;
}
/**
* 给msg赋值,连缀风格
* @param msg msg
* @return 对象自身
*/
public SaResult setMsg(String msg) {
this.put("msg", msg);
return this;
}
/**
* 给data赋值,连缀风格
* @param data data
* @return 对象自身
*/
public SaResult setData(Object data) {
this.put("data", data);
return this;
}
/**
* 写入一个值 自定义key, 连缀风格
* @param key key
* @param data data
* @return 对象自身
*/
public SaResult set(String key, Object data) {
this.put(key, data);
return this;
}
/**
* 获取一个值 根据自定义key
* @param <T> 要转换为的类型
* @param key key
* @param cs 要转换为的类型
* @return 值
*/
public <T> T get(String key, Class<T> cs) {
return SaFoxUtil.getValueByType(get(key), cs);
}
/**
* 写入一个Map, 连缀风格
* @param map map
* @return 对象自身
*/
public SaResult setMap(Map<String, ?> map) {
for (String key : map.keySet()) {
this.put(key, map.get(key));
}
return this;
}
/**
* 写入一个 json 字符串, 连缀风格
* @param jsonString json 字符串
* @return 对象自身
*/
public SaResult setJsonString(String jsonString) {
Map<String, Object> map = SaManager.getSaJsonTemplate().jsonToMap(jsonString);
return setMap(map);
}
/**
* 移除默认属性(code、msg、data), 连缀风格
* @return 对象自身
*/
public SaResult removeDefaultFields() {
this.remove("code");
this.remove("msg");
this.remove("data");
return this;
}
/**
* 移除非默认属性(code、msg、data), 连缀风格
* @return 对象自身
*/
public SaResult removeNonDefaultFields() {
for (String key : this.keySet()) {
if("code".equals(key) || "msg".equals(key) || "data".equals(key)) {
continue;
}
this.remove(key);
}
return this;
}
// ============================ 静态方法快速构建 ==================================
// 构建成功
public static SaResult ok() {
return new SaResult(CODE_SUCCESS, "ok", null);
}
public static SaResult ok(String msg) {
return new SaResult(CODE_SUCCESS, msg, null);
}
public static SaResult code(int code) {
return new SaResult(code, null, null);
}
public static SaResult data(Object data) {
return new SaResult(CODE_SUCCESS, "ok", data);
}
// 构建失败
public static SaResult error() {
return new SaResult(CODE_ERROR, "error", null);
}
public static SaResult error(String msg) {
return new SaResult(CODE_ERROR, msg, null);
}
// 构建指定状态码
public static SaResult get(int code, String msg, Object data) {
return new SaResult(code, msg, data);
}
// 构建一个空的
public static SaResult empty() {
return new SaResult();
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "{"
+ "\"code\": " + this.getCode()
+ ", \"msg\": " + transValue(this.getMsg())
+ ", \"data\": " + transValue(this.getData())
+ "}";
}
/**
* 转换 value 值:
* 如果 value 值属于 String 类型,则在前后补上引号
* 如果 value 值属于其它类型,则原样返回
*
* @param value 具体要操作的值
* @return 转换后的值
*/
private String transValue(Object value) {
if(value == null) {
return null;
}
if(value instanceof String) {
return "\"" + value + "\"";
}
return String.valueOf(value);
}
}
权限通配符
当一个账号拥有art.*的权限时,art.add、art.delete、art.update都将匹配通过
当一个账号拥有 “*” 权限时,他可以验证通过任何权限码 (角色认证同理)
// 当拥有 art.* 权限时
StpUtil.hasPermission("art.add"); // true
StpUtil.hasPermission("art.update"); // true
StpUtil.hasPermission("goods.add"); // false
// 当拥有 *.delete 权限时
StpUtil.hasPermission("art.delete"); // true
StpUtil.hasPermission("user.delete"); // true
StpUtil.hasPermission("user.update"); // false
// 当拥有 *.js 权限时
StpUtil.hasPermission("index.js"); // true
StpUtil.hasPermission("index.css"); // false
StpUtil.hasPermission("index.html"); // false
如何将权限精确到按钮级
如此精确的范围控制只依赖后端已经难以完成,此时需要前端进行一定的逻辑判断。
如果是前后端一体项目,可以参考:Thymeleaf标签方言,如果是前后端分离项目,则:
1.登陆时,把当前帐号拥有的所有权限码返回给前端
2.前端将权限码集合保存在localStroage 或者 其它全局状态管理对象中。
3.在需要权限控制的按钮上,使用js进行逻辑判断,例如Vue框架中:
// `arr`是当前用户拥有的权限码数组
// `user.delete`是显示按钮需要拥有的权限码
// `删除按钮`是用户拥有权限码才可以看到的内容。
<button v-if="arr.indexOf('user.delete') > -1">删除按钮</button>
前端的鉴权只是一个辅助功能,为了保证服务器安全:无论前端是否进行了权限校验,后端接口都需要对会话请求再次进行权限校验
后续内容
强烈建议官网查看,非常详细而且通俗易懂还提供了gif动图
3160

被折叠的 条评论
为什么被折叠?



