Sa-Token

框架介绍


轻量级 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动图

Sa-Token官网

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~Yogi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值