Compare commits

..

10 Commits

Author SHA1 Message Date
shengzhang
ba640295c5 v1.12.0 版本更新 2021-01-12 01:01:23 +08:00
shengzhang
1ce7b945d8 完善文档 2021-01-11 23:17:15 +08:00
shengzhang
4bfad95bad 新增身份临时切换功能 2021-01-11 23:10:11 +08:00
shengzhang
c7f3b6d493 路由拦截器 增加参数 2021-01-11 21:18:55 +08:00
shengzhang
c3bedaef99 v.1.12.0新特性:路由拦截式鉴权 2021-01-11 20:08:17 +08:00
shengzhang
ac4ac37175 取消判断同账号免重复登陆 2021-01-10 23:01:34 +08:00
shengzhang
713758c304 新增jwt测试 2021-01-10 18:39:32 +08:00
shengzhang
857e260a0a jwt集成示例 2021-01-10 18:36:35 +08:00
shengzhang
f1503d93a0 优化文档首页介绍 2021-01-10 00:55:45 +08:00
shengzhang
1648422617 修正更新日期 2021-01-10 00:49:29 +08:00
38 changed files with 1424 additions and 148 deletions

View File

@@ -1,11 +1,11 @@
<p align="center">
<img alt="logo" src="https://gitee.com/sz6/sa-token/raw/master/sa-token-doc/doc/logo.png" width="150" height="150" style="margin-bottom: 10px;">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">sa-token v1.11.0</h1>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">sa-token v1.12.0</h1>
<h4 align="center">这可能是史上功能最全的Java权限认证框架</h4>
<h4 align="center">
<a href="https://gitee.com/sz6/sa-token/stargazers"><img src="https://gitee.com/sz6/sa-token/badge/star.svg"></a>
<a href="https://github.com/click33/sa-token"><img src="https://img.shields.io/badge/sa--token-v1.11.0-2B9939"></a>
<a href="https://github.com/click33/sa-token"><img src="https://img.shields.io/badge/sa--token-v1.12.0-2B9939"></a>
<a href="https://github.com/click33/sa-token/stargazers"><img src="https://img.shields.io/github/stars/click33/sa-token"></a>
<a href="https://github.com/click33/sa-token/watchers"><img src="https://img.shields.io/github/watchers/click33/sa-token"></a>
<a href="https://github.com/click33/sa-token/network/members"><img src="https://img.shields.io/github/forks/click33/sa-token"></a>
@@ -71,6 +71,7 @@ StpUtil.getSessionByLoginId(10001); // 获取账号id为10001的Session
StpUtil.getTokenValueByLoginId(10001); // 获取账号id为10001的token令牌值
StpUtil.setLoginId(10001, "PC"); // 指定设备标识登录
StpUtil.logoutByLoginId(10001, "PC"); // 指定设备标识进行强制注销 (不同端不受影响)
StpUtil.switchTo(10044); // 将当前会话身份临时切换为其它账号
```
sa-token的API众多请恕此处无法为您逐一展示更多示例请戳官方在线文档
@@ -80,11 +81,13 @@ sa-token的API众多请恕此处无法为您逐一展示更多示例请戳
- **权限验证** —— 适配RBAC模型不同角色不同授权
- **Session会话** —— 专业的数据缓存中心
- **踢人下线** —— 将违规用户立刻清退下线
- **模拟他人账号** —— 实时操作任意用户状态数据
- **持久层扩展** —— 可集成redis、MongoDB等专业缓存中间件
- **多账号认证体系** —— 比如一个商城项目的user表和admin表分开鉴权
- **无Cookie模式** —— APP、小程序等前后台分离场景
- **注解式鉴权** —— 优雅的将鉴权与业务代码分离
- **路由拦截式鉴权** —— 设定全局路由拦截,并排除指定路由
- **模拟他人账号** —— 实时操作任意用户状态数据
- **临时身份切换** —— 将会话身份临时切换为其它账号
- **花式token生成** —— 内置六种token风格还可自定义token生成策略
- **自动续签** —— 提供两种token过期策略灵活搭配使用还可自动续签
- **同端互斥登录** —— 像QQ一样手机电脑同时在线但是两个手机上互斥登录

View File

@@ -8,7 +8,7 @@
<groupId>cn.dev33</groupId>
<artifactId>sa-token-parent</artifactId>
<packaging>pom</packaging>
<version>1.11.0</version>
<version>1.12.0</version>
<!-- 项目介绍 -->
<name>sa-token</name>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-parent</artifactId>
<version>1.11.0</version>
<version>1.12.0</version>
</parent>
<packaging>jar</packaging>

View File

@@ -21,5 +21,13 @@ public class SaTokenException extends RuntimeException {
super(message);
}
/**
* 构建一个异常
* @param cause 异常对象
*/
public SaTokenException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,15 @@
package cn.dev33.satoken.stp;
/**
* 模拟身份方法的辅助类
* @author kong
*
*/
public interface SaFunction {
/**
* 执行的方法
*/
public void run();
}

View File

@@ -44,6 +44,24 @@ public class StpLogic {
this.loginKey = loginKey;
}
/**
* 获取当前StpLogin的loginKey
* @return 当前StpLogin的loginKey
*/
public String getLoginKey(){
return loginKey;
}
/**
* 写入当前StpLogin的loginKey
* @param loginKey loginKey
* @return 对象自身
*/
public StpLogic setLoginKey(String loginKey){
this.loginKey = loginKey;
return this;
}
// =================== 获取token 相关 ===================
@@ -77,8 +95,8 @@ public class StpLogic {
String tokenValue = null;
// 1. 尝试从request里读取
if(request.getAttribute(SaTokenConsts.JUST_CREATED_SAVE_KEY) != null) {
tokenValue = String.valueOf(request.getAttribute(SaTokenConsts.JUST_CREATED_SAVE_KEY));
if(request.getAttribute(getKeyJustCreatedSave()) != null) {
tokenValue = String.valueOf(request.getAttribute(getKeyJustCreatedSave()));
}
// 2. 尝试从请求体里面读取
if(tokenValue == null && config.getIsReadBody() == true){
@@ -100,14 +118,6 @@ public class StpLogic {
return tokenValue;
}
/**
* 获取当前StpLogin的loginKey
* @return 当前StpLogin的loginKey
*/
public String getLoginKey(){
return loginKey;
}
/**
* 获取当前会话的token信息
* @return token信息
@@ -145,14 +155,6 @@ public class StpLogic {
*/
public void setLoginId(Object loginId, String device) {
// ------ 0、如果当前会话已经登录上了此LoginId且登录设备相同则立即返回
Object loggedId = getLoginIdDefaultNull();
if(loggedId != null && loggedId.toString().equals(loginId.toString())) {
String loggedDevice = getLoginDevice();
if(loggedDevice != null && loggedDevice.equals(device)) {
return;
}
}
// ------ 1、获取相应对象
HttpServletRequest request = SaTokenManager.getSaTokenServlet().getRequest();
@@ -199,7 +201,7 @@ public class StpLogic {
// ------ 4. 持久化其它数据
dao.setValue(getKeyTokenValue(tokenValue), String.valueOf(loginId), config.getTimeout()); // token -> uid
request.setAttribute(SaTokenConsts.JUST_CREATED_SAVE_KEY, tokenValue); // 将token保存到本次request里
request.setAttribute(getKeyJustCreatedSave(), tokenValue); // 将token保存到本次request里
setLastActivityToNow(tokenValue); // 写入 [最后操作时间]
if(config.getIsReadCookie() == true){ // cookie注入
SaTokenManager.getSaTokenCookie().addCookie(SaTokenManager.getSaTokenServlet().getResponse(), getTokenName(), tokenValue, "/", (int)config.getTimeout());
@@ -231,7 +233,7 @@ public class StpLogic {
clearLastActivity(tokenValue);
// 2. 尝试清除token-id键值对 (先从db中获取loginId值如果根本查不到loginId那么无需继续操作 )
String loginId = SaTokenManager.getSaTokenDao().getValue(getKeyTokenValue(tokenValue));
String loginId = getLoginIdNotHandle(tokenValue);
if(loginId == null || NotLoginException.ABNORMAL_LIST.contains(loginId)) {
return;
}
@@ -311,13 +313,17 @@ public class StpLogic {
* @return 账号id
*/
public Object getLoginId() {
// 如果正在[临时身份切换]
if(isSwitch()) {
return getSwitchLoginId();
}
// 如果获取不到token则抛出无token
String tokenValue = getTokenValue();
if(tokenValue == null) {
throw NotLoginException.newInstance(loginKey, NotLoginException.NOT_TOKEN);
}
// 查找此token对应loginId, 则抛出无效token
String loginId = SaTokenManager.getSaTokenDao().getValue(getKeyTokenValue(tokenValue));
String loginId = getLoginIdNotHandle(tokenValue);
if(loginId == null) {
throw NotLoginException.newInstance(loginKey, NotLoginException.INVALID_TOKEN);
}
@@ -371,13 +377,17 @@ public class StpLogic {
* @return 账号id
*/
public Object getLoginIdDefaultNull() {
// 如果正在[临时身份切换]
if(isSwitch()) {
return getSwitchLoginId();
}
// 如果连token都是空的则直接返回
String tokenValue = getTokenValue();
if(tokenValue == null) {
return null;
}
// loginId为null或者在异常项里面均视为未登录
Object loginId = SaTokenManager.getSaTokenDao().getValue(getKeyTokenValue(tokenValue));
Object loginId = getLoginIdNotHandle(tokenValue);
if(loginId == null || NotLoginException.ABNORMAL_LIST.contains(loginId)) {
return null;
}
@@ -427,16 +437,24 @@ public class StpLogic {
* @return 登录id
*/
public Object getLoginIdByToken(String tokenValue) {
if(tokenValue != null) {
Object loginId = SaTokenManager.getSaTokenDao().getValue(getKeyTokenValue(tokenValue));
if(loginId != null) {
return loginId;
}
if(tokenValue == null) {
return null;
}
return null;
return getLoginIdNotHandle(tokenValue);
}
/**
* 获取指定token对应的登录id (不做任何特殊处理)
* @param tokenValue token值
* @return loginId
*/
public String getLoginIdNotHandle(String tokenValue) {
return SaTokenManager.getSaTokenDao().getValue(getKeyTokenValue(tokenValue));
}
// =================== session相关 ===================
/**
@@ -536,7 +554,7 @@ public class StpLogic {
if(tokenValue == null || Objects.equals(tokenValue, "")) {
// 随机一个token送给ta
tokenValue = createTokenValue(null);
SaTokenManager.getSaTokenServlet().getRequest().setAttribute(SaTokenConsts.JUST_CREATED_SAVE_KEY, tokenValue);
SaTokenManager.getSaTokenServlet().getRequest().setAttribute(getKeyJustCreatedSave(), tokenValue);
setLastActivityToNow(tokenValue); // 写入 [最后操作时间]
if(getConfig().getIsReadCookie() == true){ // cookie注入
SaTokenManager.getSaTokenCookie().addCookie(SaTokenManager.getSaTokenServlet().getResponse(), getTokenName(), tokenValue, "/", (int)getConfig().getTimeout());
@@ -1039,6 +1057,22 @@ public class StpLogic {
return getConfig().getTokenName() + ":" + loginKey + ":last-activity:" + tokenValue;
}
/**
* 在进行身份切换时使用的存储key
* @return key
*/
public String getKeySwitch() {
return SaTokenConsts.SWITCH_TO_SAVE_KEY + getLoginKey();
}
/**
* 如果token为本次请求新创建的则以此字符串为key存储在当前request中
* @return key
*/
public String getKeyJustCreatedSave() {
return SaTokenConsts.JUST_CREATED_SAVE_KEY + getLoginKey();
}
// =================== Bean对象代理 ===================
@@ -1052,7 +1086,7 @@ public class StpLogic {
}
// =================== 注解鉴权 ===================
// =================== 其它方法 ===================
/**
* 对一个Method对象进行注解检查注解鉴权内部实现
@@ -1111,6 +1145,58 @@ public class StpLogic {
// 验证通过
}
// =================== 身份切换 ===================
/**
* 临时切换身份为指定loginId
* @param loginId 指定loginId
*/
public void switchTo(Object loginId) {
SaTokenManager.getSaTokenServlet().getRequest().setAttribute(getKeySwitch(), loginId);
}
/**
* 结束临时切换身份
*/
public void endSwitch() {
SaTokenManager.getSaTokenServlet().getRequest().removeAttribute(getKeySwitch());
}
/**
* 当前是否正处于[身份临时切换]中
* @return 是否正处于[身份临时切换]中
*/
public boolean isSwitch() {
return SaTokenManager.getSaTokenServlet().getRequest().getAttribute(getKeySwitch()) != null;
}
/**
* 返回[身份临时切换]的loginId
* @return 返回[身份临时切换]的loginId
*/
public Object getSwitchLoginId() {
return SaTokenManager.getSaTokenServlet().getRequest().getAttribute(getKeySwitch());
}
/**
* 在一个代码段里方法内临时切换身份为指定loginId
* @param loginId 指定loginId
* @param function 要执行的方法
*/
public void switchTo(Object loginId, SaFunction function) {
try {
switchTo(loginId);
function.run();
} catch (Exception e) {
throw e;
} finally {
endSwitch();
}
}

View File

@@ -14,7 +14,16 @@ public class StpUtil {
* 底层的 StpLogic 对象
*/
public static StpLogic stpLogic = new StpLogic("login");
/**
* 获取当前StpLogin的loginKey
* @return 当前StpLogin的loginKey
*/
public static String getLoginKey(){
return stpLogic.getLoginKey();
}
// =================== 获取token 相关 ===================
@@ -34,14 +43,6 @@ public class StpUtil {
return stpLogic.getTokenValue();
}
/**
* 获取当前StpLogin的loginKey
* @return 当前StpLogin的loginKey
*/
public static String getLoginKey(){
return stpLogic.getLoginKey();
}
/**
* 获取当前会话的token信息
* @return token信息
@@ -486,4 +487,39 @@ public class StpUtil {
}
// =================== 身份切换 ===================
/**
* 临时切换身份为指定loginId
* @param loginId 指定loginId
*/
public static void switchTo(Object loginId) {
stpLogic.switchTo(loginId);
}
/**
* 结束临时切换身份
*/
public static void endSwitch() {
stpLogic.endSwitch();
}
/**
* 当前是否正处于[身份临时切换]中
* @return 是否正处于[身份临时切换]中
*/
public static boolean isSwitch() {
return stpLogic.isSwitch();
}
/**
* 在一个代码段里方法内临时切换身份为指定loginId
* @param loginId 指定loginId
* @param function 要执行的方法
*/
public static void switchTo(Object loginId, SaFunction function) {
stpLogic.switchTo(loginId, function);
}
}

View File

@@ -10,7 +10,7 @@ public class SaTokenConsts {
/**
* sa-token 版本号
*/
public static final String VERSION_NO = "v1.11.0";
public static final String VERSION_NO = "v1.12.0";
/**
* sa-token 开源地址
@@ -18,7 +18,7 @@ public class SaTokenConsts {
public static final String GITHUB_URL = "https://github.com/click33/sa-token";
/**
* 如果token为本次请求新创建的则以此字符串为key存储在当前request中 JUST_CREATED_SAVE_KEY
* 如果token为本次请求新创建的则以此字符串为key存储在当前request中
*/
public static final String JUST_CREATED_SAVE_KEY = "JUST_CREATED_SAVE_KEY_";
@@ -32,11 +32,10 @@ public class SaTokenConsts {
*/
public static final String DEFAULT_LOGIN_DEVICE = "default-device";
// /**
// * 在用一个字符串存储多个token时所使用的分隔符
// */
// public static final String MULTIPLE_TOKEN_SEPARATOR = ",";
/**
* 在进行临时身份切换时使用的key
*/
public static final String SWITCH_TO_SAVE_KEY = "SWITCH_TO_SAVE_KEY_";

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-parent</artifactId>
<version>1.11.0</version>
<version>1.12.0</version>
</parent>
<packaging>jar</packaging>
@@ -20,7 +20,7 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.11.0</version>
<version>1.12.0</version>
</dependency>
<!-- RedisTemplate 相关操作API -->
<dependency>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-parent</artifactId>
<version>1.11.0</version>
<version>1.12.0</version>
</parent>
<packaging>jar</packaging>
@@ -20,7 +20,7 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.11.0</version>
<version>1.12.0</version>
</dependency>
<!-- RedisTemplate 相关操作API -->
<dependency>

12
sa-token-demo-jwt/.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
target/
node_modules/
bin/
.settings/
unpackage/
.classpath
.project
.idea/
.factorypath

80
sa-token-demo-jwt/pom.xml Normal file
View File

@@ -0,0 +1,80 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-demo-jwt</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- SpringBoot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
<dependencies>
<!-- springboot依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- sa-token 权限认证, 在线文档http://sa-token.dev33.cn/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.12.0</version>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- sa-token整合redis (使用jdk默认序列化方式) -->
<!-- <dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis</artifactId>
<version>1.12.0</version>
</dependency> -->
<!-- sa-token整合redis (使用jackson序列化方式) -->
<!-- <dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.12.0</version>
</dependency> -->
<!-- 提供redis连接池 -->
<!-- <dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency> -->
<!-- sa-token整合SpringAOP实现注解鉴权 -->
<!-- <dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-aop</artifactId>
<version>1.12.0</version>
</dependency> -->
<!-- @ConfigurationProperties -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,16 @@
package com.pj;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import cn.dev33.satoken.SaTokenManager;
@SpringBootApplication
public class SaTokenJwtDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SaTokenJwtDemoApplication.class, args);
System.out.println("\n启动成功sa-token配置如下" + SaTokenManager.getConfig());
}
}

View File

@@ -0,0 +1,232 @@
package com.pj.satoken.jwt;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import cn.dev33.satoken.SaTokenManager;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaTokenConsts;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
@Component
public class SaTokenJwtUtil {
/**
* 秘钥 (随便手打几个字母就好了)
*/
public static final String BASE64_SECURITY = "79e7c69681b8270162386e6daa53d1dd";
/**
* token有效期 (单位: 秒)
*/
public static final long TIMEOUT = 60 * 60 * 2;
public static final String LOGIN_ID_KEY = "loginId";
/**
* 根据userId生成token
* @param loginId 账号id
* @param base64Security 秘钥
* @return jwt-token
*/
public static String createToken(Object loginId) {
// 判断,不可使用默认秘钥
if(BASE64_SECURITY.equals("79e7c69681b8270162386e6daa53d1d3")) {
throw new SaTokenException("请更换秘钥");
}
// 在这里你可以使用官方提供的claim方法构建载荷也可以使用setPayload自定义载荷但是两者不可一起使用
JwtBuilder builder = Jwts.builder()
.setHeaderParam("type", "JWT")
.claim(LOGIN_ID_KEY, loginId)
.setIssuedAt(new Date()) // 签发日期
.setExpiration(new Date(System.currentTimeMillis() + 1000 * TIMEOUT)) // 有效截止日期
.signWith(SignatureAlgorithm.HS256, BASE64_SECURITY.getBytes()); // 加密算法
//生成JWT
return builder.compact();
}
/**
* 从一个jwt里面解析出Claims
* @param tokenValue token值
* @param base64Security 秘钥
* @return Claims对象
*/
public static Claims getClaims(String tokenValue) {
// System.out.println(tokenValue);
Claims claims = Jwts.parser()
.setSigningKey(BASE64_SECURITY.getBytes())
.parseClaimsJws(tokenValue).getBody();
return claims;
}
/**
* 从一个jwt里面解析loginId
* @param tokenValue token值
* @param base64Security 秘钥
* @return loginId
*/
public static String getLoginId(String tokenValue) {
try {
Object loginId = getClaims(tokenValue).get(LOGIN_ID_KEY);
if(loginId == null) {
return null;
}
return String.valueOf(loginId);
} catch (ExpiredJwtException e) {
// throw NotLoginException.newInstance(StpUtil.stpLogic.loginKey, NotLoginException.TOKEN_TIMEOUT);
return NotLoginException.TOKEN_TIMEOUT;
} catch (MalformedJwtException e) {
throw NotLoginException.newInstance(StpUtil.stpLogic.loginKey, NotLoginException.INVALID_TOKEN);
} catch (Exception e) {
throw new SaTokenException(e);
}
}
static {
// 判断秘钥
if(BASE64_SECURITY.equals("79e7c69681b8270162386e6daa53d1dd")) {
String warn = "-------------------------------------\n";
warn += "请更换JWT秘钥不要使用示例默认秘钥\n";
warn += "-------------------------------------";
System.err.println(warn);
}
// 修改默认实现
StpUtil.stpLogic = new StpLogic("login") {
// 重写 (随机生成一个tokenValue)
@Override
public String createTokenValue(Object loginId) {
return SaTokenJwtUtil.createToken(loginId);
}
// 重写 (在当前会话上登录id )
@Override
public void setLoginId(Object loginId, String device) {
// ------ 1、获取相应对象
HttpServletRequest request = SaTokenManager.getSaTokenServlet().getRequest();
SaTokenConfig config = getConfig();
// ------ 2、生成一个token
String tokenValue = createTokenValue(loginId);
request.setAttribute(getKeyJustCreatedSave(), tokenValue); // 将token保存到本次request里
if(config.getIsReadCookie() == true){ // cookie注入
SaTokenManager.getSaTokenCookie().addCookie(SaTokenManager.getSaTokenServlet().getResponse(), getTokenName(), tokenValue, "/", (int)config.getTimeout());
}
}
// 重写 (获取指定token对应的登录id)
@Override
public String getLoginIdNotHandle(String tokenValue) {
try {
return SaTokenJwtUtil.getLoginId(tokenValue);
} catch (Exception e) {
return null;
}
}
// 重写 (当前会话注销登录)
@Override
public void logout() {
// 如果连token都没有那么无需执行任何操作
String tokenValue = getTokenValue();
if(tokenValue == null) {
return;
}
// 如果打开了cookie模式把cookie清除掉
if(getConfig().getIsReadCookie() == true){
SaTokenManager.getSaTokenCookie().delCookie(SaTokenManager.getSaTokenServlet().getRequest(), SaTokenManager.getSaTokenServlet().getResponse(), getTokenName());
}
}
// 重写 (获取指定key的session)
@Override
public SaSession getSessionBySessionId(String sessionId, boolean isCreate) {
throw new SaTokenException("jwt has not session");
}
// 重写 (获取当前登录者的token剩余有效时间 (单位: 秒))
@Override
public long getTokenTimeout() {
// 如果没有token
String tokenValue = getTokenValue();
if(tokenValue == null) {
return SaTokenDao.NOT_VALUE_EXPIRE;
}
// 开始取值
Claims claims = null;
try {
claims = SaTokenJwtUtil.getClaims(tokenValue);
} catch (Exception e) {
return SaTokenDao.NOT_VALUE_EXPIRE;
}
if(claims == null) {
return SaTokenDao.NOT_VALUE_EXPIRE;
}
Date expiration = claims.getExpiration();
if(expiration == null) {
return SaTokenDao.NOT_VALUE_EXPIRE;
}
return (expiration.getTime() - System.currentTimeMillis()) / 1000;
}
// 重写 (返回当前token的登录设备)
@Override
public String getLoginDevice() {
return SaTokenConsts.DEFAULT_LOGIN_DEVICE;
}
// 重写 (获取当前会话的token信息)
@Override
public SaTokenInfo getTokenInfo() {
SaTokenInfo info = new SaTokenInfo();
info.tokenName = getTokenName();
info.tokenValue = getTokenValue();
info.isLogin = isLogin();
info.loginId = getLoginIdDefaultNull();
info.loginKey = getLoginKey();
info.tokenTimeout = getTokenTimeout();
// info.sessionTimeout = getSessionTimeout();
// info.tokenSessionTimeout = getTokenSessionTimeout();
// info.tokenActivityTimeout = getTokenActivityTimeout();
info.loginDevice = getLoginDevice();
return info;
}
};
}
}

View File

@@ -0,0 +1,100 @@
package com.pj.test;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.pj.util.AjaxJson;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
/**
* 全局异常处理
*/
@RestControllerAdvice // 可指定包前缀,比如:(basePackages = "com.pj.admin")
public class GlobalException {
// 在每个控制器之前触发的操作
@ModelAttribute
public void get(HttpServletRequest request) throws IOException {
}
// 全局异常拦截(拦截项目中的所有异常)
@ExceptionHandler
public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response)
throws Exception {
// 打印堆栈,以供调试
e.printStackTrace();
// 不同异常返回不同状态码
AjaxJson aj = null;
if (e instanceof NotLoginException) { // 如果是未登录异常
NotLoginException ee = (NotLoginException) e;
aj = AjaxJson.getNotLogin().setMsg(ee.getMessage());
} else if(e instanceof NotRoleException) { // 如果是角色异常
NotRoleException ee = (NotRoleException) e;
aj = AjaxJson.getNotJur("无此角色:" + ee.getRole());
} else if(e instanceof NotPermissionException) { // 如果是权限异常
NotPermissionException ee = (NotPermissionException) e;
aj = AjaxJson.getNotJur("无此权限:" + ee.getCode());
} else { // 普通异常, 输出500 + 异常信息
aj = AjaxJson.getError(e.getMessage());
}
// 返回给前端
return aj;
// 输出到客户端
// response.setContentType("application/json; charset=utf-8"); // http说明我要返回JSON对象
// response.getWriter().print(new ObjectMapper().writeValueAsString(aj));
}
// 全局异常拦截拦截项目中的NotLoginException异常
// @ExceptionHandler(NotLoginException.class)
// public AjaxJson handlerNotLoginException(NotLoginException nle, HttpServletRequest request, HttpServletResponse response)
// throws Exception {
//
// // 打印堆栈,以供调试
// nle.printStackTrace();
//
// // 判断场景值,定制化异常信息
// String message = "";
// if(nle.getType().equals(NotLoginException.NOT_TOKEN)) {
// message = "未提供token";
// }
// else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) {
// message = "token无效";
// }
// else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
// message = "token已过期";
// }
// else if(nle.getType().equals(NotLoginException.BE_REPLACED)) {
// message = "token已被顶下线";
// }
// else if(nle.getType().equals(NotLoginException.KICK_OUT)) {
// message = "token已被踢下线";
// }
// else {
// message = "当前会话未登录";
// }
//
// // 返回给前端
// return AjaxJson.getError(message);
// }
}

View File

@@ -0,0 +1,80 @@
package com.pj.test;
import java.util.Date;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pj.util.AjaxJson;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
/**
* 测试专用Controller
* @author kong
*
*/
@RestController
@RequestMapping("/test/")
public class TestJwtController {
// 测试登录接口, 浏览器访问: http://localhost:8081/test/login
@RequestMapping("login")
public AjaxJson login(@RequestParam(defaultValue="10001") String id) {
System.out.println("======================= 进入方法,测试登录接口 ========================= ");
System.out.println("当前会话的token" + StpUtil.getTokenValue());
System.out.println("当前是否登录:" + StpUtil.isLogin());
System.out.println("当前登录账号:" + StpUtil.getLoginIdDefaultNull());
StpUtil.setLoginId(id); // 在当前会话登录此账号
System.out.println("登录成功");
System.out.println("当前是否登录:" + StpUtil.isLogin());
System.out.println("当前登录账号:" + StpUtil.getLoginId());
// System.out.println("当前登录账号并转为int" + StpUtil.getLoginIdAsInt());
System.out.println("当前登录设备:" + StpUtil.getLoginDevice());
// System.out.println("当前token信息" + StpUtil.getTokenInfo());
return AjaxJson.getSuccess();
}
// 打印当前token信息 浏览器访问: http://localhost:8081/test/tokenInfo
@RequestMapping("tokenInfo")
public AjaxJson tokenInfo() {
System.out.println("======================= 进入方法打印当前token信息 ========================= ");
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
System.out.println(tokenInfo);
return AjaxJson.getSuccessData(tokenInfo);
}
// 测试会话session接口 浏览器访问: http://localhost:8081/test/session
@RequestMapping("session")
public AjaxJson session() throws JsonProcessingException {
System.out.println("======================= 进入方法测试会话session接口 ========================= ");
System.out.println("当前是否登录:" + StpUtil.isLogin());
System.out.println("当前登录账号session的id" + StpUtil.getSession().getId());
System.out.println("当前登录账号session的id" + StpUtil.getSession().getId());
System.out.println("测试取值name" + StpUtil.getSession().getAttribute("name"));
StpUtil.getSession().setAttribute("name", new Date()); // 写入一个值
System.out.println("测试取值name" + StpUtil.getSession().getAttribute("name"));
System.out.println( new ObjectMapper().writeValueAsString(StpUtil.getSession()));
return AjaxJson.getSuccess();
}
// 测试 浏览器访问: http://localhost:8081/test/test
@RequestMapping("test")
public AjaxJson test() {
StpUtil.getTokenSession().logout();
StpUtil.logoutByLoginId(10001);
return AjaxJson.getSuccess();
}
}

View File

@@ -0,0 +1,162 @@
package com.pj.util;
import java.io.Serializable;
import java.util.List;
/**
* ajax请求返回Json格式数据的封装
*/
public class AjaxJson implements Serializable{
private static final long serialVersionUID = 1L; // 序列化版本号
public static final int CODE_SUCCESS = 200; // 成功状态码
public static final int CODE_ERROR = 500; // 错误状态码
public static final int CODE_WARNING = 501; // 警告状态码
public static final int CODE_NOT_JUR = 403; // 无权限状态码
public static final int CODE_NOT_LOGIN = 401; // 未登录状态码
public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码
public int code; // 状态码
public String msg; // 描述信息
public Object data; // 携带对象
public Long dataCount; // 数据总数,用于分页
/**
* 返回code
* @return
*/
public int getCode() {
return this.code;
}
/**
* 给msg赋值连缀风格
*/
public AjaxJson setMsg(String msg) {
this.msg = msg;
return this;
}
public String getMsg() {
return this.msg;
}
/**
* 给data赋值连缀风格
*/
public AjaxJson setData(Object data) {
this.data = data;
return this;
}
/**
* 将data还原为指定类型并返回
*/
@SuppressWarnings("unchecked")
public <T> T getData(Class<T> cs) {
return (T) data;
}
// ============================ 构建 ==================================
public AjaxJson(int code, String msg, Object data, Long dataCount) {
this.code = code;
this.msg = msg;
this.data = data;
this.dataCount = dataCount;
}
// 返回成功
public static AjaxJson getSuccess() {
return new AjaxJson(CODE_SUCCESS, "ok", null, null);
}
public static AjaxJson getSuccess(String msg) {
return new AjaxJson(CODE_SUCCESS, msg, null, null);
}
public static AjaxJson getSuccess(String msg, Object data) {
return new AjaxJson(CODE_SUCCESS, msg, data, null);
}
public static AjaxJson getSuccessData(Object data) {
return new AjaxJson(CODE_SUCCESS, "ok", data, null);
}
public static AjaxJson getSuccessArray(Object... data) {
return new AjaxJson(CODE_SUCCESS, "ok", data, null);
}
// 返回失败
public static AjaxJson getError() {
return new AjaxJson(CODE_ERROR, "error", null, null);
}
public static AjaxJson getError(String msg) {
return new AjaxJson(CODE_ERROR, msg, null, null);
}
// 返回警告
public static AjaxJson getWarning() {
return new AjaxJson(CODE_ERROR, "warning", null, null);
}
public static AjaxJson getWarning(String msg) {
return new AjaxJson(CODE_WARNING, msg, null, null);
}
// 返回未登录
public static AjaxJson getNotLogin() {
return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null);
}
// 返回没有权限的
public static AjaxJson getNotJur(String msg) {
return new AjaxJson(CODE_NOT_JUR, msg, null, null);
}
// 返回一个自定义状态码的
public static AjaxJson get(int code, String msg){
return new AjaxJson(code, msg, null, null);
}
// 返回分页和数据的
public static AjaxJson getPageData(Long dataCount, Object data){
return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount);
}
// 返回,根据受影响行数的(大于0=ok小于0=error)
public static AjaxJson getByLine(int line){
if(line > 0){
return getSuccess("ok", line);
}
return getError("error").setData(line);
}
// 返回,根据布尔值来确定最终结果的 (true=okfalse=error)
public static AjaxJson getByBoolean(boolean b){
return b ? getSuccess("ok") : getError("error");
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@SuppressWarnings("rawtypes")
@Override
public String toString() {
String data_string = null;
if(data == null){
} else if(data instanceof List){
data_string = "List(length=" + ((List)data).size() + ")";
} else {
data_string = data.toString();
}
return "{"
+ "\"code\": " + this.getCode()
+ ", \"msg\": \"" + this.getMsg() + "\""
+ ", \"data\": " + data_string
+ ", \"dataCount\": " + dataCount
+ "}";
}
}

View File

@@ -0,0 +1,45 @@
# 端口
server:
port: 8081
spring:
# sa-token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: satoken
# token有效期单位s 默认30天, -1代表永不过期
timeout: 2592000
# token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
activity-timeout: -1
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
allow-concurrent-login: true
# 在多人登录同一账号时是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
is-share: true
# token风格
token-style: uuid
# redis配置
redis:
# Redis数据库索引默认为0
database: 0
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码默认为空
password:
# 连接超时时间(毫秒)
timeout: 10000ms
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0

View File

@@ -29,21 +29,21 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.11.0</version>
<version>1.12.0</version>
</dependency>
<!-- sa-token整合redis (使用jdk默认序列化方式) -->
<!-- <dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis</artifactId>
<version>1.11.0</version>
<version>1.12.0</version>
</dependency> -->
<!-- sa-token整合redis (使用jackson序列化方式) -->
<!-- <dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.11.0</version>
<version>1.12.0</version>
</dependency> -->
<!-- 提供redis连接池 -->
@@ -56,7 +56,7 @@
<!-- <dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-aop</artifactId>
<version>1.11.0</version>
<version>1.12.0</version>
</dependency> -->
<!-- @ConfigurationProperties -->

View File

@@ -5,7 +5,7 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.interceptor.SaCheckInterceptor;
import cn.dev33.satoken.interceptor.SaAnnotationInterceptor;
/**
* sa-token代码方式进行配置
@@ -30,7 +30,8 @@ public class MySaTokenConfig implements WebMvcConfigurer {
// 注册sa-token的拦截器打开注解式鉴权功能
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaCheckInterceptor()).addPathPatterns("/**"); // 全局拦截器
// 注册注解拦截器
registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**"); // 全局拦截器
}

View File

@@ -145,7 +145,6 @@ public class TestController {
return AjaxJson.getSuccess();
}
// 打印当前token信息 浏览器访问: http://localhost:8081/test/tokenInfo
@RequestMapping("tokenInfo")
public AjaxJson tokenInfo() {
@@ -203,6 +202,18 @@ public class TestController {
return AjaxJson.getSuccess();
}
// 测试身份临时切换: http://localhost:8081/test/switchTo
@RequestMapping("switchTo")
public AjaxJson switchTo() {
System.out.println("当前会话身份:" + StpUtil.getLoginIdDefaultNull());
System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch());
StpUtil.switchTo(10044, () -> {
System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch());
System.out.println("当前会话身份已被切换为:" + StpUtil.getLoginId());
});
System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch());
return AjaxJson.getSuccess();
}
// 测试会话治理 浏览器访问: http://localhost:8081/test/search
@RequestMapping("search")
@@ -234,8 +245,14 @@ public class TestController {
// 测试 浏览器访问: http://localhost:8081/test/test
@RequestMapping("test")
public AjaxJson test() {
StpUtil.getTokenSession().logout();
StpUtil.logoutByLoginId(10001);
// StpUtil.getTokenSession().logout();
// StpUtil.logoutByLoginId(10001);
return AjaxJson.getSuccess();
}
// 测试 浏览器访问: http://localhost:8081/test/test2
@RequestMapping("test2")
public AjaxJson test2() {
return AjaxJson.getSuccess();
}

View File

@@ -1,11 +1,11 @@
<p align="center">
<img alt="logo" src="https://gitee.com/sz6/sa-token/raw/master/sa-token-doc/doc/logo.png" width="150" height="150" style="margin-bottom: 10px;">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">sa-token v1.11.0</h1>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">sa-token v1.12.0</h1>
<h4 align="center">这可能是史上功能最全的Java权限认证框架</h4>
<h4 align="center">
<a href="https://gitee.com/sz6/sa-token/stargazers"><img src="https://gitee.com/sz6/sa-token/badge/star.svg"></a>
<a href="https://github.com/click33/sa-token"><img src="https://img.shields.io/badge/sa--token-v1.11.0-2B9939"></a>
<a href="https://github.com/click33/sa-token"><img src="https://img.shields.io/badge/sa--token-v1.12.0-2B9939"></a>
<a href="https://github.com/click33/sa-token/stargazers"><img src="https://img.shields.io/github/stars/click33/sa-token"></a>
<a href="https://github.com/click33/sa-token/watchers"><img src="https://img.shields.io/github/watchers/click33/sa-token"></a>
<a href="https://github.com/click33/sa-token/network/members"><img src="https://img.shields.io/github/forks/click33/sa-token"></a>
@@ -71,6 +71,7 @@ StpUtil.getSessionByLoginId(10001); // 获取账号id为10001的Session
StpUtil.getTokenValueByLoginId(10001); // 获取账号id为10001的token令牌值
StpUtil.setLoginId(10001, "PC"); // 指定设备标识登录
StpUtil.logoutByLoginId(10001, "PC"); // 指定设备标识进行强制注销 (不同端不受影响)
StpUtil.switchTo(10044); // 将当前会话身份临时切换为其它账号
```
sa-token的API众多请恕此处无法为您逐一展示更多示例请戳官方在线文档
@@ -80,11 +81,13 @@ sa-token的API众多请恕此处无法为您逐一展示更多示例请戳
- **权限验证** —— 适配RBAC模型不同角色不同授权
- **Session会话** —— 专业的数据缓存中心
- **踢人下线** —— 将违规用户立刻清退下线
- **模拟他人账号** —— 实时操作任意用户状态数据
- **持久层扩展** —— 可集成redis、MongoDB等专业缓存中间件
- **多账号认证体系** —— 比如一个商城项目的user表和admin表分开鉴权
- **无Cookie模式** —— APP、小程序等前后台分离场景
- **注解式鉴权** —— 优雅的将鉴权与业务代码分离
- **路由拦截式鉴权** —— 设定全局路由拦截,并排除指定路由
- **模拟他人账号** —— 实时操作任意用户状态数据
- **临时身份切换** —— 将会话身份临时切换为其它账号
- **花式token生成** —— 内置六种token风格还可自定义token生成策略
- **自动续签** —— 提供两种token过期策略灵活搭配使用还可自动续签
- **同端互斥登录** —— 像QQ一样手机电脑同时在线但是两个手机上互斥登录

