Compare commits

...

46 Commits

Author SHA1 Message Date
shengzhang
1df216879c 文档集成Redis章节新增redis配置示例说明,感谢群友 @-) 提供的建议 2021-03-16 14:44:38 +08:00
click33
91998af186 Merge pull request #48 from zhangzi0291/patch-1
修改cookie中没有path的问题
2021-03-16 00:36:34 +08:00
zhangzi0291
f33df38f64 修改cookie中没有path的问题
addCookies时如果path没有指定,path默认为/
2021-03-16 00:33:12 +08:00
shengzhang
aaf4bb8931 Session新增getString(key)方法,方便取值时类型转换 2021-03-15 17:05:07 +08:00
shengzhang
5808710bbf 修复错别字Request->Response 2021-03-14 14:39:21 +08:00
shengzhang
62787c3257 更改路由拦截示例 2021-03-14 00:04:11 +08:00
省长
6dd39505f9 增加友链 2021-03-13 17:28:03 +08:00
省长
d86f72b469 update README.md. 2021-03-13 17:25:41 +08:00
省长
7b7920123a update README.md. 2021-03-13 17:23:46 +08:00
shengzhang
8872982455 优化文档 2021-03-13 00:54:39 +08:00
shengzhang
80f07ae01e 技术栈 2021-03-13 00:47:43 +08:00
shengzhang
540730d9a4 优化文档 2021-03-12 09:51:37 +08:00
省长
03c8abb269 update README.md. 2021-03-12 09:46:11 +08:00
shengzhang
f9ad71db66 v1.14.0 更新 2021-03-12 01:45:13 +08:00
shengzhang
9fe0e1f97b 优化文档 2021-03-11 01:06:27 +08:00
shengzhang
9f6717f8c0 更换二维码地址 2021-03-10 14:17:43 +08:00
shengzhang
af2d04ca1f 更换二维码 2021-03-10 14:04:58 +08:00
省长
64e6b58a05 update README.md. 2021-03-10 14:01:56 +08:00
shengzhang
275b3cec6f 完成 [记住我]模式 开发文档 2021-03-10 02:37:45 +08:00
省长
e92ff9fb7d !18 update README.md.
Merge pull request !18 from ZhuBJ0510/N/A
2021-03-09 15:02:10 +08:00
ZhuBJ0510
cffd462cd8 update README.md. 2021-03-09 15:01:57 +08:00
省长
9ca2cad5e6 !17 update README.md.
Merge pull request !17 from ZhuBJ0510/N/A
2021-03-09 14:57:28 +08:00
ZhuBJ0510
a076c8f8fc update README.md. 2021-03-09 14:33:17 +08:00
省长
dbddd05a6e !16 update sa-token-doc/doc/use/route-check.md.
Merge pull request !16 from AppleOfGray/N/A
2021-03-09 10:00:17 +08:00
AppleOfGray
18760010fc update sa-token-doc/doc/use/route-check.md. 2021-03-09 09:59:39 +08:00
shengzhang
9151f9a5c8 优化文档 2021-03-09 00:29:57 +08:00
shengzhang
cdaf9fa65a Merge branch 'dev' of https://gitee.com/sz6/sa-token into dev 2021-03-08 23:27:38 +08:00
shengzhang
c316746f0d [密码加密]模块的文档 2021-03-08 23:27:08 +08:00
省长
0695ee6cdf update sa-token-doc/doc/use/many-account.md. 2021-03-08 09:01:42 +08:00
shengzhang
3716b874e3 新增密码加密算法工具类 2021-03-08 02:13:18 +08:00
shengzhang
255a15033e 集成Redis章节提供常见问题说明 2021-03-07 00:15:53 +08:00
省长
ad70bb0300 update sa-token-doc/doc/more/link.md.
增加 vue-next-admin 友链
2021-03-06 14:26:41 +08:00
shengzhang
8c35d838ff SaSession新增refreshDataMap方法,用于一次性重置整个数据集合 2021-03-05 13:57:24 +08:00
shengzhang
1eae6861d1 更新微信群聊二维码 2021-03-04 10:13:00 +08:00
shengzhang
bbcc016fc7 SaOAuth2Config sa-token oauth2 配置类所有set方法新增支持链式调用 2021-03-03 10:47:32 +08:00
省长
c62f9482df update sa-token-doc/doc/README.md. 2021-03-02 11:24:28 +08:00
省长
d2c0ac3f52 update README.md. 2021-03-02 11:23:24 +08:00
shengzhang
51b76f07f4 起草[记住我]模式开发文档 2021-03-02 11:01:59 +08:00
shengzhang
0b32a1f552 优化源码注释 2021-03-02 09:22:55 +08:00
shengzhang
f7cf015324 新增SaLoginModel登录参数Model,适配[记住我]模式 **[重要]** 2021-03-01 02:20:22 +08:00
shengzhang
4965149c23 新增:权限认证文档新增[如何把权限精确搭到按钮级]示例说明 2021-02-28 02:05:48 +08:00
省长
7268279736 !15 update README.md.
Merge pull request !15 from ZhuBJ0510/N/A
2021-02-22 17:56:13 +08:00
ZhuBJ0510
20304aa717 update README.md. 2021-02-22 17:54:16 +08:00
省长
0d46d232b4 update README.md. 2021-02-20 23:56:18 +08:00
RockMan
64aa2edf9f update sa-token-doc/doc/README.md. 2021-02-20 10:19:41 +08:00
RockMan
cb72d4601c update README.md. 2021-02-20 10:18:43 +08:00
64 changed files with 1328 additions and 360 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">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">sa-token v1.13.0</h1>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">sa-token v1.14.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.13.0-2B9939"></a>
<a href="https://github.com/click33/sa-token"><img src="https://img.shields.io/badge/sa--token-v1.14.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>
@@ -28,29 +28,11 @@
## Sa-Token是什么
sa-token是一个轻量级Java权限认证框架主要解决登录认证、权限认证、Session会话、单点登录 等一系列权限相关问题
sa-token是一个轻量级Java权限认证框架主要解决登录认证、权限认证、Session会话、单点登录、OAuth2.0 等一系列权限相关问题
近年来有关权限认证的解决方案层出不穷例如单点登录、OAuth2.0、分布式Session等等难题无一不有着各种优秀框架大行其道
框架针对踢人下线、自动续签、前后台分离、分布式会话……等常见业务进行N多适配通过sa-token你可以以一种极简的方式实现系统的权限认证部分
然而当我们把视线放低那些最基础的有如登录认证、权限认证、Session会话等基础问题却一直被两大上古神兽 `Apache Shiro``Spring Security` 所把持
在此并非专门diss此两大框架诚然两大框架背景强大历史悠久其生态也比较齐全。但是它们毕竟已经是十几年前的产物那是一个还在用 `jsp` 写页面的时代两大框架的很多功能都是为jsp那一套量身定做。
在前后台分离已成标配的今天,两大框架的很多设计理念已经比较滞后,已经不能和我们的项目进行无缝适配,很多功能点都需要进行二次封装,甚至找一大堆扩展插件才能集成,已经逐渐不太适合现代化项目的应用
所以,为什么不能有一个自底向上,从最基础的登录、权限做起,以业务需求为核心,做到开箱即用的轻量级权限认证框架?
秉承着这个目的,`sa-token` 诞生了!
## Sa-Token 优点?
sa-token架构设计精简不引入各种复杂的概念如丝般顺滑的API调用大量高级特性统统只需一行代码即可实现
- 在sa-token之前权限认证业务概念纷飞、代码复杂在sa-token之后权限认证将会变成逻辑清晰架构简单人人可写
- 在sa-token之前你会在百度上频繁搜索: xx框架如何集成Redis前后台分离下如何鉴权踢人下线的原理是什么在sa-token之后你将轻松知道这一切的答案
- 在sa-token之前权限认证、分布式会话、单点登录、多账号认证你需要找不同的框架在sa-token之后你只需要这一个框架就足够了
与其它权限认证框架相比,你将会从以下方面感受到 `sa-token` 的优势:
与其它权限认证框架相比,`sa-token` 具有以下优势:
1. **简单** :可零配置启动框架,真正的开箱即用,低成本上手
2. **强大** :目前已集成几十项权限相关特性,涵盖了大部分业务场景的解决方案
3. **易用** 如丝般顺滑的API调用大量高级特性统统只需一行代码即可实现
@@ -58,7 +40,6 @@ sa-token架构设计精简不引入各种复杂的概念如丝般顺滑的
有了sa-token你所有的权限认证问题都不再是问题
## Sa-Token 能做什么?
- **登录验证** —— 轻松登录鉴权,并提供五种细分场景值
- **权限验证** —— 适配RBAC权限模型不同角色不同授权
@@ -74,9 +55,11 @@ sa-token架构设计精简不引入各种复杂的概念如丝般顺滑的
- **多账号认证体系** —— 比如一个商城项目的user表和admin表分开鉴权
- **花式token生成** —— 内置六种token风格还可自定义token生成策略
- **注解式鉴权** —— 优雅的将鉴权与业务代码分离
- **路由拦截式鉴权** —— 设定全局路由拦截,并排除指定路由
- **路由拦截式鉴权** —— 根据路由拦截鉴权可适配restful模式
- **自动续签** —— 提供两种token过期策略灵活搭配使用还可自动续签
- **会话治理** —— 提供方便灵活的会话查询接口
- **记住我模式** —— 适配[记住我]模式,重启浏览器免验证
- **密码加密** —— 提供密码加密模块可快速MD5、SHA1、SHA256、AES、RSA加密
- **组件自动注入** —— 零配置与Spring等框架集成
- **更多功能正在集成中...** —— 如有您有好想法或者建议,欢迎加群交流
@@ -106,7 +89,8 @@ StpUtil.checkLogin();
@SaCheckPermission("user:add")
@RequestMapping("/user/insert")
public String insert(SysUser user) {
return "用户增加";
// ...
return "用户增加";
}
```
@@ -118,19 +102,19 @@ StpUtil.logoutByLoginId(10001);
除了以上的示例sa-token还可以一行代码完成以下功能
``` java
StpUtil.setLoginId(10001); // 标记当前会话登录的账号id
StpUtil.getLoginId(); // 获取当前会话登录的账号id
StpUtil.isLogin(); // 获取当前会话是否已经登录, 返回true或false
StpUtil.logout(); // 当前会话注销登录
StpUtil.logoutByLoginId(10001); // 让账号为10001的会话注销登录踢人下线
StpUtil.hasRole("super-admin"); // 查询当前账号是否含有指定角色标识, 返回true或false
StpUtil.hasPermission("user:add"); // 查询当前账号是否含有指定权限, 返回true或false
StpUtil.getSession(); // 获取当前账号id的Session
StpUtil.getSessionByLoginId(10001); // 获取账号id为10001的Session
StpUtil.getTokenValueByLoginId(10001); // 获取账号id为10001的token令牌值
StpUtil.setLoginId(10001, "PC"); // 指定设备标识登录
StpUtil.logoutByLoginId(10001, "PC"); // 指定设备标识进行强制注销 (不同端不受影响)
StpUtil.switchTo(10044); // 将当前会话身份临时切换为其它账号
StpUtil.setLoginId(10001); // 标记当前会话登录的账号id
StpUtil.getLoginId(); // 获取当前会话登录的账号id
StpUtil.isLogin(); // 获取当前会话是否已经登录, 返回true或false
StpUtil.logout(); // 当前会话注销登录
StpUtil.logoutByLoginId(10001); // 让账号为10001的会话注销登录踢人下线
StpUtil.hasRole("super-admin"); // 查询当前账号是否含有指定角色标识, 返回true或false
StpUtil.hasPermission("user:add"); // 查询当前账号是否含有指定权限, 返回true或false
StpUtil.getSession(); // 获取当前账号id的Session
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众多请恕此处无法为您逐一展示更多示例请戳官方在线文档
@@ -198,16 +182,20 @@ sa-token秉承着开放的思想欢迎大家贡献代码为框架添砖加
## 友情链接
[**[ okhttps ]** 一个轻量级http通信框架API设计无比优雅支持 WebSocket 以及 Stomp 协议](https://gitee.com/ejlchina-zhxu/okhttps)
[**[ OkHttps ]** 一个轻量级http通信框架API设计无比优雅支持 WebSocket 以及 Stomp 协议](https://gitee.com/ejlchina-zhxu/okhttps)
[**[ 小诺快速开发平台 ]** 基于SpringBoot2 + AntDesignVue全新快速开发平台同时拥有三个版本](https://xiaonuo.vip/index#pricing)
## 交流群
QQ交流群[1002350610 点击加入](https://jq.qq.com/?_wv=1027&k=45H977HM)
![扫码加群](https://color-test.oss-cn-qingdao.aliyuncs.com/sa-token/qq-group.png ':size=150')
![扫码加群](https://color-test.oss-cn-qingdao.aliyuncs.com/sa-token/qq-group.png ':size=230')
**微信群**
**微信群**
![微信群](https://images.gitee.com/uploads/images/2021/0216/170729_25fcde8b_8073399.jpeg "sa-token-wx.jpg")
![微信群](https://dev33-test.oss-cn-beijing.aliyuncs.com/sa-token/i-wx-qr.png ':size=230')
(扫码添加微信备注sa-token邀您加入群聊)
<br>

View File

@@ -8,7 +8,7 @@
<groupId>cn.dev33</groupId>
<artifactId>sa-token-parent</artifactId>
<packaging>pom</packaging>
<version>1.13.0</version>
<version>1.14.0</version>
<!-- 项目介绍 -->
<name>sa-token</name>
@@ -38,7 +38,7 @@
<!-- 一些属性 -->
<properties>
<sa-token-version>1.13.0</sa-token-version>
<sa-token-version>1.14.0</sa-token-version>
<jdk.version>1.8</jdk.version>
<project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>utf-8</project.reporting.outputEncoding>

View File

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

View File

@@ -16,7 +16,7 @@ public class SaTokenActionDefaultImpl implements SaTokenAction {
/**
* 生成一个token
* 根据一定的算法生成一个token
*/
@Override
public String createToken(Object loginId, String loginKey) {

View File

@@ -66,9 +66,11 @@ public class SaTokenConfig {
/**
* @param tokenName token名称 (同时也是cookie名称)
* @return 对象自身
*/
public void setTokenName(String tokenName) {
public SaTokenConfig setTokenName(String tokenName) {
this.tokenName = tokenName;
return this;
}
/**
@@ -80,9 +82,11 @@ public class SaTokenConfig {
/**
* @param timeout token的长久有效期(单位:秒) 默认30天, -1代表永久
* @return 对象自身
*/
public void setTimeout(long timeout) {
public SaTokenConfig setTimeout(long timeout) {
this.timeout = timeout;
return this;
}
/**
@@ -96,9 +100,11 @@ public class SaTokenConfig {
/**
* @param activityTimeout token临时有效期 [指定时间内无操作就视为token过期] (单位: 秒), 默认-1 代表不限制
* (例如可以设置为1800代表30分钟内无操作就过期)
* @return 对象自身
*/
public void setActivityTimeout(long activityTimeout) {
public SaTokenConfig setActivityTimeout(long activityTimeout) {
this.activityTimeout = activityTimeout;
return this;
}
/**
@@ -110,9 +116,11 @@ public class SaTokenConfig {
/**
* @param allowConcurrentLogin 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
* @return 对象自身
*/
public void setAllowConcurrentLogin(Boolean allowConcurrentLogin) {
public SaTokenConfig setAllowConcurrentLogin(Boolean allowConcurrentLogin) {
this.allowConcurrentLogin = allowConcurrentLogin;
return this;
}
/**
@@ -124,9 +132,11 @@ public class SaTokenConfig {
/**
* @param isShare 在多人登录同一账号时是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
* @return 对象自身
*/
public void setIsShare(Boolean isShare) {
public SaTokenConfig setIsShare(Boolean isShare) {
this.isShare = isShare;
return this;
}
/**
@@ -138,9 +148,11 @@ public class SaTokenConfig {
/**
* @param isReadBody 是否尝试从请求体里读取token
* @return 对象自身
*/
public void setIsReadBody(Boolean isReadBody) {
public SaTokenConfig setIsReadBody(Boolean isReadBody) {
this.isReadBody = isReadBody;
return this;
}
/**
@@ -152,9 +164,11 @@ public class SaTokenConfig {
/**
* @param isReadHead 是否尝试从header里读取token
* @return 对象自身
*/
public void setIsReadHead(Boolean isReadHead) {
public SaTokenConfig setIsReadHead(Boolean isReadHead) {
this.isReadHead = isReadHead;
return this;
}
/**
@@ -166,9 +180,11 @@ public class SaTokenConfig {
/**
* @param isReadCookie 是否尝试从cookie里读取token
* @return 对象自身
*/
public void setIsReadCookie(Boolean isReadCookie) {
public SaTokenConfig setIsReadCookie(Boolean isReadCookie) {
this.isReadCookie = isReadCookie;
return this;
}
/**
@@ -180,9 +196,11 @@ public class SaTokenConfig {
/**
* @param tokenStyle token风格(默认可取值uuid、simple-uuid、random-32、random-64、random-128、tik)
* @return 对象自身
*/
public void setTokenStyle(String tokenStyle) {
public SaTokenConfig setTokenStyle(String tokenStyle) {
this.tokenStyle = tokenStyle;
return this;
}
/**
@@ -195,9 +213,11 @@ public class SaTokenConfig {
/**
* @param dataRefreshPeriod 默认dao层实现类中每次清理过期数据间隔的时间 (单位: 秒)
* 默认值30秒设置为-1代表不启动定时清理
* @return 对象自身
*/
public void setDataRefreshPeriod(int dataRefreshPeriod) {
public SaTokenConfig setDataRefreshPeriod(int dataRefreshPeriod) {
this.dataRefreshPeriod = dataRefreshPeriod;
return this;
}
/**
@@ -210,9 +230,11 @@ public class SaTokenConfig {
/**
* @param tokenSessionCheckLogin 获取[token专属session]时是否必须登录
* (如果配置为true会在每次获取[token-session]时校验是否登录)
* @return 对象自身
*/
public void setTokenSessionCheckLogin(Boolean tokenSessionCheckLogin) {
public SaTokenConfig setTokenSessionCheckLogin(Boolean tokenSessionCheckLogin) {
this.tokenSessionCheckLogin = tokenSessionCheckLogin;
return this;
}
/**
@@ -224,9 +246,11 @@ public class SaTokenConfig {
/**
* @param autoRenew 是否打开自动续签 (如果此值为true, 框架会在每次直接或间接调用getLoginId()时进行一次过期检查与续签操作)
* @return 对象自身
*/
public void setAutoRenew(Boolean autoRenew) {
public SaTokenConfig setAutoRenew(Boolean autoRenew) {
this.autoRenew = autoRenew;
return this;
}
/**
@@ -238,9 +262,11 @@ public class SaTokenConfig {
/**
* @param cookieDomain 写入Cookie时显式指定的作用域, 常用于单点登录二级域名共享Cookie的场景
* @return 对象自身
*/
public void setCookieDomain(String cookieDomain) {
public SaTokenConfig setCookieDomain(String cookieDomain) {
this.cookieDomain = cookieDomain;
return this;
}
/**
@@ -252,9 +278,11 @@ public class SaTokenConfig {
/**
* @param isV 是否在初始化配置时打印版本字符画
* @return 对象自身
*/
public void setIsV(Boolean isV) {
public SaTokenConfig setIsV(Boolean isV) {
this.isV = isV;
return this;
}

View File

@@ -44,7 +44,7 @@ public class SaTokenCookieUtil {
*/
public static void addCookie(HttpServletResponse response, String name, String value, String path, String domain, int timeout) {
Cookie cookie = new Cookie(name, value);
if(SaTokenInsideUtil.isEmpty(path) == false) {
if(SaTokenInsideUtil.isEmpty(path) == true) {
path = "/";
}
if(SaTokenInsideUtil.isEmpty(domain) == false) {
@@ -95,4 +95,4 @@ public class SaTokenCookieUtil {
}
}
}
}

View File

@@ -128,7 +128,7 @@ public interface SaTokenDao {
}
/**
* 更新指定session
* 更新指定session
* @param session 要更新的session对象
*/
public default void updateSession(SaSession session) {

View File

@@ -23,14 +23,27 @@ public class IsRunFunction {
}
/**
* 根据变量决定是否执行此函数
*
* 当 isRun == true 时执行此函数
* @param function 函数
* @return 对象自身
*/
public void exe(SaFunction function) {
public IsRunFunction exe(SaFunction function) {
if (isRun) {
function.run();
}
return this;
}
/**
* 当 isRun == false 时执行此函数
* @param function 函数
* @return 对象自身
*/
public IsRunFunction noExe(SaFunction function) {
if (!isRun) {
function.run();
}
return this;
}
}

View File

@@ -0,0 +1,61 @@
package cn.dev33.satoken.secure;
import java.io.UnsupportedEncodingException;
import java.util.Base64;
/**
* Base64工具类
* @author kong
*
*/
public class SaBase64Util {
private static Base64.Encoder encoder = Base64.getEncoder();
private static Base64.Decoder decoder = Base64.getDecoder();
/**
* Base64编码byte[] 转 String
* @param bytes byte[]
* @return 字符串
*/
public static String encodeBytesToString(byte[] bytes){
return encoder.encodeToString(bytes);
}
/**
* Base64解码String 转 byte[]
* @param text 字符串
* @return byte[]
*/
public static byte[] decodeStringToBytes(String text){
return decoder.decode(text);
}
/**
* Base64编码String 转 String
* @param text 字符串
* @return Base64格式字符串
*/
public static String encode(String text){
try {
return encoder.encodeToString(text.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* Base64解码String 转 String
* @param base64Text Base64格式字符串
* @return 字符串
*/
public static String decode(String base64Text){
try {
return new String(decoder.decode(base64Text), "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,443 @@
package cn.dev33.satoken.secure;
import java.security.InvalidParameterException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.UUID;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import cn.dev33.satoken.exception.SaTokenException;
/**
* sa-token 常见加密算法工具类
*
* @author kong
*
*/
public class SaSecureUtil {
/**
* Base64编码
*/
private static Base64.Encoder encoder = Base64.getEncoder();
/**
* Base64解码
*/
private static Base64.Decoder decoder = Base64.getDecoder();
// ----------------------- 摘要加密 -----------------------
/**
* md5加密
* @param str 指定字符串
* @return 加密后的字符串
*/
public static String md5(String str) {
str = (str == null ? "" : str);
char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
try {
byte[] btInput = str.getBytes();
MessageDigest mdInst = MessageDigest.getInstance("MD5");
mdInst.update(btInput);
byte[] md = mdInst.digest();
int j = md.length;
char[] strA = new char[j * 2];
int k = 0;
for (byte byte0 : md) {
strA[k++] = hexDigits[byte0 >>> 4 & 0xf];
strA[k++] = hexDigits[byte0 & 0xf];
}
return new String(strA);
} catch (Exception e) {
throw new SaTokenException(e);
}
}
/**
* sha1加密
*
* @param str 指定字符串
* @return 加密后的字符串
*/
public static String sha1(String str) {
try {
str = (str == null ? "" : str);
MessageDigest md = MessageDigest.getInstance("SHA1");
byte[] b = str.getBytes();
md.update(b);
byte[] b2 = md.digest();
int len = b2.length;
String strA = "0123456789abcdef";
char[] ch = strA.toCharArray();
char[] chs = new char[len * 2];
for (int i = 0, k = 0; i < len; i++) {
byte b3 = b2[i];
chs[k++] = ch[b3 >>> 4 & 0xf];
chs[k++] = ch[b3 & 0xf];
}
return new String(chs);
} catch (Exception e) {
throw new SaTokenException(e);
}
}
/**
* sha256加密
*
* @param str 指定字符串
* @return 加密后的字符串
*/
public static String sha256(String str) {
try {
str = (str == null ? "" : str);
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(str.getBytes("UTF-8"));
byte[] bytes = messageDigest.digest();
StringBuilder builder = new StringBuilder();
String temp;
for (int i = 0; i < bytes.length; i++) {
temp = Integer.toHexString(bytes[i] & 0xFF);
if (temp.length() == 1) {
builder.append("0");
}
builder.append(temp);
}
return builder.toString();
} catch (Exception e) {
throw new SaTokenException(e);
}
}
/**
* md5加盐加密: md5(md5(str) + md5(salt))
* @param str 字符串
* @param salt 盐
* @return 加密后的字符串
*/
public static String md5BySalt(String str, String salt) {
return md5(md5(str) + md5(salt));
}
// ----------------------- 对称加密 AES -----------------------
/**
* 默认密码算法
*/
private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
/**
* AES加密
*
* @param key 加密的密钥
* @param text 需要加密的字符串
* @return 返回Base64转码后的加密数据
*/
public static String aesEncrypt(String key, String text) {
try {
Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
byte[] byteContent = text.getBytes("utf-8");
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(key));
byte[] result = cipher.doFinal(byteContent);
return encoder.encodeToString(result);
} catch (Exception e) {
throw new SaTokenException(e);
}
}
/**
* AES解密
* @param key 加密的密钥
* @param text 已加密的密文
* @return 返回解密后的数据
*/
public static String aesDecrypt(String key, String text) {
try {
Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(key));
byte[] result = cipher.doFinal(decoder.decode(text));
return new String(result, "utf-8");
} catch (Exception e) {
throw new SaTokenException(e);
}
}
/**
* 生成加密秘钥
* @param password 秘钥
* @return SecretKeySpec
* @throws NoSuchAlgorithmException
*/
private static SecretKeySpec getSecretKey(final String password) throws NoSuchAlgorithmException {
KeyGenerator kg = KeyGenerator.getInstance("AES");
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
random.setSeed(password.getBytes());
kg.init(128, random);
SecretKey secretKey = kg.generateKey();
return new SecretKeySpec(secretKey.getEncoded(), "AES");
}
// ----------------------- 非对称加密 RSA -----------------------
private static final String ALGORITHM = "RSA";
private static final int KEY_SIZE = 1024;
// ---------- 5个常用方法
/**
* 生成密钥对
* @return Map对象 (private=私钥, public=公钥)
* @throws Exception 异常
*/
public static HashMap<String, String> rsaGenerateKeyPair() throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM);
KeyPair keyPair;
try {
keyPairGenerator.initialize(KEY_SIZE,
new SecureRandom(UUID.randomUUID().toString().replaceAll("-", "").getBytes()));
keyPair = keyPairGenerator.generateKeyPair();
} catch (InvalidParameterException e) {
throw e;
} catch (NullPointerException e) {
throw e;
}
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
HashMap<String, String> map = new HashMap<String, String>(16);
map.put("private", encoder.encodeToString(rsaPrivateKey.getEncoded()));
map.put("public", encoder.encodeToString(rsaPublicKey.getEncoded()));
return map;
}
/**
* RSA公钥加密
* @param publicKeyString 公钥
* @param content 内容
* @return 加密后内容
*/
public static String rsaEncryptByPublic(String publicKeyString, String content) {
try {
// 获得公钥对象
PublicKey publicKey = getPublicKeyFromString(publicKeyString);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
// 该密钥能够加密的最大字节长度
int splitLength = ((RSAPublicKey) publicKey).getModulus().bitLength() / 8 - 11;
byte[][] arrays = splitBytes(content.getBytes(), splitLength);
StringBuffer stringBuffer = new StringBuffer();
for (byte[] array : arrays) {
stringBuffer.append(bytesToHexString(cipher.doFinal(array)));
}
return stringBuffer.toString();
} catch (Exception e) {
throw new SaTokenException(e);
}
}
/**
* RSA私钥加密
* @param privateKeyString 私钥
* @param content 内容
* @return 加密后内容
*/
public static String rsaEncryptByPrivate(String privateKeyString, String content) {
try {
PrivateKey privateKey = getPrivateKeyFromString(privateKeyString);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
// 该密钥能够加密的最大字节长度
int splitLength = ((RSAPrivateKey) privateKey).getModulus().bitLength() / 8 - 11;
byte[][] arrays = splitBytes(content.getBytes(), splitLength);
StringBuffer stringBuffer = new StringBuffer();
for (byte[] array : arrays) {
stringBuffer.append(bytesToHexString(cipher.doFinal(array)));
}
return stringBuffer.toString();
} catch (Exception e) {
throw new SaTokenException(e);
}
}
/**
* RSA公钥解密
* @param publicKeyString 公钥
* @param content 已加密内容
* @return 解密后内容
*/
public static String rsaDecryptByPublic(String publicKeyString, String content) {
try {
PublicKey publicKey = getPublicKeyFromString(publicKeyString);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
// 该密钥能够加密的最大字节长度
int splitLength = ((RSAPublicKey) publicKey).getModulus().bitLength() / 8;
byte[] contentBytes = hexStringToBytes(content);
byte[][] arrays = splitBytes(contentBytes, splitLength);
StringBuffer stringBuffer = new StringBuffer();
for (byte[] array : arrays) {
stringBuffer.append(new String(cipher.doFinal(array)));
}
return stringBuffer.toString();
} catch (Exception e) {
throw new SaTokenException(e);
}
}
/**
* RSA私钥解密
* @param privateKeyString 公钥
* @param content 已加密内容
* @return 解密后内容
*/
public static String rsaDecryptByPrivate(String privateKeyString, String content) {
try {
PrivateKey privateKey = getPrivateKeyFromString(privateKeyString);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
// 该密钥能够加密的最大字节长度
int splitLength = ((RSAPrivateKey) privateKey).getModulus().bitLength() / 8;
byte[] contentBytes = hexStringToBytes(content);
byte[][] arrays = splitBytes(contentBytes, splitLength);
StringBuffer stringBuffer = new StringBuffer();
for (byte[] array : arrays) {
stringBuffer.append(new String(cipher.doFinal(array)));
}
return stringBuffer.toString();
} catch (Exception e) {
throw new SaTokenException(e);
}
}
// ---------- 获取*钥
/** 根据公钥字符串获取 公钥对象 */
private static PublicKey getPublicKeyFromString(String key)
throws NoSuchAlgorithmException, InvalidKeySpecException {
// 过滤掉\r\n
key = key.replace("\r\n", "");
// 取得公钥
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(decoder.decode(key));
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
PublicKey publicKey = keyFactory.generatePublic(x509KeySpec);
return publicKey;
}
/** 根据私钥字符串获取 私钥对象 */
private static PrivateKey getPrivateKeyFromString(String key)
throws NoSuchAlgorithmException, InvalidKeySpecException {
// 过滤掉\r\n
key = key.replace("\r\n", "");
// 取得私钥
PKCS8EncodedKeySpec x509KeySpec = new PKCS8EncodedKeySpec(decoder.decode(key));
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
PrivateKey privateKey = keyFactory.generatePrivate(x509KeySpec);
return privateKey;
}
// ---------- 一些辅助方法
/** 根据限定的每组字节长度,将字节数组分组 */
private static byte[][] splitBytes(byte[] bytes, int splitLength) {
// bytes与splitLength的余数
int remainder = bytes.length % splitLength;
// 数据拆分后的组数余数不为0时加1
int quotient = remainder != 0 ? bytes.length / splitLength + 1 : bytes.length / splitLength;
byte[][] arrays = new byte[quotient][];
byte[] array = null;
for (int i = 0; i < quotient; i++) {
// 如果是最后一组quotient-1,同时余数不等于0就将最后一组设置为remainder的长度
if (i == quotient - 1 && remainder != 0) {
array = new byte[remainder];
System.arraycopy(bytes, i * splitLength, array, 0, remainder);
} else {
array = new byte[splitLength];
System.arraycopy(bytes, i * splitLength, array, 0, splitLength);
}
arrays[i] = array;
}
return arrays;
}
/** 将字节数组转换成16进制字符串 */
private static String bytesToHexString(byte[] bytes) {
StringBuffer sb = new StringBuffer(bytes.length);
String temp = null;
for (int i = 0; i < bytes.length; i++) {
temp = Integer.toHexString(0xFF & bytes[i]);
if (temp.length() < 2) {
sb.append(0);
}
sb.append(temp);
}
return sb.toString();
}
/** 将16进制字符串转换成字节数组 */
private static byte[] hexStringToBytes(String hex) {
int len = (hex.length() / 2);
hex = hex.toUpperCase();
byte[] result = new byte[len];
char[] chars = hex.toCharArray();
for (int i = 0; i < len; i++) {
int pos = i * 2;
result[i] = (byte) (toByte(chars[pos]) << 4 | toByte(chars[pos + 1]));
}
return result;
}
/** 将char转换为byte */
private static byte toByte(char c) {
return (byte) "0123456789ABCDEF".indexOf(c);
}
}

View File

@@ -226,6 +226,64 @@ public class SaSession implements Serializable {
return dataMap;
}
/**
* 写入数据集合 (不改变底层对象只将此dataMap所有数据进行替换)
* @param dataMap 数据集合
*/
public void refreshDataMap(Map<String, Object> dataMap) {
this.dataMap.clear();
this.dataMap.putAll(dataMap);
this.update();
}
// ----------------------- 存取值 (类型转换)
/**
* 从Session中取值并转化为Object类型
* @param key key
* @return 值
*/
public Object getObject(String key) {
return getAttribute(key);
}
/**
* 从Session中取值并转化为String类型
* @param key key
* @return 值
*/
public String getString(String key) {
Object value = getObject(key);
if(value == null) {
return null;
}
return String.valueOf(value);
}
/**
* 从Session中取值并转化为int类型如果value为空则返回0
* @param key key
* @return 值
*/
public int getInt(String key) {
Object value = getObject(key);
if(valueIsNull(value)) {
return 0;
}
return Integer.valueOf(String.valueOf(value));
}
/**
* 判断一个值是否为null
* @param value 指定值
* @return 此value是否为null
*/
public boolean valueIsNull(Object value) {
return value == null || value.equals("");
}
// ----------------------- 一些操作
/**

View File

@@ -21,7 +21,7 @@ public class SaSessionCustomUtil {
* @param sessionId 会话id
* @return sessionId
*/
public static String getSessionKey(String sessionId) {
public static String splicingSessionKey(String sessionId) {
return SaTokenManager.getConfig().getTokenName() + ":" + sessionKey + ":session:" + sessionId;
}
@@ -32,7 +32,7 @@ public class SaSessionCustomUtil {
* @return 是否存在
*/
public boolean isExists(String sessionId) {
return SaTokenManager.getSaTokenDao().getSession(getSessionKey(sessionId)) != null;
return SaTokenManager.getSaTokenDao().getSession(splicingSessionKey(sessionId)) != null;
}
/**
@@ -43,7 +43,7 @@ public class SaSessionCustomUtil {
* @return SaSession
*/
public static SaSession getSessionById(String sessionId, boolean isCreate) {
SaSession session = SaTokenManager.getSaTokenDao().getSession(getSessionKey(sessionId));
SaSession session = SaTokenManager.getSaTokenDao().getSession(splicingSessionKey(sessionId));
if (session == null && isCreate) {
session = SaTokenManager.getSaTokenAction().createSession(sessionId);
SaTokenManager.getSaTokenDao().setSession(session, SaTokenManager.getConfig().getTimeout());
@@ -67,7 +67,7 @@ public class SaSessionCustomUtil {
* @param sessionId 指定key
*/
public static void deleteSessionById(String sessionId) {
SaTokenManager.getSaTokenDao().deleteSession(getSessionKey(sessionId));
SaTokenManager.getSaTokenDao().deleteSession(splicingSessionKey(sessionId));
}
}

View File

@@ -0,0 +1,142 @@
package cn.dev33.satoken.stp;
import cn.dev33.satoken.SaTokenManager;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.util.SaTokenConsts;
/**
* 调用 `StpUtil.setLogin()` 时的 [配置参数 Model ]
* @author kong
*
*/
public class SaLoginModel {
/**
* 此次登录的客户端设备标识
*/
public String device;
/**
* 是否为持久Cookie临时Cookie在浏览器关闭时会自动删除持久Cookie在重新打开后依然存在
*/
public Boolean isLastingCookie;
/**
* 指定此次登录token的有效期, 单位:秒 如未指定自动取全局配置的timeout值
*/
public Long timeout;
/**
* @return device
*/
public String getDevice() {
return device;
}
/**
* @param device 要设置的 device
* @return 对象自身
*/
public SaLoginModel setDevice(String device) {
this.device = device;
return this;
}
/**
* @return isLastingCookie
*/
public Boolean getIsLastingCookie() {
return isLastingCookie;
}
/**
* @param isLastingCookie 要设置的 isLastingCookie
* @return 对象自身
*/
public SaLoginModel setIsLastingCookie(Boolean isLastingCookie) {
this.isLastingCookie = isLastingCookie;
return this;
}
/**
* @return timeout
*/
public Long getTimeout() {
return timeout;
}
/**
* @param timeout 要设置的 timeout
* @return 对象自身
*/
public SaLoginModel setTimeout(long timeout) {
this.timeout = timeout;
return this;
}
/**
* @return cookie时长
*/
public int getCookieTimeout() {
if(isLastingCookie == false) {
return -1;
}
if(timeout == SaTokenDao.NEVER_EXPIRE) {
return Integer.MAX_VALUE;
}
return (int)(long)timeout;
}
/**
* 构建对象,初始化默认值
* @return 对象自身
*/
public SaLoginModel build() {
return build(SaTokenManager.getConfig());
}
/**
* 构建对象,初始化默认值
* @param config 配置对象
* @return 对象自身
*/
public SaLoginModel build(SaTokenConfig config) {
if(device == null) {
device = SaTokenConsts.DEFAULT_LOGIN_DEVICE;
}
if(isLastingCookie == null) {
isLastingCookie = true;
}
if(timeout == null) {
timeout = config.getTimeout();
}
return this;
}
/**
* 静态方法获取一个 SaLoginModel 对象
* @return SaLoginModel 对象
*/
public static SaLoginModel create() {
return new SaLoginModel();
}
/**
* toString
*/
@Override
public String toString() {
return "SaLoginModel [device=" + device + ", isLastingCookie=" + isLastingCookie + ", timeout=" + timeout + "]";
}
}

View File

@@ -8,6 +8,7 @@ import java.util.Objects;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import cn.dev33.satoken.SaTokenManager;
import cn.dev33.satoken.annotation.SaCheckLogin;
@@ -71,7 +72,7 @@ public class StpLogic {
* @return 此StpLogic的token名称
*/
public String getTokenName() {
return getKeyTokenName();
return splicingKeyTokenName();
}
/**
@@ -95,8 +96,8 @@ public class StpLogic {
String tokenValue = null;
// 1. 尝试从request里读取
if(request.getAttribute(getKeyJustCreatedSave()) != null) {
tokenValue = String.valueOf(request.getAttribute(getKeyJustCreatedSave()));
if(request.getAttribute(splicingKeyJustCreatedSave()) != null) {
tokenValue = String.valueOf(request.getAttribute(splicingKeyJustCreatedSave()));
}
// 2. 尝试从请求体里面读取
if(tokenValue == null && config.getIsReadBody()){
@@ -145,20 +146,39 @@ public class StpLogic {
* @param loginId 登录id建议的类型long | int | String
*/
public void setLoginId(Object loginId) {
setLoginId(loginId, SaTokenConsts.DEFAULT_LOGIN_DEVICE);
setLoginId(loginId, new SaLoginModel());
}
/**
* 在当前会话上登录id
* 在当前会话上登录id, 并指定登录设备
* @param loginId 登录id建议的类型long | int | String
* @param device 设备标识
*/
public void setLoginId(Object loginId, String device) {
setLoginId(loginId, new SaLoginModel().setDevice(device));
}
/**
* 在当前会话上登录id, 并指定登录设备
* @param loginId 登录id建议的类型long | int | String
* @param isLastingCookie 是否为持久Cookie
*/
public void setLoginId(Object loginId, boolean isLastingCookie) {
setLoginId(loginId, new SaLoginModel().setIsLastingCookie(isLastingCookie));
}
/**
* 在当前会话上登录id, 并指定所有登录参数Model
* @param loginId 登录id建议的类型long | int | String
* @param loginModel 此次登录的参数Model
*/
public void setLoginId(Object loginId, SaLoginModel loginModel) {
// ------ 1、获取相应对象
HttpServletRequest request = SaTokenManager.getSaTokenServlet().getRequest();
SaTokenConfig config = getConfig();
SaTokenDao dao = SaTokenManager.getSaTokenDao();
loginModel.build(config);
// ------ 2、生成一个token
String tokenValue = null;
@@ -166,7 +186,7 @@ public class StpLogic {
if(config.getAllowConcurrentLogin() == true) {
// 如果配置为共享token, 则尝试从Session签名记录里取出token
if(config.getIsShare() == true) {
tokenValue = getTokenValueByLoginId(loginId, device);
tokenValue = getTokenValueByLoginId(loginId, loginModel.getDevice());
}
} else {
// --- 如果不允许并发登录
@@ -175,9 +195,9 @@ public class StpLogic {
if(session != null) {
List<TokenSign> tokenSignList = session.getTokenSignList();
for (TokenSign tokenSign : tokenSignList) {
if(tokenSign.getDevice().equals(device)) {
if(tokenSign.getDevice().equals(loginModel.getDevice())) {
// 1. 将此token 标记为已顶替
dao.update(getKeyTokenValue(tokenSign.getValue()), NotLoginException.BE_REPLACED);
dao.update(splicingKeyTokenValue(tokenSign.getValue()), NotLoginException.BE_REPLACED);
// 2. 清理掉[token-最后操作时间]
clearLastActivity(tokenSign.getValue());
// 3. 清理账号session上的token签名记录
@@ -196,22 +216,26 @@ public class StpLogic {
if(session == null) {
session = getSessionByLoginId(loginId);
} else {
dao.updateSessionTimeout(session.getId(), config.getTimeout());
// 保证此Session的有效期 >= token的有效期
if(dao.getSessionTimeout(session.getId()) < loginModel.getTimeout()) {
dao.updateSessionTimeout(session.getId(), loginModel.getTimeout());
}
}
// 在session上记录token签名
session.addTokenSign(new TokenSign(tokenValue, device));
session.addTokenSign(new TokenSign(tokenValue, loginModel.getDevice()));
// ------ 4. 持久化其它数据
// token -> uid
dao.set(getKeyTokenValue(tokenValue), String.valueOf(loginId), config.getTimeout());
dao.set(splicingKeyTokenValue(tokenValue), String.valueOf(loginId), loginModel.getTimeout());
// 将token保存到本次request里
request.setAttribute(getKeyJustCreatedSave(), tokenValue);
request.setAttribute(splicingKeyJustCreatedSave(), tokenValue);
// 写入 [最后操作时间]
setLastActivityToNow(tokenValue);
// cookie注入
if(config.getIsReadCookie() == true){
SaTokenManager.getSaTokenCookie().addCookie(SaTokenManager.getSaTokenServlet().getResponse(), getTokenName(), tokenValue,
"/", config.getCookieDomain(), (int)config.getTimeout());
// 注入Cookie
if(config.getIsReadCookie() == true){
HttpServletResponse response = SaTokenManager.getSaTokenServlet().getResponse();
SaTokenManager.getSaTokenCookie().addCookie(response, getTokenName(), tokenValue,
"/", config.getCookieDomain(), loginModel.getCookieTimeout());
}
}
@@ -244,7 +268,7 @@ public class StpLogic {
if(loginId == null || NotLoginException.ABNORMAL_LIST.contains(loginId)) {
return;
}
SaTokenManager.getSaTokenDao().delete(getKeyTokenValue(tokenValue));
SaTokenManager.getSaTokenDao().delete(splicingKeyTokenValue(tokenValue));
// 3. 尝试清理账号session上的token签名 (如果为null或已被标记为异常, 那么无需继续执行 )
SaSession session = getSessionByLoginId(loginId, false);
@@ -288,7 +312,7 @@ public class StpLogic {
// 2. 清理掉[token-最后操作时间]
clearLastActivity(tokenValue);
// 3. 标记:已被踢下线
SaTokenManager.getSaTokenDao().update(getKeyTokenValue(tokenValue), NotLoginException.KICK_OUT);
SaTokenManager.getSaTokenDao().update(splicingKeyTokenValue(tokenValue), NotLoginException.KICK_OUT);
// 4. 清理账号session上的token签名
session.removeTokenSign(tokenValue);
}
@@ -450,7 +474,7 @@ public class StpLogic {
* @return loginId
*/
public String getLoginIdNotHandle(String tokenValue) {
return SaTokenManager.getSaTokenDao().get(getKeyTokenValue(tokenValue));
return SaTokenManager.getSaTokenDao().get(splicingKeyTokenValue(tokenValue));
}
@@ -487,7 +511,7 @@ public class StpLogic {
* @return SaSession
*/
public SaSession getSessionByLoginId(Object loginId, boolean isCreate) {
return getSessionBySessionId(getKeySession(loginId), isCreate);
return getSessionBySessionId(splicingKeySession(loginId), isCreate);
}
/**
@@ -526,7 +550,7 @@ public class StpLogic {
* @return session会话
*/
public SaSession getTokenSessionByToken(String tokenValue, boolean isCreate) {
return getSessionBySessionId(getKeyTokenSession(tokenValue), isCreate);
return getSessionBySessionId(splicingKeyTokenSession(tokenValue), isCreate);
}
/**
@@ -535,7 +559,7 @@ public class StpLogic {
* @return session会话
*/
public SaSession getTokenSessionByToken(String tokenValue) {
return getSessionBySessionId(getKeyTokenSession(tokenValue), true);
return getSessionBySessionId(splicingKeyTokenSession(tokenValue), true);
}
/**
@@ -554,18 +578,19 @@ public class StpLogic {
// 随机一个token送给Ta
tokenValue = createTokenValue(null);
// Request做上标记
SaTokenManager.getSaTokenServlet().getRequest().setAttribute(getKeyJustCreatedSave(), tokenValue);
SaTokenManager.getSaTokenServlet().getRequest().setAttribute(splicingKeyJustCreatedSave(), tokenValue);
// 写入 [最后操作时间]
setLastActivityToNow(tokenValue);
// cookie注入
if(getConfig().getIsReadCookie() == true){
int cookieTimeout = (int)(getConfig().getTimeout() == SaTokenDao.NEVER_EXPIRE ? Integer.MAX_VALUE : getConfig().getTimeout());
SaTokenManager.getSaTokenCookie().addCookie(SaTokenManager.getSaTokenServlet().getResponse(), getTokenName(), tokenValue,
"/", getConfig().getCookieDomain(), (int)getConfig().getTimeout());
"/", getConfig().getCookieDomain(), cookieTimeout);
}
}
}
// 返回这个token对应的专属session
return getSessionBySessionId(getKeyTokenSession(getTokenValue()), isCreate);
return getSessionBySessionId(splicingKeyTokenSession(getTokenValue()), isCreate);
}
/**
@@ -589,7 +614,7 @@ public class StpLogic {
return;
}
// 将[最后操作时间]标记为当前时间戳
SaTokenManager.getSaTokenDao().set(getKeyLastActivityTime(tokenValue), String.valueOf(System.currentTimeMillis()), getConfig().getTimeout());
SaTokenManager.getSaTokenDao().set(splicingKeyLastActivityTime(tokenValue), String.valueOf(System.currentTimeMillis()), getConfig().getTimeout());
}
/**
@@ -602,7 +627,7 @@ public class StpLogic {
return;
}
// 删除[最后操作时间]
SaTokenManager.getSaTokenDao().delete(getKeyLastActivityTime(tokenValue));
SaTokenManager.getSaTokenDao().delete(splicingKeyLastActivityTime(tokenValue));
// 清除标记
SaTokenManager.getSaTokenServlet().getRequest().removeAttribute(SaTokenConsts.TOKEN_ACTIVITY_TIMEOUT_CHECKED_KEY);
}
@@ -654,7 +679,7 @@ public class StpLogic {
if(tokenValue == null || getConfig().getActivityTimeout() == SaTokenDao.NEVER_EXPIRE) {
return;
}
SaTokenManager.getSaTokenDao().update(getKeyLastActivityTime(tokenValue), String.valueOf(System.currentTimeMillis()));
SaTokenManager.getSaTokenDao().update(splicingKeyLastActivityTime(tokenValue), String.valueOf(System.currentTimeMillis()));
}
/**
@@ -674,7 +699,7 @@ public class StpLogic {
* @return token剩余有效时间
*/
public long getTokenTimeout() {
return SaTokenManager.getSaTokenDao().getTimeout(getKeyTokenValue(getTokenValue()));
return SaTokenManager.getSaTokenDao().getTimeout(splicingKeyTokenValue(getTokenValue()));
}
/**
@@ -683,7 +708,7 @@ public class StpLogic {
* @return token剩余有效时间
*/
public long getTokenTimeoutByLoginId(Object loginId) {
return SaTokenManager.getSaTokenDao().getTimeout(getKeyTokenValue(getTokenValueByLoginId(loginId)));
return SaTokenManager.getSaTokenDao().getTimeout(splicingKeyTokenValue(getTokenValueByLoginId(loginId)));
}
/**
@@ -700,7 +725,7 @@ public class StpLogic {
* @return token剩余有效时间
*/
public long getSessionTimeoutByLoginId(Object loginId) {
return SaTokenManager.getSaTokenDao().getSessionTimeout(getKeySession(loginId));
return SaTokenManager.getSaTokenDao().getSessionTimeout(splicingKeySession(loginId));
}
/**
@@ -717,7 +742,7 @@ public class StpLogic {
* @return token剩余有效时间
*/
public long getTokenSessionTimeoutByTokenValue(String tokenValue) {
return SaTokenManager.getSaTokenDao().getSessionTimeout(getKeyTokenSession(tokenValue));
return SaTokenManager.getSaTokenDao().getSessionTimeout(splicingKeyTokenSession(tokenValue));
}
/**
@@ -744,7 +769,7 @@ public class StpLogic {
}
// ------ 开始查询
// 获取相关数据
String keyLastActivityTime = getKeyLastActivityTime(tokenValue);
String keyLastActivityTime = splicingKeyLastActivityTime(tokenValue);
String lastActivityTimeString = SaTokenManager.getSaTokenDao().get(keyLastActivityTime);
// 查不到,返回-2
if(lastActivityTimeString == null) {
@@ -990,7 +1015,7 @@ public class StpLogic {
* @return token集合
*/
public List<String> searchTokenValue(String keyword, int start, int size) {
return SaTokenManager.getSaTokenDao().searchData(getKeyTokenValue(""), keyword, start, size);
return SaTokenManager.getSaTokenDao().searchData(splicingKeyTokenValue(""), keyword, start, size);
}
/**
@@ -1001,7 +1026,7 @@ public class StpLogic {
* @return sessionId集合
*/
public List<String> searchSessionId(String keyword, int start, int size) {
return SaTokenManager.getSaTokenDao().searchData(getKeySession(""), keyword, start, size);
return SaTokenManager.getSaTokenDao().searchData(splicingKeySession(""), keyword, start, size);
}
/**
@@ -1012,7 +1037,7 @@ public class StpLogic {
* @return sessionId集合
*/
public List<String> searchTokenSessionId(String keyword, int start, int size) {
return SaTokenManager.getSaTokenDao().searchData(getKeyTokenSession(""), keyword, start, size);
return SaTokenManager.getSaTokenDao().searchData(splicingKeyTokenSession(""), keyword, start, size);
}
@@ -1022,7 +1047,7 @@ public class StpLogic {
* 获取key客户端 tokenName
* @return key
*/
public String getKeyTokenName() {
public String splicingKeyTokenName() {
return getConfig().getTokenName();
}
@@ -1031,7 +1056,7 @@ public class StpLogic {
* @param tokenValue token值
* @return key
*/
public String getKeyTokenValue(String tokenValue) {
public String splicingKeyTokenValue(String tokenValue) {
return getConfig().getTokenName() + ":" + loginKey + ":token:" + tokenValue;
}
@@ -1040,7 +1065,7 @@ public class StpLogic {
* @param loginId 账号id
* @return key
*/
public String getKeySession(Object loginId) {
public String splicingKeySession(Object loginId) {
return getConfig().getTokenName() + ":" + loginKey + ":session:" + loginId;
}
@@ -1049,7 +1074,7 @@ public class StpLogic {
* @param tokenValue token值
* @return key
*/
public String getKeyTokenSession(String tokenValue) {
public String splicingKeyTokenSession(String tokenValue) {
return getConfig().getTokenName() + ":" + loginKey + ":token-session:" + tokenValue;
}
@@ -1058,7 +1083,7 @@ public class StpLogic {
* @param tokenValue token值
* @return key
*/
public String getKeyLastActivityTime(String tokenValue) {
public String splicingKeyLastActivityTime(String tokenValue) {
return getConfig().getTokenName() + ":" + loginKey + ":last-activity:" + tokenValue;
}
@@ -1066,7 +1091,7 @@ public class StpLogic {
* 在进行身份切换时使用的存储key
* @return key
*/
public String getKeySwitch() {
public String splicingKeySwitch() {
return SaTokenConsts.SWITCH_TO_SAVE_KEY + getLoginKey();
}
@@ -1074,7 +1099,7 @@ public class StpLogic {
* 如果token为本次请求新创建的则以此字符串为key存储在当前request中
* @return key
*/
public String getKeyJustCreatedSave() {
public String splicingKeyJustCreatedSave() {
return SaTokenConsts.JUST_CREATED_SAVE_KEY + getLoginKey();
}
@@ -1159,14 +1184,14 @@ public class StpLogic {
* @param loginId 指定loginId
*/
public void switchTo(Object loginId) {
SaTokenManager.getSaTokenServlet().getRequest().setAttribute(getKeySwitch(), loginId);
SaTokenManager.getSaTokenServlet().getRequest().setAttribute(splicingKeySwitch(), loginId);
}
/**
* 结束临时切换身份
*/
public void endSwitch() {
SaTokenManager.getSaTokenServlet().getRequest().removeAttribute(getKeySwitch());
SaTokenManager.getSaTokenServlet().getRequest().removeAttribute(splicingKeySwitch());
}
/**
@@ -1174,7 +1199,7 @@ public class StpLogic {
* @return 是否正处于[身份临时切换]中
*/
public boolean isSwitch() {
return SaTokenManager.getSaTokenServlet().getRequest().getAttribute(getKeySwitch()) != null;
return SaTokenManager.getSaTokenServlet().getRequest().getAttribute(splicingKeySwitch()) != null;
}
/**
@@ -1182,7 +1207,7 @@ public class StpLogic {
* @return 返回[身份临时切换]的loginId
*/
public Object getSwitchLoginId() {
return SaTokenManager.getSaTokenServlet().getRequest().getAttribute(getKeySwitch());
return SaTokenManager.getSaTokenServlet().getRequest().getAttribute(splicingKeySwitch());
}
/**

View File

@@ -64,13 +64,31 @@ public class StpUtil {
}
/**
* 在当前会话上登录id
* 在当前会话上登录id, 并指定登录设备
* @param loginId 登录id建议的类型long | int | String
* @param device 设备标识
*/
public static void setLoginId(Object loginId, String device) {
stpLogic.setLoginId(loginId, device);
}
/**
* 在当前会话上登录id, 并指定登录设备
* @param loginId 登录id建议的类型long | int | String
* @param isLastingCookie 是否为持久Cookie
*/
public static void setLoginId(Object loginId, boolean isLastingCookie) {
stpLogic.setLoginId(loginId, isLastingCookie);
}
/**
* 在当前会话上登录id, 并指定所有登录参数Model
* @param loginId 登录id建议的类型long | int | String
* @param loginModel 此次登录的参数Model
*/
public static void setLoginId(Object loginId, SaLoginModel loginModel) {
stpLogic.setLoginId(loginId, loginModel);
}
/**
* 当前会话注销登录

View File

@@ -13,7 +13,7 @@ public class SaTokenConsts {
/**
* sa-token 版本号
*/
public static final String VERSION_NO = "v1.13.0";
public static final String VERSION_NO = "v1.14.0";
/**
* sa-token 开源地址

View File

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

View File

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

View File

@@ -16,7 +16,7 @@
<!-- 定义sa-token版本号 -->
<properties>
<sa-token-version>1.13.0</sa-token-version>
<sa-token-version>1.14.0</sa-token-version>
</properties>
<dependencies>

View File

@@ -48,9 +48,9 @@ public class SaTokenJwtUtil {
*/
public static String createToken(Object loginId) {
// 判断,不可使用默认秘钥
if(BASE64_SECURITY.equals("79e7c69681b8270162386e6daa53d1d3")) {
throw new SaTokenException("请更换秘钥");
}
// if(BASE64_SECURITY.equals("79e7c69681b8270162386e6daa53d1dd")) {
// throw new SaTokenException("请更换秘钥");
// }
// 在这里你可以使用官方提供的claim方法构建载荷也可以使用setPayload自定义载荷但是两者不可一起使用
JwtBuilder builder = Jwts.builder()
.setHeaderParam("type", "JWT")
@@ -128,7 +128,7 @@ public class SaTokenJwtUtil {
SaTokenConfig config = getConfig();
// ------ 2、生成一个token
String tokenValue = createTokenValue(loginId);
request.setAttribute(getKeyJustCreatedSave(), tokenValue); // 将token保存到本次request里
request.setAttribute(splicingKeyJustCreatedSave(), tokenValue); // 将token保存到本次request里
if(config.getIsReadCookie() == true){ // cookie注入
SaTokenManager.getSaTokenCookie().addCookie(SaTokenManager.getSaTokenServlet().getResponse(), getTokenName(), tokenValue, "/", config.getCookieDomain(), (int)config.getTimeout());
}

View File

@@ -16,6 +16,8 @@
<properties>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<!-- 定义sa-token版本号 -->
<sa-token-version>1.14.0</sa-token-version>
</properties>
<dependencies>
@@ -30,7 +32,7 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.13.0</version>
<version>${sa-token-version}</version>
</dependency>
<!-- OkHttps网络请求库 http://okhttps.ejlchina.com/ -->

View File

@@ -16,6 +16,8 @@
<properties>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<!-- 定义sa-token版本号 -->
<sa-token-version>1.14.0</sa-token-version>
</properties>
<dependencies>
@@ -30,21 +32,21 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.13.0</version>
<version>${sa-token-version}</version>
</dependency>
<!-- sa-token 实现 oauth2.0 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-oauth2</artifactId>
<version>1.13.0-alpha</version>
<version>1.14.0-alpha</version>
</dependency>
<!-- sa-token整合redis (使用jackson序列化方式) -->
<!-- <dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.13.0</version>
<version>${sa-token-version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>

View File

@@ -16,7 +16,7 @@
<!-- 定义sa-token版本号 -->
<properties>
<sa-token-version>1.13.0</sa-token-version>
<sa-token-version>1.14.0</sa-token-version>
</properties>
<dependencies>

View File

@@ -1,67 +0,0 @@
package com.pj;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
/**
* 跨域过滤器
* @author kong
*/
@Component
public class CorsFilter implements Filter {
static final String OPTIONS = "OPTIONS";
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 获得客户端domain
String origin = request.getHeader("Origin");
if (origin == null) {
origin = request.getHeader("Referer");
}
// 允许指定域访问跨域资源
response.setHeader("Access-Control-Allow-Origin", origin);
// 允许客户端携带跨域cookie此时origin值不能为“*”,只能为指定单一域名
response.setHeader("Access-Control-Allow-Credentials", "true");
// 允许所有请求方式
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
// 有效时间
response.setHeader("Access-Control-Max-Age", "3600");
// 允许的header参数
response.setHeader("Access-Control-Allow-Headers", "x-requested-with,satoken");
// 允许的header参数
// response.setHeader("Access-Control-Allow-Headers", "*");
// 如果是预检请求,直接返回
if (OPTIONS.equals(request.getMethod())) {
System.out.println("=======================浏览器发来了OPTIONS预检请求==========");
response.getWriter().print("");
return;
}
// System.out.println("*********************************过滤器被使用**************************2233");
chain.doFilter(req, res);
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
}

View File

@@ -9,6 +9,8 @@ import cn.dev33.satoken.interceptor.SaAnnotationInterceptor;
/**
* sa-token代码方式进行配置
* @author kong
*
*/
@Configuration
public class MySaTokenConfig implements WebMvcConfigurer {
@@ -34,5 +36,4 @@ public class MySaTokenConfig implements WebMvcConfigurer {
registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**");
}
}

View File

@@ -21,10 +21,10 @@ import cn.dev33.satoken.exception.NotRoleException;
@RestControllerAdvice // 可指定包前缀,比如:(basePackages = "com.pj.admin")
public class GlobalException {
// 在每个控制器之前触发的操作
// 在当前类每个方法进入之前触发的操作
@ModelAttribute
public void get(HttpServletRequest request) throws IOException {
}

View File

@@ -3,7 +3,8 @@ package com.pj.test;
import java.util.Date;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@@ -229,26 +230,25 @@ public class TestController {
return AjaxJson.getSuccess();
}
@Autowired
TestService TestService;
// 测试AOP注解鉴权 http://localhost:8081/test/testAOP
@RequestMapping("testAOP")
public AjaxJson testAOP() {
System.out.println("testAOP");
TestService.getList();
return AjaxJson.getSuccess();
// 测试指定设备登录 浏览器访问: http://localhost:8081/test/loginByDevice
@RequestMapping("loginByDevice")
public AjaxJson loginByDevice() {
System.out.println("--------------");
StpUtil.setLoginId(10001, "PC");
return AjaxJson.getSuccessData("登录成功");
}
// 测试 浏览器访问: http://localhost:8081/test/test
@RequestMapping("test")
public AjaxJson test() {
// StpUtil.getTokenSession().logout();
// StpUtil.logoutByLoginId(10001);
StpUtil.getLoginId();
return AjaxJson.getSuccess();
public AjaxJson test(HttpServletResponse response) {
System.out.println("进来了");
// StpUtil.setLoginId(10001, new SaLoginModel()
// .setDevice("PC") // 此次登录的客户端设备标识, 用于[同端互斥登录]时指定此次登录的设备名称
// .setIsLastingCookie(true) // 是否为持久Cookie临时Cookie在浏览器关闭时会自动删除持久Cookie在重新打开后依然存在
// .setTimeout(60 * 60 * 24 * 7) // 指定此次登录token的有效期, 单位:秒 如未指定自动取全局配置的timeout值
// );
return AjaxJson.getSuccess("访问成功");
}
// 测试 浏览器访问: http://localhost:8081/test/test2

View File

@@ -1,25 +0,0 @@
package com.pj.test;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import cn.dev33.satoken.annotation.SaCheckLogin;
/**
* 用来测试AOP注解鉴权
* @author kong
*
*/
@Service
public class TestService {
@SaCheckLogin
public List<String> getList() {
System.out.println("getList");
return new ArrayList<String>();
}
}

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">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">sa-token v1.13.0</h1>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">sa-token v1.14.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.13.0-2B9939"></a>
<a href="https://github.com/click33/sa-token"><img src="https://img.shields.io/badge/sa--token-v1.14.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>
@@ -28,29 +28,11 @@
## Sa-Token是什么
sa-token是一个轻量级Java权限认证框架主要解决登录认证、权限认证、Session会话、单点登录 等一系列权限相关问题
sa-token是一个轻量级Java权限认证框架主要解决登录认证、权限认证、Session会话、单点登录、OAuth2.0 等一系列权限相关问题
近年来有关权限认证的解决方案层出不穷例如单点登录、OAuth2.0、分布式Session等等难题无一不有着各种优秀框架大行其道
框架针对踢人下线、自动续签、前后台分离、分布式会话……等常见业务进行N多适配通过sa-token你可以以一种极简的方式实现系统的权限认证部分
然而当我们把视线放低那些最基础的有如登录认证、权限认证、Session会话等基础问题却一直被两大上古神兽 `Apache Shiro``Spring Security` 所把持
在此并非专门diss此两大框架诚然两大框架背景强大历史悠久其生态也比较齐全。但是它们毕竟已经是十几年前的产物那是一个还在用 `jsp` 写页面的时代两大框架的很多功能都是为jsp那一套量身定做。
在前后台分离已成标配的今天,两大框架的很多设计理念已经比较滞后,已经不能和我们的项目进行无缝适配,很多功能点都需要进行二次封装,甚至找一大堆扩展插件才能集成,已经逐渐不太适合现代化项目的应用
所以,为什么不能有一个自底向上,从最基础的登录、权限做起,以业务需求为核心,做到开箱即用的轻量级权限认证框架?
秉承着这个目的,`sa-token` 诞生了!
## Sa-Token 优点?
sa-token架构设计精简不引入各种复杂的概念如丝般顺滑的API调用大量高级特性统统只需一行代码即可实现
- 在sa-token之前权限认证业务概念纷飞、代码复杂在sa-token之后权限认证将会变成逻辑清晰架构简单人人可写
- 在sa-token之前你会在百度上频繁搜索: xx框架如何集成Redis前后台分离下如何鉴权踢人下线的原理是什么在sa-token之后你将轻松知道这一切的答案
- 在sa-token之前权限认证、分布式会话、单点登录、多账号认证你需要找不同的框架在sa-token之后你只需要这一个框架就足够了
与其它权限认证框架相比,你将会从以下方面感受到 `sa-token` 的优势:
与其它权限认证框架相比,`sa-token` 具有以下优势:
1. **简单** :可零配置启动框架,真正的开箱即用,低成本上手
2. **强大** :目前已集成几十项权限相关特性,涵盖了大部分业务场景的解决方案
3. **易用** 如丝般顺滑的API调用大量高级特性统统只需一行代码即可实现
@@ -58,7 +40,6 @@ sa-token架构设计精简不引入各种复杂的概念如丝般顺滑的
有了sa-token你所有的权限认证问题都不再是问题
## Sa-Token 能做什么?
- **登录验证** —— 轻松登录鉴权,并提供五种细分场景值
- **权限验证** —— 适配RBAC权限模型不同角色不同授权
@@ -74,9 +55,11 @@ sa-token架构设计精简不引入各种复杂的概念如丝般顺滑的
- **多账号认证体系** —— 比如一个商城项目的user表和admin表分开鉴权
- **花式token生成** —— 内置六种token风格还可自定义token生成策略
- **注解式鉴权** —— 优雅的将鉴权与业务代码分离
- **路由拦截式鉴权** —— 设定全局路由拦截,并排除指定路由
- **路由拦截式鉴权** —— 根据路由拦截鉴权可适配restful模式
- **自动续签** —— 提供两种token过期策略灵活搭配使用还可自动续签
- **会话治理** —— 提供方便灵活的会话查询接口
- **记住我模式** —— 适配[记住我]模式,重启浏览器免验证
- **密码加密** —— 提供密码加密模块可快速MD5、SHA1、SHA256、AES、RSA加密
- **组件自动注入** —— 零配置与Spring等框架集成
- **更多功能正在集成中...** —— 如有您有好想法或者建议,欢迎加群交流
@@ -106,7 +89,8 @@ StpUtil.checkLogin();
@SaCheckPermission("user:add")
@RequestMapping("/user/insert")
public String insert(SysUser user) {
return "用户增加";
// ...
return "用户增加";
}
```
@@ -118,19 +102,19 @@ StpUtil.logoutByLoginId(10001);
除了以上的示例sa-token还可以一行代码完成以下功能
``` java
StpUtil.setLoginId(10001); // 标记当前会话登录的账号id
StpUtil.getLoginId(); // 获取当前会话登录的账号id
StpUtil.isLogin(); // 获取当前会话是否已经登录, 返回true或false
StpUtil.logout(); // 当前会话注销登录
StpUtil.logoutByLoginId(10001); // 让账号为10001的会话注销登录踢人下线
StpUtil.hasRole("super-admin"); // 查询当前账号是否含有指定角色标识, 返回true或false
StpUtil.hasPermission("user:add"); // 查询当前账号是否含有指定权限, 返回true或false
StpUtil.getSession(); // 获取当前账号id的Session
StpUtil.getSessionByLoginId(10001); // 获取账号id为10001的Session
StpUtil.getTokenValueByLoginId(10001); // 获取账号id为10001的token令牌值
StpUtil.setLoginId(10001, "PC"); // 指定设备标识登录
StpUtil.logoutByLoginId(10001, "PC"); // 指定设备标识进行强制注销 (不同端不受影响)
StpUtil.switchTo(10044); // 将当前会话身份临时切换为其它账号
StpUtil.setLoginId(10001); // 标记当前会话登录的账号id
StpUtil.getLoginId(); // 获取当前会话登录的账号id
StpUtil.isLogin(); // 获取当前会话是否已经登录, 返回true或false
StpUtil.logout(); // 当前会话注销登录
StpUtil.logoutByLoginId(10001); // 让账号为10001的会话注销登录踢人下线
StpUtil.hasRole("super-admin"); // 查询当前账号是否含有指定角色标识, 返回true或false
StpUtil.hasPermission("user:add"); // 查询当前账号是否含有指定权限, 返回true或false
StpUtil.getSession(); // 获取当前账号id的Session
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众多请恕此处无法为您逐一展示更多示例请戳官方在线文档
@@ -200,14 +184,17 @@ sa-token秉承着开放的思想欢迎大家贡献代码为框架添砖加
## 友情链接
[**[ okhttps ]** 一个轻量级http通信框架API设计无比优雅支持 WebSocket 以及 Stomp 协议](https://gitee.com/ejlchina-zhxu/okhttps)
[**[ 小诺快速开发平台 ]** 基于SpringBoot2 + AntDesignVue全新快速开发平台同时拥有三个版本](https://xiaonuo.vip/index#pricing)
## 交流群
QQ交流群[1002350610 点击加入](https://jq.qq.com/?_wv=1027&k=45H977HM)
![扫码加群](https://color-test.oss-cn-qingdao.aliyuncs.com/sa-token/qq-group.png ':size=150')
![扫码加群](https://color-test.oss-cn-qingdao.aliyuncs.com/sa-token/qq-group.png ':size=230')
**微信群**
**微信群**
![微信群](https://images.gitee.com/uploads/images/2021/0216/170729_25fcde8b_8073399.jpeg "sa-token-wx.jpg")
![微信群](https://dev33-test.oss-cn-beijing.aliyuncs.com/sa-token/i-wx-qr.png ':size=230')
(扫码添加微信备注sa-token邀您加入群聊)
<br>

View File

@@ -19,6 +19,8 @@
- [花式token](/use/token-style)
- [框架配置](/use/config)
- [会话治理](/use/search-session)
- [记住我模式](/use/remember-me)
- [密码加密](/use/password-secure)
- **进阶**
- [集群、分布式](/senior/dcs)
@@ -33,7 +35,9 @@
- **附录**
- [未登录场景值](/fun/not-login-scene)
- [token有效期详解](/fun/token-timeout)
- [Session模型详解](/fun/session-model)
- [TokenInfo参数详解](/fun/token-info)
- [框架源码所有技术栈](/fun/tech-stack)

View File

@@ -0,0 +1,21 @@
# Session模型详解
---
`sa-token`中, `Session` 分为三种, 分别是:
- `User-Session`: 指的是框架为每个`loginId`分配的`Session`
- `Token-Session`: 指的是框架为每个`token`分配的`Session`
- `自定义Session`: 指的是以一个`特定的值`作为SessionId来分配的`Session`
User-Session和Token-Session到底有什么不同下面这张图可以解释两者的区别
![../static/session-model.png](../static/session-model.png)
简而言之:
- `Token-Session` 以token为主只要token不同那么对应的Session对象就不同
- `User-Session` 以UserId为主只要token指向的UserId一致那么对应的Session对象就一致
- `自定义Session` 以特定的key为主不同key对应不同的Session对象

View File

@@ -0,0 +1,15 @@
# sa-token 源码用到的所有技术栈
包括但不限于以下:
- Maven多模块项目
- Servlet API、临时Cookie与永久Cookie、Request参数获取
- SpringBoot2.0、Redis、Jackson、Hutool、jwt
- SpringBoot自定义starter、Spring包扫码 + 依赖注入、AOP注解切面、yml配置映射、拦截器
- Java8 接口与default实现、静态方法、枚举、定时器、异常类、泛型、反射、IO流、自定义注解、Lambda表达式、函数式编程
- package-info注释、Serializable序列化接口、
- java加密算法MD5、SHA1、SHA256、AES、RSA
- OAuth2.0、同域单点登录、集群与分布式、路由Ant匹配

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<title>sa-token</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="sa-token是一个java权限认证框架功能全面上手简单登录验证、权限验证、Session会话、踢人下线、集成Redis、分布式会话、单点登录、前后台分离、模拟他人账号、临时身份切换、多账号体系、注解式鉴权、路由拦截式鉴权、花式token、自动续签、同端互斥登录、会话治理、Spring集成...零配置开箱即用,覆盖所有应用场景,你所需要的功能,这里都有">
<meta name="description" content="sa-token是一个java权限认证框架功能全面上手简单登录验证、权限验证、Session会话、踢人下线、集成Redis、分布式会话、单点登录、前后台分离、记住我模式、模拟他人账号、临时身份切换、多账号体系、注解式鉴权、路由拦截式鉴权、花式token、自动续签、同端互斥登录、会话治理、密码加密、jwt集成、Spring集成...有了sa-token你所有的权限认证问题都不再是问题">
<meta name="keywords" content="sa-token,sa-token框架,sa-token文档,java权限认证">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link rel="shortcut icon" type="image/x-icon" href="logo.png">
@@ -21,6 +21,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.13.0/doc/index.html">v1.13.0</option>
<option value="http://sa-token.dev33.cn/v/v1.12.1/doc/index.html">v1.12.1</option>
<option value="http://sa-token.dev33.cn/v/v1.12.0/doc/index.html">v1.12.0</option>
<option value="http://sa-token.dev33.cn/v/v1.11.0/doc/index.html">v1.11.0</option>
@@ -42,7 +43,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.13.0</sub>'
name += '<b style="font-size: 24px; vertical-align: middle;">sa-token</b> <sub>v1.14.0</sub>'
window.$docsify = {
name: name, // 名字
repo: 'https://github.com/click33/sa-token', // github地址

View File

@@ -49,12 +49,6 @@
可能是404了SpringBoot环境下如果访问接口404后会被重定向到`/error`,然后被再次拦截,如果是其它原因,欢迎加群反馈
### 如何把权限精确搭到按钮级?
在登录时,把当前用户拥有的权限码一次性返回给前端,让前端进行判断,<br>
例如使用vue等框架可以这样写`<button v-if="arr.indexOf('user:delete') > -1">删除按钮</button>` <br>
其中`arr`是当前用户拥有的权限码数组,`user:delete`是显示按钮需要拥有的权限码,`删除按钮`是用户拥有权限码才可以看到的内容
### 权限可以做成动态的吗?
权限本来就是动态的只有jwt那种模式才是非动态的
@@ -76,9 +70,18 @@
步骤:先在配置文件里将`tokenSessionCheckLogin`配置为`false`,然后通过`StpUtil.getTokenSession()`获取Session
### 我只使用header来传输token还需要打开Cookie模式吗
不需要如果只使用header来传输token可以在配置文件关闭Cookie模式`isReadCookie=false`
### 我想让用户修改密码后立即掉线重新登录,应该怎么做?
框架内置 [强制指定账号下线] 的APi在执行修改密码逻辑之后调用此API即可: `StpUtil.logout()`
### 还是有不明白到的地方?
请在`github`提交`issues`或者加入qq群交流群链接在[首页](README?id=交流群)
### 我能为这个框架贡献代码吗?
**可以**请参照首页的提交pr步骤 [贡献代码](README?id=贡献代码)
**可以**请参照首页的提交pr步骤 [贡献代码](README?id=贡献代码)

View File

@@ -17,6 +17,9 @@
[[sa-admin] - 一个多窗口后台模板,流畅、易上手、提高生产力](https://gitee.com/ejlchina-zhxu/okhttps)
[[vue-next-admin] - 一套为开发者快速开发准备的基于 vue2.x 越看越精彩的后台管理系统一站式平台模板](https://gitee.com/lyt-top/vue-admin-wonderful)
[[小诺快速开发平台] - 基于SpringBoot2 + AntDesignVue全新快速开发平台同时拥有三个版本](https://xiaonuo.vip/index#pricing)
<br>
虚位以待...

View File

@@ -1,5 +1,22 @@
# 更新日志
### 2021-3-12 @v1.14.0
- 新增:新增`SaLoginModel`登录参数Model适配 [记住我] 模式 **[重要]**
- 新增:新增 `StpUtil.setLoginId()` 时指定token有效期可灵活控制用户的一次登录免验证时长
- 新增新增Cookie时间判断`timeout`设置为-1时`Cookie`有效期将为`Integer.MAX_VALUE` **[重要]**
- 新增新增密码加密工具类可快速MD5、SHA1、SHA256、AES、RSA加密 **[重要]**
- 新增:新增 OAuth2.0 模块 **[重要]**
- 新增:`SaTokenConfig`配置类所有set方法支持链式调用
- 新增:`SaOAuth2Config` sa-token oauth2 配置类所有set方法新增支持链式调用
- 优化:`StpLogic`类所有`getKey`方法重名为`splicingKey`,更语义化的函数名称
- 新增:`IsRunFunction`新增`noExe`函数,用于指定当`isRun`值为`false`时执行的函数
- 新增:`SaSession`新增数据存取值操作API
- 优化:优化`SaTokenDao`接口增加Object操作API
- 优化jwt示例`createToken`方法去除默认秘钥判断,只在启动项目时打印警告
- 文档:常见问题新增示例(修改密码后如何立即掉线)
- 文档:权限认证文档新增[如何把权限精确搭到按钮级]示例说明
- 文档:优化文档,部分模块添加图片说明
### 2021-2-9 @v1.13.0
- 优化:优化源码注释与文档

View File

@@ -9,14 +9,14 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.13.0</version>
<version>1.14.0</version>
</dependency>
```
## Gradle依赖
Gradle用户引入依赖
```
implementation 'cn.dev33:sa-token-spring-boot-starter:1.13.0'
implementation 'cn.dev33:sa-token-spring-boot-starter:1.14.0'
```
@@ -34,8 +34,12 @@ implementation 'cn.dev33:sa-token-spring-boot-starter:1.13.0'
├── sa-token-dao-redis // sa-token整合redis (使用jdk默认序列化方式)
├── sa-token-dao-redis-jackson // sa-token整合redis (使用jackson序列化方式)
├── sa-token-spring-aop // sa-token整合SpringAOP 注解鉴权
├── sa-token-oauth2 // sa-token实现 OAuth2.0 模块(内测暂未发布)
├── sa-token-demo-springboot // sa-token示例
├── sa-token-demo-jwt // sa-token集成jwt示例
├── sa-token-demo-oauth2 // sa-token集成OAuth2.0模块示例
├── sa-token-demo-oauth2-client // OAuth2.0 客户端
├── sa-token-demo-oauth2-server // OAuth2.0 服务端
├── sa-token-doc // sa-token开发文档
├──pom.xml
```
@@ -46,7 +50,7 @@ implementation 'cn.dev33:sa-token-spring-boot-starter:1.13.0'
## jar包下载
[点击下载sa-token-1.6.0.jar](https://oss.dev33.cn/sa-token/sa-token-1.6.0.jar)
(注意:当前仅提供`v1.6.0`版本jar包下载更多版本请前往maven中央仓库获取)
(注意:当前仅提供`v1.6.0`版本jar包下载更多版本请前往maven中央仓库获取[直达链接](https://search.maven.org/search?q=sa-token))

View File

@@ -19,7 +19,7 @@
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.13.0</version>
<version>1.14.0</version>
</dependency>
```

BIN
sa-token-doc/doc/static/kickout.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
sa-token-doc/doc/static/login-view.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
sa-token-doc/doc/static/not-jur.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
sa-token-doc/doc/static/not-login.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -21,7 +21,7 @@ sa-token内置两种模式完成注解鉴权分别是`AOP模式`和`拦截器
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-aop</artifactId>
<version>1.13.0</version>
<version>1.14.0</version>
</dependency>
```

View File

@@ -9,13 +9,12 @@ Sa-token默认将会话数据保存在内存中此模式读写速度最快
### 1. sa-token 整合 Redis (使用jdk默认序列化方式)
(注意: 整合Redis只需要引入对应的pom依赖即可所有上层API保持不变)
``` xml
<!-- sa-token整合redis (使用jdk默认序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis</artifactId>
<version>1.13.0</version>
<version>1.14.0</version>
</dependency>
```
优点兼容性好缺点Session序列化后基本不可读对开发者来讲等同于乱码
@@ -27,7 +26,7 @@ Sa-token默认将会话数据保存在内存中此模式读写速度最快
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.13.0</version>
<version>1.14.0</version>
</dependency>
```
优点Session序列化后可读性强可灵活手动修改缺点兼容性稍差
@@ -35,7 +34,10 @@ Sa-token默认将会话数据保存在内存中此模式读写速度最快
<br>
**请注意无论使用哪种序列化方式你都必须为项目提供一个Redis实例化方案例如**
### 集成Redis请注意
**1. 无论使用哪种序列化方式你都必须为项目提供一个Redis实例化方案例如**
``` xml
<!-- 提供redis连接池 -->
<dependency>
@@ -44,6 +46,38 @@ Sa-token默认将会话数据保存在内存中此模式读写速度最快
</dependency>
```
**2. 引入了依赖我还需要为Redis配置连接信息吗** <br>
需要只有项目初始化了正确的Redis实例`sa-token`才可以使用Redis进行数据持久化参考以下`yml配置`
``` java
# 端口
spring:
# redis配置
redis:
# Redis数据库索引默认为0
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码默认为空
# password:
# 连接超时时间(毫秒)
timeout: 1000ms
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
```
**3. 集成Redis后是我额外手动保存数据还是框架自动保存** <br>
框架自动保存。集成`Redis`只需要引入对应的`pom依赖`即可框架所有上层API保持不变
<br><br>

View File

@@ -8,7 +8,9 @@
有,就让你通过。没有?那么禁止访问!
再往底了说,就是每个账号都会拥有一个权限码集合,我来验证这个集合中是否包含指定的权限码 <br/>
例如:当前账号拥有权限码集合:`["user:add", "user:delete", "user:get"]`,这时候我来验证权 `"user:update"`,则结果就是:**验证失败,禁止访问**
例如:当前账号拥有权限码集合:`["user:add", "user:delete", "user:get"]`,这时候我来验证权 `"user:update"`,则结果就是:**验证失败,禁止访问**
![无权限](../static/not-jur.png)
所以现在问题的核心就是:
1. 如何获取一个账号所拥有的的权限码集合
@@ -112,4 +114,24 @@ StpUtil.checkRoleOr("user:update", "user:delete");
### 拦截全局异常
有同学要问,鉴权失败,抛出异常,然后呢?要把异常显示给用户看吗?**当然不可以!** <br>
你可以创建一个全局异常拦截器,统一返回给前端的格式,参考:[码云GlobalException.java](https://gitee.com/sz6/sa-token/blob/master/sa-token-demo-springboot/src/main/java/com/pj/test/GlobalException.java)
你可以创建一个全局异常拦截器,统一返回给前端的格式,参考:[码云GlobalException.java](https://gitee.com/sz6/sa-token/blob/master/sa-token-demo-springboot/src/main/java/com/pj/test/GlobalException.java)
### 如何把权限精确搭到按钮级?
权限精确到按钮级的意思就是指:**权限范围可以控制到页面上的每一个按钮是否显示**
思路:如此精确的范围控制只依赖后端已经难以完成,此时需要前端进行一定的逻辑判断
1. 在登录时,把当前账号拥有的所有权限码一次性返回给前端
2. 前端将权限码集合保存在`localStorage`或其它全局状态管理对象中
3. 在需要权限控制的按钮上使用js进行逻辑判断例如在`vue`框架中我们可以使用如下写法:
``` js
<button v-if="arr.indexOf('user:delete') > -1">删除按钮</button>
```
其中:`arr`是当前用户拥有的权限码数组,`user:delete`是显示按钮需要拥有的权限码,`删除按钮`是用户拥有权限码才可以看到的内容
注意:以上写法只为提供一个参考示例,不同框架有不同写法,开发者可根据项目技术栈灵活封装进行调用
### 前端有了鉴权后端还需要鉴权吗?
**需要!前端的鉴权只是一个辅助功能,对于专业人员这些限制都是可以轻松绕过的,为保证服务器安全,无论前端是否进行了权限校验,后端接口都需要对会话请求再次进行权限校验!**

View File

@@ -1,6 +1,8 @@
# 踢人下线
所谓踢人下线,核心操作就是找到其指定`loginId`对应的`token`,并设置其失效
![踢下线](../static/kickout.png)
---

View File

@@ -7,6 +7,7 @@
所谓登录验证,说白了就是限制某些接口只有登录后才能访问(如:查询我的账号资料) <br>
那么判断一个会话是否登录的依据是什么?当然是登录成功后框架给你做个标记!然后在需要鉴权的接口里检查此标记,有标记者视为已登录,无标记者视为未登录!
![暂未登录](../static/not-login.png)
### 登录与注销
@@ -66,6 +67,6 @@ StpUtil.getTokenValue();
StpUtil.getTokenInfo();
```
有关TokenInfo参数详解请参考[参考TokenInfo参数详解](/fun/token-info)
?> 有关TokenInfo参数详解请参考[参考TokenInfo参数详解](/fun/token-info)

View File

@@ -43,7 +43,7 @@
``` java
// 底层的 StpLogic 对象
public static StpLogic stpLogic = new StpLogic("login") {
public static StpLogic stpLogic = new StpLogic("user") {
// 重写 `getTokenName` 函数,返回一个与 `StpUtil` 不同的token名称, 防止冲突
@Override
public String getTokenName() {

View File

@@ -0,0 +1,83 @@
# 密码加密
严格来讲,密码加密不属于 [权限认证] 的范畴,但是对于大多数系统来讲,密码加密又是安全认证不可或缺的部分,
所以,应大家要求,`sa-token``v1.14版本`添加密码加密模块,该模块非常简单,仅仅封装了一些常见的加密算法
### 摘要加密
md5、sha1、sha256
``` java
// md5加密
SaSecureUtil.md5("123456");
// sha1加密
SaSecureUtil.sha1("123456");
// sha256加密
SaSecureUtil.sha256("123456");
// md5加盐加密: md5(md5(str) + md5(salt))
SaSecureUtil.md5BySalt("123456", "salt");
```
### 对称加密
AES加密
``` java
// 定义秘钥和明文
String key = "123456";
String text = "sa-token 一个轻量级java权限认证框架";
// 加密
String ciphertext = SaSecureUtil.aesEncrypt(key, text);
System.out.println("AES加密后" + ciphertext);
// 解密
String text2 = SaSecureUtil.aesDecrypt(key, ciphertext);
System.out.println("AES解密后" + text2);
```
### 非对称加密
RSA加密
``` java
// 定义私钥和公钥
String privateKey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAO+wmt01pwm9lHMdq7A8gkEigk0XKMfjv+4IjAFhWCSiTeP7dtlnceFJbkWxvbc7Qo3fCOpwmfcskwUc3VSgyiJkNJDs9ivPbvlt8IU2bZ+PBDxYxSCJFrgouVOpAr8ar/b6gNuYTi1vt3FkGtSjACFb002/68RKUTye8/tdcVilAgMBAAECgYA1COmrSqTUJeuD8Su9ChZ0HROhxR8T45PjMmbwIz7ilDsR1+E7R4VOKPZKW4Kz2VvnklMhtJqMs4MwXWunvxAaUFzQTTg2Fu/WU8Y9ha14OaWZABfChMZlpkmpJW9arKmI22ZuxCEsFGxghTiJQ3tK8npj5IZq5vk+6mFHQ6aJAQJBAPghz91Dpuj+0bOUfOUmzi22obWCBncAD/0CqCLnJlpfOoa9bOcXSusGuSPuKy5KiGyblHMgKI6bq7gcM2DWrGUCQQD3SkOcmia2s/6i7DUEzMKaB0bkkX4Ela/xrfV+A3GzTPv9bIBamu0VIHznuiZbeNeyw7sVo4/GTItq/zn2QJdBAkEA8xHsVoyXTVeShaDIWJKTFyT5dJ1TR++/udKIcuiNIap34tZdgGPI+EM1yoTduBM7YWlnGwA9urW0mj7F9e9WIQJAFjxqSfmeg40512KP/ed/lCQVXtYqU7U2BfBTg8pBfhLtEcOg4wTNTroGITwe2NjL5HovJ2n2sqkNXEio6Ji0QQJAFLW1Kt80qypMqot+mHhS+0KfdOpaKeMWMSR4Ij5VfE63WzETEeWAMQESxzhavN1WOTb3/p6icgcVbgPQBaWhGg==";
String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvsJrdNacJvZRzHauwPIJBIoJNFyjH47/uCIwBYVgkok3j+3bZZ3HhSW5Fsb23O0KN3wjqcJn3LJMFHN1UoMoiZDSQ7PYrz275bfCFNm2fjwQ8WMUgiRa4KLlTqQK/Gq/2+oDbmE4tb7dxZBrUowAhW9NNv+vESlE8nvP7XXFYpQIDAQAB";
// 文本
String text = "sa-token 一个轻量级java权限认证框架";
// 使用公钥加密
String ciphertext = SaSecureUtil.rsaEncryptByPublic(publicKey, text);
System.out.println("公钥加密后:" + ciphertext);
// 使用私钥解密
String text2 = SaSecureUtil.rsaDecryptByPrivate(privateKey, ciphertext);
System.out.println("私钥解密后:" + text2);
```
你可能会有疑问,私钥和公钥这么长的一大串,我怎么弄出来,手写吗?当然不是,调用以下方法生成即可
``` java
// 生成一对公钥和私钥其中Map对象 (private=私钥, public=公钥)
System.out.println(SaSecureUtil.rsaGenerateKeyPair());
```
### Base64编码与解码
``` java
// 文本
String text = "sa-token 一个轻量级java权限认证框架";
// 使用Base64编码
String base64Text = SaBase64Util.encode(text);
System.out.println("Base64编码后" + base64Text);
// 使用Base64解码
String text2 = SaBase64Util.decode(base64Text);
System.out.println("Base64解码后" + text2);
```
<br>
如需更多加密算法请提交pr

View File

@@ -0,0 +1,81 @@
# [记住我]模式
---
如下图所示,一般网站的登录界面都会有一个 [ 记住我 ] 按钮,当你勾选它后,即时你关闭浏览器再次打开网站,也依然会处于登录状态,无须重复验证密码
![../static/login-view.png](../static/login-view.png)
那么在sa-token中如何做到 [ 记住我 ] 功能呢?
### 在sa-token中实现记住我功能
sa-token的登录授权**默认就是`[记住我]`模式**,为了实现`[非记住我]`模式, 你需要在登录时如下设置:
``` java
// 设置登录账号id为10001第二个参数指定是否为[记住我]当此值为false后关闭浏览器后再次打开需要重新登录
StpUtil.setLoginId(10001, false);
```
那么sa-token实现`[记住我]`的具体原理是?
### 实现原理
Cookie作为浏览器提供的默认会话跟踪机制其生命周期有两种形式分别是
- 临时Cookie有效期为本次会话只要关闭浏览器窗口Cookie就会消失
- 永久Cookie有效期为一个具体的时间在时间未到期之前即使用户关闭了浏览器Cookie也不会消失
利用Cookie的此特性我们便可以轻松实现 [记住我] 模式:
- 勾选[记住我]按钮时:调用`StpUtil.setLoginId(10001, true)`,在浏览器写入一个`永久Cookie`储存token此时用户即使重启浏览器token依然有效
- 不勾选[记住我]按钮时:调用`StpUtil.setLoginId(10001, false)`,在浏览器写入一个`临时Cookie`储存token此时用户在重启浏览器后token便会消失导致会话失效
### 前后台分离模式下如何实现[记住我]?
此时机智的你😏很快发现一个问题Cookie虽好却无法在前后端分离环境下使用那是不是代表上述方案在APP、小程序等环境中无效
准确的讲答案是肯定的任何基于Cookie的认证方案在前后台分离环境下都会失效原因在于这些客户端默认没有实现Cookie功能不过好在这些客户端一般都提供了替代方案
唯一遗憾的是此场景中token的生命周期需要我们在前端手动控制
以经典跨端框架 [uni-app](https://uniapp.dcloud.io/) 为例,我们可以使用如下方式达到同样的效果:
``` js
// 使用本地存储保存token达到 [永久Cookie] 的效果
uni.setStorageSync("satoken", "xxxx-xxxx-xxxx-xxxx-xxx");
// 使用globalData保存token达到 [临时Cookie] 的效果
getApp().globalData.satoken = "xxxx-xxxx-xxxx-xxxx-xxx";
```
如果你决定在PC浏览器环境下进行前后台分离模式开发那么更加简单
``` js
// 使用 localStorage 保存token达到 [永久Cookie] 的效果
localStorage.setItem("satoken", "xxxx-xxxx-xxxx-xxxx-xxx");
// 使用 sessionStorage 保存token达到 [临时Cookie] 的效果
sessionStorage.setItem("satoken", "xxxx-xxxx-xxxx-xxxx-xxx");
```
Remember me, it's too easy!
### 登录时指定token有效期
登录时不仅可以指定是否为`[记住我]`模式还可以指定一个特定的时间作为token有效时长如下示例
``` java
// 示例1
// 指定token有效期(单位: 秒)如下所示token七天有效
StpUtil.setLoginId(10001, new SaLoginModel().setTimeout(60 * 60 * 24 * 7));
// ----------------------- 示例2所有参数
// `SaLoginModel`为登录参数Model其有诸多参数决定登录时的各种逻辑例如
StpUtil.setLoginId(10001, new SaLoginModel()
.setDevice("PC") // 此次登录的客户端设备标识, 用于[同端互斥登录]时指定此次登录的设备名称
.setIsLastingCookie(true) // 是否为持久Cookie临时Cookie在浏览器关闭时会自动删除持久Cookie在重新打开后依然存在
.setTimeout(60 * 60 * 24 * 7) // 指定此次登录token的有效期, 单位:秒 如未指定自动取全局配置的timeout值
);
```

View File

@@ -10,7 +10,7 @@
## 1、注册路由拦截器
`springboot2.0`为例, 新建配置类`MySaTokenConfig.java`
`SpringBoot2.0`为例, 新建配置类`MySaTokenConfig.java`
``` java
@Configuration
public class MySaTokenConfig implements WebMvcConfigurer {
@@ -22,49 +22,19 @@ public class MySaTokenConfig implements WebMvcConfigurer {
}
}
```
以上代码,我们注册了一个登录验证拦截器,并且排除了`/user/doLogin`接口用来开放登录 <br>
以上代码,我们注册了一个登录验证拦截器,并且排除了`/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((request, response, handler)->{
System.out.println("---------- 进入自定义认证 --------------- ");
// 你可以在这里写任意认证代码, 例如: StpUtil.checkLogin();
})).addPathPatterns("/**");
}
}
```
(你不必像上面的示例一样注册所有拦截器,只要按需注册即可
## 3、让我们利用自定义拦截器做点快活的事情
你可以根据路由划分模块,不同模块不同鉴权
## 2、自定义权限验证规则
你可以使用函数式编程自定义验证规则
``` java
@Configuration
public class MySaTokenConfig implements WebMvcConfigurer {
// 注册sa-token的所有拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册路由拦截器,自定义验证规则
registry.addInterceptor(new SaRouteInterceptor((request, response, handler)->{
// 根据路由划分模块,不同模块不同鉴权
SaRouterUtil.match("/user/**", () -> StpUtil.checkPermission("user"));
@@ -78,9 +48,8 @@ public class MySaTokenConfig implements WebMvcConfigurer {
}
```
## 4、完整的示例
最终的代码,可能会类似于下面的样子:
## 3、完整示例
所有用法示例
``` java
@Configuration
@@ -88,12 +57,15 @@ public class MySaTokenConfig implements WebMvcConfigurer {
// 注册sa-token的拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 自定义验证拦截器
// 注册路由拦截器,自定义验证规则
registry.addInterceptor(new SaRouteInterceptor((request, response, handler) -> {
// 登录验证 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
SaRouterUtil.match("/**", "/user/doLogin", () -> StpUtil.checkLogin());
// 登录验证 -- 排除多个路径
SaRouterUtil.match(Arrays.asList("/**"), Arrays.asList("/user/doLogin", "/user/reg"), () -> StpUtil.checkLogin());
// 角色认证 -- 拦截以 admin 开头的路由,必须具备[admin]角色或者[super-admin]角色才可以通过认证
SaRouterUtil.match("/admin/**", () -> StpUtil.checkRoleOr("admin", "super-admin"));
@@ -108,6 +80,16 @@ public class MySaTokenConfig implements WebMvcConfigurer {
// 匹配 restful 风格路由
SaRouterUtil.match("/article/get/{id}", () -> StpUtil.checkPermission("article"));
// 检查请求方式
SaRouterUtil.match("/notice/**", () -> {
if(request.getMethod().equals(HttpMethod.GET.toString())) {
StpUtil.checkPermission("notice");
}
});
// 在多账号模式下可以使用任意StpUtil进行校验
SaRouterUtil.match("/user/**", () -> StpUserUtil.checkLogin());
})).addPathPatterns("/**");
}
}
@@ -120,3 +102,5 @@ public class MySaTokenConfig implements WebMvcConfigurer {

View File

@@ -9,6 +9,8 @@ Session是会话中专业的数据缓存组件通过`Session`我们可以很
- `Token-Session`: 指的是框架为每个`token`分配的`Session`
- `自定义Session`: 指的是以一个`特定的值`作为SessionId来分配的`Session`
> 有关User-Session与Token-Session的详细区别请参考[Session模型详解](/fun/session-model)
### User-Session
有关账号Session的API如下

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<title>sa-token</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="sa-token是一个java权限认证框架功能全面上手简单登录验证、权限验证、Session会话、踢人下线、集成Redis、分布式会话、单点登录、前后台分离、模拟他人账号、临时身份切换、多账号体系、注解式鉴权、路由拦截式鉴权、花式token、自动续签、同端互斥登录、会话治理、Spring集成...零配置开箱即用,覆盖所有应用场景,你所需要的功能,这里都有">
<meta name="description" content="sa-token是一个java权限认证框架功能全面上手简单登录验证、权限验证、Session会话、踢人下线、集成Redis、分布式会话、单点登录、前后台分离、记住我模式、模拟他人账号、临时身份切换、多账号体系、注解式鉴权、路由拦截式鉴权、花式token、自动续签、同端互斥登录、会话治理、密码加密、jwt集成、Spring集成...有了sa-token你所有的权限认证问题都不再是问题">
<meta name="keywords" content="sa-token,sa-token框架,sa-token文档,java权限认证">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link rel="shortcut icon" type="image/x-icon" href="doc/logo.png">
@@ -41,10 +41,10 @@
<!-- 内容部分 -->
<div class="main-box">
<div class="content-box">
<h1>sa-token<small>v1.13.0</small></h1>
<h1>sa-token<small>v1.14.0</small></h1>
<div class="sub-title">这可能是史上功能最全的java权限认证框架</div>
<!-- <p>0配置开箱即用低学习成本</p> -->
<p>登录验证、权限验证、Session会话、踢人下线、集成Redis、分布式会话、单点登录、前后台分离、模拟他人账号、临时身份切换、多账号体系、注解式鉴权、路由拦截式鉴权、花式token、自动续签、同端互斥登录、会话治理、Spring集成...</p>
<p>登录验证、权限验证、Session会话、踢人下线、集成Redis、分布式会话、单点登录、前后台分离、记住我模式、模拟他人账号、临时身份切换、多账号体系、注解式鉴权、路由拦截式鉴权、花式token、自动续签、同端互斥登录、会话治理、密码加密、jwt集成、Spring集成...</p>
<!-- <p>零配置开箱即用,覆盖所有应用场景,你所需要的功能,这里都有</p> -->
<p>有了sa-token你所有的权限认证问题都不再是问题</p>
<div class="btn-box">

View File

@@ -9,3 +9,4 @@ sa-token-oauth2 模块是 sa-token 实现 oauth2.0 的部分,目前该模块
3. 根据控制台打印,访问测试地址即可:[http://localhost:8002/login.html](http://localhost:8002/login.html)
可结合代码注释学习查看

View File

@@ -7,13 +7,13 @@
<parent>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-parent</artifactId>
<version>1.13.0</version>
<version>1.14.0</version>
</parent>
<packaging>jar</packaging>
<name>sa-token-dao-redis</name>
<artifactId>sa-token-oauth2</artifactId>
<version>1.13.0-alpha</version>
<version>1.14.0-alpha</version>
<description>sa-token realization oauth2.0</description>
<dependencies>

View File

@@ -32,9 +32,11 @@ public class SaOAuth2Config {
/**
* @param codeTimeout 要设置的 codeTimeout
* @return 对象自身
*/
public void setCodeTimeout(long codeTimeout) {
public SaOAuth2Config setCodeTimeout(long codeTimeout) {
this.codeTimeout = codeTimeout;
return this;
}
/**
@@ -46,9 +48,11 @@ public class SaOAuth2Config {
/**
* @param accessTokenTimeout 要设置的 accessTokenTimeout
* @return 对象自身
*/
public void setAccessTokenTimeout(long accessTokenTimeout) {
public SaOAuth2Config setAccessTokenTimeout(long accessTokenTimeout) {
this.accessTokenTimeout = accessTokenTimeout;
return this;
}
/**
@@ -60,9 +64,11 @@ public class SaOAuth2Config {
/**
* @param refreshTokenTimeout 要设置的 refreshTokenTimeout
* @return 对象自身
*/
public void setRefreshTokenTimeout(long refreshTokenTimeout) {
public SaOAuth2Config setRefreshTokenTimeout(long refreshTokenTimeout) {
this.refreshTokenTimeout = refreshTokenTimeout;
return this;
}

View File

@@ -523,7 +523,7 @@ public interface SaOAuth2Interface {
}
/**
* 获取key[Client&账号]最新授权码记录, 持久化使用的key
* 获取key[Client and 账号]最新授权码记录, 持久化使用的key
* @param loginId 账号id
* @param clientId 应用id
* @return key

View File

@@ -51,6 +51,7 @@ public class SaOAuth2Util {
* @param clientId 应用id
* @param scope 权限
* @param loginId 账号id
* @return 是否已经授权
*/
public static boolean isGrant(Object loginId, String clientId, String scope) {
return SaOAuth2Manager.getInterface().isGrant(loginId, clientId, scope);

View File

@@ -50,6 +50,7 @@ public class RequestAuthModel {
/**
* @param clientId 要设置的 clientId
* @return 对象自身
*/
public RequestAuthModel setClientId(String clientId) {
this.clientId = clientId;
@@ -65,6 +66,7 @@ public class RequestAuthModel {
/**
* @param scope 要设置的 scope
* @return 对象自身
*/
public RequestAuthModel setScope(String scope) {
this.scope = scope;
@@ -80,6 +82,7 @@ public class RequestAuthModel {
/**
* @param loginId 要设置的 loginId
* @return 对象自身
*/
public RequestAuthModel setLoginId(Object loginId) {
this.loginId = loginId;
@@ -95,6 +98,7 @@ public class RequestAuthModel {
/**
* @param redirectUri 要设置的 redirectUri
* @return 对象自身
*/
public RequestAuthModel setRedirectUri(String redirectUri) {
this.redirectUri = redirectUri;
@@ -110,6 +114,7 @@ public class RequestAuthModel {
/**
* @param responseType 要设置的 responseType
* @return 对象自身
*/
public RequestAuthModel setResponseType(String responseType) {
this.responseType = responseType;
@@ -125,6 +130,7 @@ public class RequestAuthModel {
/**
* @param state 要设置的 state
* @return 对象自身
*/
public RequestAuthModel setState(String state) {
this.state = state;
@@ -133,6 +139,7 @@ public class RequestAuthModel {
/**
* 检查此Model参数是否有效
* @return 对象自身
*/
public RequestAuthModel checkModel() {
if(SaTokenInsideUtil.isEmpty(clientId)) {

View File

@@ -26,7 +26,7 @@ public class ScopeModel {
}
/**
* 构造一个
* @param id 权限id
* @param name 权限名称
* @param introduce 权限详细介绍
*/
public ScopeModel(String name, String introduce) {

View File

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

View File

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

View File

@@ -34,7 +34,7 @@ public class SpringMVCUtil {
public static HttpServletResponse getResponse() {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if(servletRequestAttributes == null) {
throw new SaTokenException("非Web上下文无法获取Request");
throw new SaTokenException("非Web上下文无法获取Response");
}
return servletRequestAttributes.getResponse();
}