View File

@@ -16,6 +16,7 @@
- [多账号验证](/use/many-account)
- [同端互斥登录](/use/mutex-login)
- [注解式鉴权](/use/at-check)
- [路由拦截式鉴权](/use/route-check)
- [花式token](/use/token-style)
- [框架配置](/use/config)
- [会话治理](/use/search-session)

View File

@@ -2,12 +2,12 @@
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>sa-token</title>
<title>sa-token 官方文档</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="Description">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta name="keywords" content="sa-token|sa-token框架|sa-token文档|sa-token在线文档|sa-token官方文档|权限认证框架">
<meta name="description" content="sa-token是一个JavaWeb权限认证框架功能全面上手简单登录验证、权限验证、Session会话、踢人下线、集成Redis、前后台分离、模拟他人账号、多账号体系、注解式鉴权、花式token、自动续签、同端互斥登录、会话治理、Spring集成...,零配置开箱即用,覆盖所有应用场景,你所需要的功能,这里都有">
<meta name="description" content="sa-token是一个JavaWeb权限认证框架功能全面上手简单登录验证、权限验证、Session会话、踢人下线、集成Redis、前后台分离、模拟他人账号、临时身份切换、多账号体系、注解式鉴权、路由拦截式鉴权、花式token、自动续签、同端互斥登录、会话治理、Spring集成...,零配置开箱即用,覆盖所有应用场景,你所需要的功能,这里都有">
<link rel="stylesheet" href="https://unpkg.zhimg.com/docsify@4.11.3/lib/themes/vue.css">
<link rel="stylesheet" href="./lib/index.css">
<link rel="shortcut icon" type="image/x-icon" href="logo.png">
@@ -22,6 +22,7 @@
<nav>
<select onchange="location.href=this.value">
<option value="http://sa-token.dev33.cn/doc/index.html">最新版</option>
<option value="http://sa-token.dev33.cn/v/v1.11.0/doc/index.html">v1.11.0</option>
<option value="http://sa-token.dev33.cn/v/v1.10.0/doc/index.html">v1.10.0</option>
<option value="http://sa-token.dev33.cn/v/v1.9.0/doc/index.html">v1.9.0</option>
<option value="http://sa-token.dev33.cn/v/v1.8.0/doc/index.html">v1.8.0</option>
@@ -40,7 +41,7 @@
</div>
<script>
var name = '<img style="width: 50px; height: 50px; vertical-align: middle;" src="logo.png" alt="logo" /> ';
name += '<b style="font-size: 24px; vertical-align: middle;">sa-token</b> <sub>v1.11.0</sub>'
name += '<b style="font-size: 24px; vertical-align: middle;">sa-token</b> <sub>v1.12.0</sub>'
window.$docsify = {
name: name, // 名字
repo: 'https://github.com/click33/sa-token', // github地址

View File

@@ -1,7 +1,15 @@
# 更新日志
### 2021-1-9 @v1.11.0
### 2021-1-12 @v1.12.0
- 新增提供JWT集成示例 **[重要]**
- 新增:新增路由式鉴权,可方便的根据路由匹配鉴权 **[重要]**
- 新增:新增身份临时切换功能,可在一个代码段内将会话临时切换为其它账号 **[重要]**
- 优化:将`SaCheckInterceptor.java`更名为`SaAnnotationInterceptor.java`,更语义化的名称
- 优化:优化文档
### 2021-1-10 @v1.11.0
- 新增提供AOP注解鉴权方案 **[重要]**
- 优化自动生成token的算法

View File

@@ -9,10 +9,15 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.11.0</version>
<version>1.12.0</version>
</dependency>
```
## Gradle依赖
Gradle用户引入依赖
```
implementation 'cn.dev33:sa-token-spring-boot-starter:1.12.0'
```
@@ -27,9 +32,11 @@
├── sa-token-core // sa-token核心模块
├── sa-token-spring-boot-starter // sa-token整合springboot快速集成
├── sa-token-dao-redis // sa-token整合redis (使用jdk默认序列化方式)
├── sa-token-dao-redis-jackson // sa-token整合redis (使用jackson序列化方式)
├── sa-token-demo-springboot // sa-token示例
├── sa-token-doc // sa-token开发文档
├── sa-token-dao-redis-jackson // sa-token整合redis (使用jackson序列化方式)
├── sa-token-spring-aop // sa-token整合SpringAOP 注解鉴权
├── sa-token-demo-springboot // sa-token示例
├── sa-token-demo-jwt // sa-token集成jwt示例
├── sa-token-doc // sa-token开发文档
├──pom.xml
```

View File

@@ -18,7 +18,7 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.11.0</version>
<version>1.12.0</version>
</dependency>
```

View File

@@ -13,10 +13,10 @@
``` java
@Configuration
public class MySaTokenConfig implements WebMvcConfigurer {
// 注册sa-token的拦截器打开注解式鉴权功能
// 注册sa-token的注解拦截器,打开注解式鉴权功能
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaCheckInterceptor()).addPathPatterns("/**"); // 全局拦截器
registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**");
}
}
```
@@ -90,7 +90,7 @@ mode有两种取值
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-aop</artifactId>
<version>1.11.0</version>
<version>1.12.0</version>
</dependency>
```
@@ -119,22 +119,3 @@ public class UserService {
<!-- ## 3、扩展
- 其实在注册拦截器时,我们也可以根据路由前缀设置不同 `StpLogic`, 从而达到不同模块不同鉴权方式的目的
- 以下为参考示例:
``` java
@Configuration
public class MySaTokenConfig implements WebMvcConfigurer {
// 注册sa-token的拦截器打开注解式鉴权功能
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaCheckInterceptor(StpUtil.stpLogic)).addPathPatterns("/admin/**");
registry.addInterceptor(new SaCheckInterceptor(StpUserUtil.stpLogic)).addPathPatterns("/user/**");
}
}
``` -->

View File

@@ -11,7 +11,7 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis</artifactId>
<version>1.11.0</version>
<version>1.12.0</version>
</dependency>
```
优点兼容性好缺点session序列化后基本不可读对开发者来讲等同于乱码
@@ -23,7 +23,7 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.11.0</version>
<version>1.12.0</version>
</dependency>
```
优点session序列化后可读性强可灵活手动修改缺点兼容性稍差

View File

@@ -1,6 +1,7 @@
# 模拟他人
---
- 以上介绍的api都是操作当前账号对当前账号进行各种鉴权操作你可能会问我能不能对别的账号进行一些操作
- 比如:查看账号`10001`有无某个权限码、获取id账号为`10002`的用户`session`,等等...
- `sa-token`在api设计时充分考虑了这一点暴露出多个api进行此类操作
@@ -8,23 +9,47 @@
## 有关操作其它账号的api
#### StpUtil.getTokenValueByLoginId(Object loginId)
- 获取指定`loginId``tokenValue`
#### StpUtil.logoutByLoginId(Object loginId)
- 指定`loginId`的会话注销登录(踢人下线)
#### StpUtil.getSessionByLoginId(Object loginId)
- 获取指定`loginId``session`(如果此id尚未创建`session`, 则返回`null`)
- 类似API还有
- `StpUtil.getSessionByLoginId(Object loginId, boolean isCreate)` 获取当前会话登录id, `isCreate`代表指定是否在无`session`的情况下新建并返回
#### StpUtil.hasRole(Object loginId, String role)
- 指定`loginId`是否含有指定角色
#### StpUtil.hasPermission(Object loginId, String permission)
- 指定`loginId`是否含有指定权限
``` java
StpUtil.getTokenValueByLoginId(10001); // 获取指定账号10001的`tokenValue`值
StpUtil.logoutByLoginId(10001); // 将账号10001的会话注销登录踢人下线
StpUtil.getSessionByLoginId(10001); // 获取账号10001的Session对象, 如果session尚未创建, 则新建并返回
StpUtil.getSessionByLoginId(10001, false); // 获取账号10001的Session对象, 如果session尚未创建, 则返回null
StpUtil.hasRole(10001, false); // 获取账号10001是否含有指定角色标识
StpUtil.hasPermission(10001, false); // 获取账号10001是否含有指定权限码
```
## 临时身份切换
有时候,我们需要直接将当前会话的身份切换为其它账号,比如:
``` java
StpUtil.switchTo(10044); // 将当前会话身份临时切换为其它账号
StpUtil.getLoginId(); // 此时再调用此方法会返回 10044
StpUtil.endSwitch(); // 结束 [身份临时切换]
```
你还可以: 直接在一个代码段里方法内临时切换身份为指定loginId (此方式无需手动调用`StpUtil.endSwitch()`关闭身份切换)
``` java
System.out.println("------- [身份临时切换]调用开始...");
StpUtil.switchTo(10044, new SaFunction() {
@Override
public void run() {
System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch());
System.out.println("获取当前登录账号id: " + StpUtil.getLoginId());
}
});
System.out.println("------- [身份临时切换]调用结束...");
```
如果你使用的JDK版本是1.8或以上,上面这一坨可以简写为以下形式:
``` java
System.out.println("------- [身份临时切换]调用开始...");
StpUtil.switchTo(10044, () -> {
System.out.println("是否正在身份临时切换中: " + StpUtil.isSwitch());
System.out.println("获取当前登录账号id: " + StpUtil.getLoginId());
});
System.out.println("------- [身份临时切换]调用结束...");
```

View File

@@ -0,0 +1,80 @@
# 路由拦截式鉴权
---
假设我们有如下需求:
> 项目中所有接口均需要登录验证,只有'登录接口'本身对外开放
我们怎么实现呢?给每个接口加上鉴权注解?手写全局拦截器?似乎都不是非常方便。<br/>
在这个需求中我们真正需要的是一种基于路由拦截的鉴权模式, 那么在sa-token怎么实现路由拦截鉴权呢
## 1、注册路由拦截器
`springboot2.0`为例, 新建配置类`MySaTokenConfig.java`
``` java
@Configuration
public class MySaTokenConfig implements WebMvcConfigurer {
// 注册sa-token的登录拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册登录拦截器,并排除登录接口地址
registry.addInterceptor(new SaRouteInterceptor()).addPathPatterns("/**").excludePathPatterns("/user/doLogin");
}
}
```
以上代码,我们注册了一个登录验证拦截器,并且排除了`/user/doLogin`接口用来开放登录 <br>
那么我们如何进行权限认证拦截呢,且往下看
## 2、所有拦截器示例
``` java
@Configuration
public class MySaTokenConfig implements WebMvcConfigurer {
// 注册sa-token的所有拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册一个登录验证拦截器
registry.addInterceptor(SaRouteInterceptor.createLoginVal()).addPathPatterns("/**").excludePathPatterns("/user/doLogin");
// 注册一个角色认证拦截器
registry.addInterceptor(SaRouteInterceptor.createRoleVal("super-admin")).addPathPatterns("/**");
// 注册一个权限认证拦截器
registry.addInterceptor(SaRouteInterceptor.createPermissionVal("user:add", "user:deelete")).addPathPatterns("/UserController/**");
// 注册一个自定义认证拦截器 (可以写任意认证代码)
registry.addInterceptor(new SaRouteInterceptor(new SaRouteFunction() {
@Override
public void run(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 你可以在这里写任意认证代码, 例如: StpUtil.checkLogin();
System.out.println("---------- 进入自定义认证 --------------- ");
}
})).addPathPatterns("/**");
/** ------ 如果你使用的JDK版本是1.8或以上,上面这一坨可以简写为以下形式 ------ */
// 注册一个自定义认证拦截器 (可以写任意认证代码)
registry.addInterceptor(new SaRouteInterceptor((request, response, handler)->{
// 你可以在这里写任意认证代码, 例如: StpUtil.checkLogin();
System.out.println("---------- 进入自定义认证2 --------------- ");
})).addPathPatterns("/**");
}
}
```
(你不必像上面的示例一样注册所有拦截器,只要按需注册即可

View File

@@ -2,12 +2,12 @@
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>sa-token 官方文档</title>
<title>sa-token</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="Description">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta name="keywords" content="sa-token|sa-token框架|sa-token文档|sa-token在线文档|权限认证框架">
<meta name="description" content="sa-token是一个JavaWeb权限认证框架功能全面上手简单登录验证、权限验证、Session会话、踢人下线、集成Redis、前后台分离、模拟他人账号、多账号体系、注解式鉴权、花式token、自动续签、同端互斥登录、会话治理、Spring集成...,零配置开箱即用,覆盖所有应用场景,你所需要的功能,这里都有">
<meta name="description" content="sa-token是一个JavaWeb权限认证框架功能全面上手简单登录验证、权限验证、Session会话、踢人下线、集成Redis、前后台分离、模拟他人账号、临时身份切换、多账号体系、注解式鉴权、路由拦截式鉴权、花式token、自动续签、同端互斥登录、会话治理、Spring集成...,零配置开箱即用,覆盖所有应用场景,你所需要的功能,这里都有">
<link rel="stylesheet" href="https://unpkg.zhimg.com/docsify@4.11.3/lib/themes/vue.css">
<link rel="shortcut icon" type="image/x-icon" href="doc/logo.png">
<link rel="stylesheet" href="index.css">
@@ -43,10 +43,10 @@
<!-- 内容部分 -->
<div class="main-box">
<div class="content-box">
<h1>sa-token<small>v1.11.0</small></h1>
<div class="sub-title">一个JavaWeb轻量级权限认证框架功能全面上手简单</div>
<h1>sa-token<small>v1.12.0</small></h1>
<div class="sub-title">这可能是史上功能最全的Java权限认证框架</div>
<!-- <p>0配置开箱即用低学习成本</p> -->
<p>登录验证、权限验证、Session会话、踢人下线、集成Redis、前后台分离、模拟他人账号、多账号体系、注解式鉴权、花式token、自动续签、同端互斥登录、会话治理、Spring集成...</p>
<p>登录验证、权限验证、Session会话、踢人下线、集成Redis、前后台分离、模拟他人账号、临时身份切换、多账号体系、注解式鉴权、路由拦截式鉴权、花式token、自动续签、同端互斥登录、会话治理、Spring集成...</p>
<p>零配置开箱即用,覆盖所有应用场景,你所需要的功能,这里都有</p>
<div class="btn-box">
<a href="https://github.com/click33/sa-token" target="_blank">GitHub</a>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-parent</artifactId>
<version>1.11.0</version>
<version>1.12.0</version>
</parent>
<packaging>jar</packaging>
@@ -20,7 +20,7 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.11.0</version>
<version>1.12.0</version>
</dependency>
<!-- spring-boot-starter-aop -->
<dependency>

View File

@@ -19,16 +19,18 @@ import cn.dev33.satoken.stp.StpUtil;
public class SaCheckAspect {
/**
* 底层的 StpLogic 对象
*/
public StpLogic stpLogic = null;
/**
* 创建,并指定一个默认的 StpLogic
* 创建
*/
public SaCheckAspect() {
this.stpLogic = StpUtil.stpLogic;
}
/**
* 获取本切面使用的StpLogic
*/
public StpLogic getStpLogic() {
return StpUtil.stpLogic;
}
/**
* 定义AOP签名 (切入所有使用sa-token鉴权注解的方法)
@@ -54,7 +56,7 @@ public class SaCheckAspect {
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 注解鉴权
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
stpLogic.checkMethodAnnotation(signature.getMethod());
getStpLogic().checkMethodAnnotation(signature.getMethod());
try {
// 执行原有逻辑
Object obj = joinPoint.proceed();

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-parent</artifactId>
<version>1.11.0</version>
<version>1.12.0</version>
</parent>
<packaging>jar</packaging>
@@ -19,7 +19,7 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-core</artifactId>
<version>1.11.0</version>
<version>1.12.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -15,7 +15,7 @@ import cn.dev33.satoken.stp.StpUtil;
* 注解式鉴权 - 拦截器
* @author kong
*/
public class SaCheckInterceptor implements HandlerInterceptor {
public class SaAnnotationInterceptor implements HandlerInterceptor {
/**
@@ -24,23 +24,32 @@ public class SaCheckInterceptor implements HandlerInterceptor {
public StpLogic stpLogic = null;
/**
* 创建并指定一个默认 StpLogic
* @return 底层 StpLogic 对象
*/
public SaCheckInterceptor() {
this(StpUtil.stpLogic);
public StpLogic getStpLogic() {
if(stpLogic == null) {
stpLogic = StpUtil.stpLogic;
}
return stpLogic;
}
/**
* 创建并指定一个的 StpLogic
* @param stpLogic 指定的StpLogic
*/
public SaCheckInterceptor(StpLogic stpLogic) {
this.stpLogic = stpLogic;
}
/**
* 每次请求之前触发
* @param stpLogic 底层的 StpLogic 对象
*/
public SaAnnotationInterceptor setStpLogic(StpLogic stpLogic) {
this.stpLogic = stpLogic;
return this;
}
/**
* 创建并指定一个默认的 StpLogic
*/
public SaAnnotationInterceptor() {
}
/**
* 每次请求之前触发的方法
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
@@ -53,7 +62,7 @@ public class SaCheckInterceptor implements HandlerInterceptor {
Method method = ((HandlerMethod) handler).getMethod();
// 进行验证
stpLogic.checkMethodAnnotation(method);
getStpLogic().checkMethodAnnotation(method);
// 通过验证
return true;
@@ -61,5 +70,4 @@ public class SaCheckInterceptor implements HandlerInterceptor {
}

View File

@@ -0,0 +1,18 @@
package cn.dev33.satoken.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 执行验证方法的辅助类
* @author kong
*
*/
public interface SaRouteFunction {
/**
* 执行验证的方法
*/
public void run(HttpServletRequest request, HttpServletResponse response, Object handler);
}

View File

@@ -0,0 +1,250 @@
package cn.dev33.satoken.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import cn.dev33.satoken.annotation.SaMode;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.stp.StpUtil;
/**
* sa-token基于路由的拦截式鉴权
* @author kong
*/
public class SaRouteInterceptor implements HandlerInterceptor {
// ----------------- 属性 -----------------
/**
* 底层的 StpLogic 对象
*/
private StpLogic stpLogic;
/**
* 验证类型 (1=登录验证, 2=角色验证, 3=权限验证, 4=自定义验证)
*/
private int type;
/**
* 验证模式 AND | OR
*/
private SaMode mode;
/**
* 标识码数组
*/
private String[] codes;
/**
* 自定义模式下的执行函数
*/
private SaRouteFunction function;
/**
* 表示登录验证
*/
public static final int LOGIN = 1;
/**
* 表示角色验证
*/
public static final int ROLE = 2;
/**
* 表示权限验证
*/
public static final int PERMISSION = 3;
/**
* 表示自定义验证
*/
public static final int CUSTOM = 4;
/**
* @return 底层的 StpLogic 对象
*/
public StpLogic getStpLogic() {
if(stpLogic == null) {
stpLogic = StpUtil.stpLogic;
}
return stpLogic;
}
/**
* @param stpLogic 底层的 StpLogic 对象
*/
public SaRouteInterceptor setStpLogic(StpLogic stpLogic) {
this.stpLogic = stpLogic;
return this;
}
/**
* @return 验证类型 (1=登录验证, 2=角色验证, 3=权限验证, 4=自定义验证)
*/
public int getType() {
return type;
}
/**
* @param type 验证类型 (1=登录验证, 2=角色验证, 3=权限验证, 4=自定义验证)
*/
public SaRouteInterceptor setType(int type) {
this.type = type;
return this;
}
/**
* @return 验证模式 AND | OR
*/
public SaMode getMode() {
return mode;
}
/**
* @param mode 验证模式 AND | OR
*/
public SaRouteInterceptor setMode(SaMode mode) {
this.mode = mode;
return this;
}
/**
* @return 标识码数组
*/
public String[] getCodes() {
return codes;
}
/**
* @param codes 标识码数组
*/
public SaRouteInterceptor setCodes(String... codes) {
this.codes = codes;
return this;
}
/**
* @return 自定义模式下的执行函数
*/
public SaRouteFunction getFunction() {
return function;
}
/**
* @param function 设置自定义模式下的执行函数
*/
public SaRouteInterceptor setFunction(SaRouteFunction function) {
this.type = SaRouteInterceptor.CUSTOM;
this.function = function;
return this;
}
// ----------------- 构建相关 -----------------
/**
* 创建 (全参数)
* @param type 验证类型 (1=登录验证, 2=角色验证, 3=权限验证, 4=自定义验证)
* @param mode 验证模式 AND | OR
* @param codes 标识码数组
*/
public SaRouteInterceptor(int type, SaMode mode, String... codes) {
super();
this.type = type;
this.mode = mode;
this.codes = codes;
}
/**
* 创建 (默认为登录验证)
*/
public SaRouteInterceptor() {
this(SaRouteInterceptor.LOGIN, null, new String[0]);
}
/**
* 创建 (默认为自定义认证)
* @param function 自定义模式下的执行函数
*/
public SaRouteInterceptor(SaRouteFunction function) {
this(SaRouteInterceptor.CUSTOM, null, new String[0]);
setFunction(function);
}
/**
* 构建一个模式为登录认证的sa路由拦截器
* @return sa拦截器
*/
public static SaRouteInterceptor createLoginVal() {
return new SaRouteInterceptor();
}
/**
* 构建一个模式为角色认证的sa路由拦截器
* @param roles 需要验证的角色标识列表
* @return sa拦截器
*/
public static SaRouteInterceptor createRoleVal(String... roles) {
return new SaRouteInterceptor(SaRouteInterceptor.ROLE, SaMode.AND, roles);
}
/**
* 构建一个模式为权限认证的sa路由拦截器
* @param permissions 需要验证的权限列表
* @return sa拦截器
*/
public static SaRouteInterceptor createPermissionVal(String... permissions) {
return new SaRouteInterceptor(SaRouteInterceptor.PERMISSION, SaMode.AND, permissions);
}
/**
* 创建一个模式为自定义认证的sa路由拦截器
* @param function 自定义模式下的执行函数
* @return sa拦截器
*/
public static SaRouteInterceptor createCustomVal(SaRouteFunction function) {
return new SaRouteInterceptor(function);
}
// ----------------- 验证方法 -----------------
/**
* 每次请求之前触发的方法
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 根据模式进行验证
if(this.type == SaRouteInterceptor.LOGIN) {
getStpLogic().checkLogin();
} else if(this.type == SaRouteInterceptor.ROLE) {
if(mode == SaMode.AND) {
getStpLogic().checkRoleAnd(codes);
} else {
getStpLogic().checkRoleOr(codes);
}
} else if(this.type == SaRouteInterceptor.PERMISSION) {
if(mode == SaMode.AND) {
getStpLogic().checkPermissionAnd(codes);
} else {
getStpLogic().checkPermissionOr(codes);
}
} else if(this.type == SaRouteInterceptor.CUSTOM) {
function.run(request, response, handler);
}
// 通过验证
return true;
}
}