Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 87efc4e3d7 | |||
| dacf86ebff | |||
| 1fe9a644c7 | |||
| 8cbb09d6b7 | |||
| 26302877d0 | |||
| e53802e9f7 | |||
| ef6c99452f | |||
| e8b1249c0b | |||
| ec7bf0dc1e | |||
| 85eafcd8b1 | |||
| 77a28f1db5 | |||
| e2003b542d | |||
| 58330904e0 | |||
| 57f9825a5b | |||
| 735627761a | |||
| a4ef404d73 | |||
| b7b13fe4ed | |||
| 079376107a | |||
| 5c0ca64a00 | |||
| a4be9928aa | |||
| 71242c15b5 | |||
| 4afc0f9b54 | |||
| 8db48f65dd | |||
| 34947f2705 | |||
| 00d4c3f672 | |||
| 3192717c0f | |||
| 6c4cdf514e | |||
| b59bb39aea | |||
| c91412e542 | |||
| 59253dc6f0 | |||
| 51a12fd9e5 | |||
| 8ff85904f8 | |||
| 24907ae6c2 | |||
| 248fd0c3b6 | |||
| f424908743 | |||
| 0644006efe | |||
| 290fea17e0 | |||
| 85a1c5ba6e | |||
| 1a89be8daa | |||
| d8bd196d24 | |||
| 80879ba3b8 | |||
| b5908c8ac0 | |||
| c62fdc9276 | |||
| 1c3b02fc13 | |||
| 6f1094c361 | |||
| 11492df031 | |||
| 9a53222baa | |||
| f221d4ce97 | |||
| 2bb45214d0 | |||
| a7895eff88 | |||
| 4caa6a8074 | |||
| e52045d3da | |||
| 20da538a3f | |||
| 21d5e02c67 | |||
| e7694bd6fb | |||
| 494030506d | |||
| 4e70438ba8 | |||
| d6b5975bdf | |||
| a8851cf54d | |||
| d1a0402c52 | |||
| bb37c972ff | |||
| 982b0dde21 | |||
| 1bfb26c191 | |||
| 3cfe6c866e | |||
| 7e2388b3e9 | |||
| 2843ef85bc | |||
| 82b9f23945 | |||
| 192b2fdd09 | |||
| 2ce1328cdc | |||
| 442cf9db41 | |||
| 601433a638 | |||
| d1203142b0 | |||
| ea8a966852 | |||
| 23d5df85e3 | |||
| ca1a8ec239 | |||
| df551cf0b5 | |||
| 150373ebae | |||
| f8807ef2bc | |||
| 0bcfeb231b | |||
| 56e39b4fba | |||
| 6b38a16102 | |||
| b395240136 | |||
| adfdacbdf2 | |||
| 8da8b50880 | |||
| 57556625cc | |||
| be8ec9c1be | |||
| d8e9e98152 | |||
| bbb30e7f96 | |||
| d3d11ce2b9 | |||
| a4b63f5875 | |||
| 824a4fec30 | |||
| d75e4b799d | |||
| 8b6dc460a5 | |||
| 1fa07884f3 | |||
| 203bf304cb | |||
| 121b857135 | |||
| b8fcf37bc4 | |||
| ffecda729a | |||
| ce6e5749f3 | |||
| 99e7f45a5c | |||
| 9c828ccddc | |||
| 8c2ff8be9a | |||
| e3d93c7103 | |||
| 7db36d5a97 | |||
| f9113ddce5 | |||
| 6954483255 | |||
| eb4bd150c3 | |||
| e52daf2cd6 |
@@ -1,15 +1,16 @@
|
||||
<p align="center">
|
||||
<img alt="logo" src="https://sa-token.cc/logo.png" width="150" height="150">
|
||||
</p>
|
||||
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">Sa-Token v1.39.0</h1>
|
||||
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">Sa-Token v1.40.0</h1>
|
||||
<h4 align="center">一个轻量级 Java 权限认证框架,让鉴权变得简单、优雅!</h4>
|
||||
<p align="center">
|
||||
<a href="https://gitee.com/dromara/sa-token/stargazers"><img src="https://gitee.com/dromara/sa-token/badge/star.svg?theme=gvp"></a>
|
||||
<a href="https://gitee.com/dromara/sa-token/members"><img src="https://gitee.com/dromara/sa-token/badge/fork.svg?theme=gvp"></a>
|
||||
<a href="https://gitcode.com/dromara/sa-token/overview"><img src="https://gitcode.com/dromara/Sa-Token/star/badge.svg"></a>
|
||||
<a href="https://github.com/dromara/sa-token/stargazers"><img src="https://img.shields.io/github/stars/dromara/sa-token?style=flat-square&logo=GitHub"></a>
|
||||
<a href="https://github.com/dromara/sa-token/network/members"><img src="https://img.shields.io/github/forks/dromara/sa-token?style=flat-square&logo=GitHub"></a>
|
||||
<a href="https://github.com/dromara/sa-token/watchers"><img src="https://img.shields.io/github/watchers/dromara/sa-token?style=flat-square&logo=GitHub"></a>
|
||||
<a href="https://github.com/dromara/sa-token/issues"><img src="https://img.shields.io/github/issues/dromara/sa-token.svg?style=flat-square&logo=GitHub"></a>
|
||||
<!-- <a href="https://github.com/dromara/sa-token/issues"><img src="https://img.shields.io/github/issues/dromara/sa-token.svg?style=flat-square&logo=GitHub"></a> -->
|
||||
<a href="https://github.com/dromara/sa-token/blob/master/LICENSE"><img src="https://img.shields.io/github/license/dromara/sa-token.svg?style=flat-square"></a>
|
||||
</p>
|
||||
<!-- <p align="center">学习测试请拉取 master 分支,dev 是在开发分支 (在根目录执行 `git checkout master`)</p> -->
|
||||
@@ -169,10 +170,11 @@ Sa-Token-OAuth2 模块分为四种授权模式,解决不同场景下的授权
|
||||
|
||||
- [[ Snowy ]](https://gitee.com/xiaonuobase/snowy):国内首个国密前后分离快速开发平台,采用 Vue3 + AntDesignVue3 + Vite + SpringBoot + Mp + HuTool + SaToken。
|
||||
- [[ RuoYi-Vue-Plus ]](https://gitee.com/dromara/RuoYi-Vue-Plus):重写RuoYi-Vue所有功能 集成 Sa-Token+Mybatis-Plus+Jackson+Xxl-Job+knife4j+Hutool+OSS 定期同步
|
||||
- [[ RuoYi-Cloud-Plus ]](https://gitee.com/dromara/RuoYi-Cloud-Plus):重写RuoYi-Cloud所有功能 整合 SpringCloudAlibaba Dubbo3.0 Sa-Token Mybatis-Plus MQ OSS ES Xxl-Job Docker 全方位升级 定期同步
|
||||
- [[Smart-Admin]](https://gitee.com/lab1024/smart-admin):SmartAdmin国内首个以「高质量代码」为核心,「简洁、高效、安全」中后台快速开发平台;
|
||||
- [[ 灯灯 ]](https://gitee.com/dromara/lamp-cloud): 专注于多租户解决方案的微服务中后台快速开发平台。租户模式支持独立数据库(DATASOURCE模式)、共享数据架构(COLUMN模式) 和 非租户模式(NONE模式)✨
|
||||
- [[ EasyAdmin ]](https://gitee.com/lakernote/easy-admin):一个基于SpringBoot2 + Sa-Token + Mybatis-Plus + Snakerflow + Layui 的后台管理系统,灵活多变可前后端分离,也可单体,内置代码生成器、权限管理、工作流引擎等
|
||||
- [[ YC-Framework ]](http://framework.youcongtech.com/):致力于打造一款优秀的分布式微服务解决方案。
|
||||
- [[ Pig-Satoken ]](https://gitee.com/wchenyang/cloud-satoken):重写 Pig 授权方式为 Sa-Token,其他代码不变。
|
||||
- [[ sa-admin-server ]](https://gitee.com/wlf213/sa-admin-server): 基于 sa-admin-ui 的后台管理开发脚手架。
|
||||
|
||||
|
||||
还有更多优秀开源案例无法逐一展示,请参考:[Awesome-Sa-Token](https://gitee.com/sa-token/awesome-sa-token)
|
||||
|
||||
@@ -192,7 +194,7 @@ Sa-Token-OAuth2 模块分为四种授权模式,解决不同场景下的授权
|
||||
### 代码托管
|
||||
- Gitee:[https://gitee.com/dromara/sa-token](https://gitee.com/dromara/sa-token)
|
||||
- GitHub:[https://github.com/dromara/sa-token](https://github.com/dromara/sa-token)
|
||||
- GitCode:[https://gitcode.com/click33/sa-token](https://gitcode.com/click33/sa-token)
|
||||
- GitCode:[https://gitcode.com/dromara/sa-token](https://gitcode.com/dromara/sa-token)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ cd sa-token-demo-springboot-redisson & call mvn clean & cd ..
|
||||
cd sa-token-demo-ssm & call mvn clean & cd ..
|
||||
cd sa-token-demo-test & call mvn clean & cd ..
|
||||
cd sa-token-demo-thymeleaf & call mvn clean & cd ..
|
||||
cd sa-token-demo-freemarker & call mvn clean & cd ..
|
||||
cd sa-token-demo-webflux & call mvn clean & cd ..
|
||||
cd sa-token-demo-webflux-springboot3 & call mvn clean & cd ..
|
||||
cd sa-token-demo-websocket & call mvn clean & cd ..
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
<!-- 一些属性 -->
|
||||
<properties>
|
||||
<revision>1.39.0</revision>
|
||||
<revision>1.40.0</revision>
|
||||
<jdk.version>1.8</jdk.version>
|
||||
<project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>utf-8</project.reporting.outputEncoding>
|
||||
|
||||
+11
-1
@@ -13,7 +13,7 @@
|
||||
<url>https://github.com/dromara/sa-token</url>
|
||||
|
||||
<properties>
|
||||
<revision>1.39.0</revision>
|
||||
<revision>1.40.0</revision>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
@@ -154,6 +154,11 @@
|
||||
<artifactId>sa-token-dialect-thymeleaf</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-freemarker</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-jwt</artifactId>
|
||||
@@ -174,6 +179,11 @@
|
||||
<artifactId>sa-token-spring-aop</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-el</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-sso</artifactId>
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package cn.dev33.satoken.config;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Sa-Token Cookie写入 相关配置
|
||||
*
|
||||
@@ -54,6 +57,12 @@ public class SaCookieConfig {
|
||||
*/
|
||||
private String sameSite;
|
||||
|
||||
/**
|
||||
* 额外扩展属性
|
||||
*/
|
||||
private Map<String, String> extraAttrs = new LinkedHashMap<>();
|
||||
|
||||
|
||||
/**
|
||||
* 获取:Cookie 作用域
|
||||
* <p> 写入 Cookie 时显式指定的作用域, 常用于单点登录二级域名共享 Cookie 的场景。 </p>
|
||||
@@ -140,11 +149,69 @@ public class SaCookieConfig {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 获取额外扩展属性
|
||||
*/
|
||||
public Map<String, String> getExtraAttrs() {
|
||||
return extraAttrs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入额外扩展属性
|
||||
* @param extraAttrs /
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaCookieConfig setExtraAttrs(Map<String, String> extraAttrs) {
|
||||
this.extraAttrs = extraAttrs;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加扩展属性
|
||||
* @param name /
|
||||
* @param value /
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaCookieConfig addExtraAttr(String name, String value) {
|
||||
if (extraAttrs == null) {
|
||||
extraAttrs = new LinkedHashMap<>();
|
||||
}
|
||||
this.extraAttrs.put(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加扩展属性
|
||||
* @param name /
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaCookieConfig addExtraAttr(String name) {
|
||||
return this.addExtraAttr(name, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除指定扩展属性
|
||||
* @param name /
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaCookieConfig removeExtraAttr(String name) {
|
||||
if(extraAttrs != null) {
|
||||
this.extraAttrs.remove(name);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// toString
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SaCookieConfig [domain=" + domain + ", path=" + path + ", secure=" + secure + ", httpOnly=" + httpOnly
|
||||
+ ", sameSite=" + sameSite + "]";
|
||||
return "SaCookieConfig [" +
|
||||
"domain=" + domain +
|
||||
", path=" + path +
|
||||
", secure=" + secure +
|
||||
", httpOnly=" + httpOnly +
|
||||
", sameSite=" + sameSite +
|
||||
", extraAttrs=" + extraAttrs +
|
||||
"]";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,14 +15,16 @@
|
||||
*/
|
||||
package cn.dev33.satoken.context.model;
|
||||
|
||||
import cn.dev33.satoken.error.SaErrorCode;
|
||||
import cn.dev33.satoken.exception.SaTokenException;
|
||||
import cn.dev33.satoken.util.SaFoxUtil;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import cn.dev33.satoken.error.SaErrorCode;
|
||||
import cn.dev33.satoken.exception.SaTokenException;
|
||||
import cn.dev33.satoken.util.SaFoxUtil;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Cookie Model,代表一个 Cookie 应该具有的所有参数
|
||||
@@ -77,6 +79,13 @@ public class SaCookie {
|
||||
*/
|
||||
private String sameSite;
|
||||
|
||||
// Cookie 属性参考文章:https://blog.csdn.net/fengbin2005/article/details/136544226
|
||||
|
||||
/**
|
||||
* 额外扩展属性
|
||||
*/
|
||||
private Map<String, String> extraAttrs = new LinkedHashMap<>();
|
||||
|
||||
|
||||
/**
|
||||
* 构造一个
|
||||
@@ -224,13 +233,73 @@ public class SaCookie {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 获取额外扩展属性
|
||||
*/
|
||||
public Map<String, String> getExtraAttrs() {
|
||||
return extraAttrs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入额外扩展属性
|
||||
* @param extraAttrs /
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaCookie setExtraAttrs(Map<String, String> extraAttrs) {
|
||||
this.extraAttrs = extraAttrs;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加扩展属性
|
||||
* @param name /
|
||||
* @param value /
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaCookie addExtraAttr(String name, String value) {
|
||||
if (extraAttrs == null) {
|
||||
extraAttrs = new LinkedHashMap<>();
|
||||
}
|
||||
this.extraAttrs.put(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加扩展属性
|
||||
* @param name /
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaCookie addExtraAttr(String name) {
|
||||
return this.addExtraAttr(name, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除指定扩展属性
|
||||
* @param name /
|
||||
* @return 对象自身
|
||||
*/
|
||||
public SaCookie removeExtraAttr(String name) {
|
||||
if(extraAttrs != null) {
|
||||
this.extraAttrs.remove(name);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// toString
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SaCookie [name=" + name + ", value=" + value + ", maxAge=" + maxAge + ", domain=" + domain + ", path=" + path
|
||||
+ ", secure=" + secure + ", httpOnly=" + httpOnly + ", sameSite="
|
||||
+ sameSite + "]";
|
||||
return "SaCookie [" +
|
||||
"name=" + name +
|
||||
", value=" + value +
|
||||
", maxAge=" + maxAge +
|
||||
", domain=" + domain +
|
||||
", path=" + path
|
||||
+ ", secure=" + secure +
|
||||
", httpOnly=" + httpOnly +
|
||||
", sameSite=" + sameSite +
|
||||
", extraAttrs=" + extraAttrs +
|
||||
"]";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -256,6 +325,7 @@ public class SaCookie {
|
||||
throw new SaTokenException("无效Value:" + value).setCode(SaErrorCode.CODE_12003);
|
||||
}
|
||||
|
||||
// example:
|
||||
// Set-Cookie: name=value; Max-Age=100000; Expires=Tue, 05-Oct-2021 20:28:17 GMT; Domain=localhost; Path=/; Secure; HttpOnly; SameSite=Lax
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
@@ -287,6 +357,17 @@ public class SaCookie {
|
||||
sb.append("; SameSite=").append(sameSite);
|
||||
}
|
||||
|
||||
// 扩展属性
|
||||
if(extraAttrs != null) {
|
||||
extraAttrs.forEach((k, v) -> {
|
||||
if(SaFoxUtil.isEmpty(v)) {
|
||||
sb.append("; ").append(k);
|
||||
} else {
|
||||
sb.append("; ").append(k).append("=").append(v);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import cn.dev33.satoken.exception.SaTokenException;
|
||||
import cn.dev33.satoken.router.SaHttpMethod;
|
||||
import cn.dev33.satoken.util.SaFoxUtil;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -95,7 +96,7 @@ public interface SaRequest {
|
||||
* 获取 [ 请求体 ] 里提交的所有参数名称
|
||||
* @return 参数名称列表
|
||||
*/
|
||||
List<String> getParamNames();
|
||||
Collection<String> getParamNames();
|
||||
|
||||
/**
|
||||
* 获取 [ 请求体 ] 里提交的所有参数
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2020-2099 sa-token.cc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package cn.dev33.satoken.fun.strategy;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 函数式接口:SaCheckELRootMap 扩展函数
|
||||
*
|
||||
* <p> 参数:SaCheckELRootMap 对象 </p>
|
||||
*
|
||||
* @author click33
|
||||
* @since 1.40.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface SaCheckELRootMapExtendFunction extends Consumer<Map<String, Object>> {
|
||||
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2020-2099 sa-token.cc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package cn.dev33.satoken.model.wrapperInfo;
|
||||
|
||||
/**
|
||||
* 返回值包装类:描述一个账号是否已被封禁等信息
|
||||
*
|
||||
* @author click33
|
||||
* @since 1.40.0
|
||||
*/
|
||||
public class SaDisableWrapperInfo {
|
||||
|
||||
/**
|
||||
* 是否被封禁
|
||||
*/
|
||||
public boolean isDisable;
|
||||
|
||||
/**
|
||||
* 封禁剩余时间,单位:秒(-1=永久封禁,0 or -2=未封禁)
|
||||
*/
|
||||
public long disableTime;
|
||||
|
||||
/**
|
||||
* 封禁等级(最小1级,0=未封禁)
|
||||
*/
|
||||
public int disableLevel;
|
||||
|
||||
/**
|
||||
* 构建对象
|
||||
*
|
||||
* @param isDisable 是否被封禁
|
||||
* @param disableTime 封禁剩余时间,单位:秒(-1=永久封禁,0 or -2=未封禁)
|
||||
* @param disableLevel 封禁等级(最小1级,0=未封禁)
|
||||
*/
|
||||
public SaDisableWrapperInfo(boolean isDisable, long disableTime, int disableLevel) {
|
||||
this.isDisable = isDisable;
|
||||
this.disableTime = disableTime;
|
||||
this.disableLevel = disableLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个已封禁描述对象
|
||||
* @param disableTime 封禁时间
|
||||
* @param disableLevel 封禁等级
|
||||
* @return /
|
||||
*/
|
||||
public static SaDisableWrapperInfo createDisabled(long disableTime, int disableLevel) {
|
||||
return new SaDisableWrapperInfo(true, disableTime, disableLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个未封禁描述对象
|
||||
* @return /
|
||||
*/
|
||||
public static SaDisableWrapperInfo createNotDisabled() {
|
||||
return new SaDisableWrapperInfo(false, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个未封禁描述对象,并指定缓存时间,指定时间内不再重复查询
|
||||
* @param cacheTime 缓存时间(单位:秒)
|
||||
* @return /
|
||||
*/
|
||||
public static SaDisableWrapperInfo createNotDisabled(long cacheTime) {
|
||||
return new SaDisableWrapperInfo(false, cacheTime, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SaDisableWrapperInfo{" +
|
||||
"isDisable=" + isDisable +
|
||||
", disableTime=" + disableTime +
|
||||
", disableLevel=" + disableLevel +
|
||||
'}';
|
||||
}
|
||||
|
||||
// setter / getter 仅为兼容部分框架序列化操作,不建议调用
|
||||
|
||||
public boolean getIsDisable() {
|
||||
return isDisable;
|
||||
}
|
||||
|
||||
public SaDisableWrapperInfo setIsDisable(boolean isDisable) {
|
||||
this.isDisable = isDisable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public long getDisableTime() {
|
||||
return disableTime;
|
||||
}
|
||||
|
||||
public SaDisableWrapperInfo setDisableTime(long disableTime) {
|
||||
this.disableTime = disableTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getDisableLevel() {
|
||||
return disableLevel;
|
||||
}
|
||||
|
||||
public SaDisableWrapperInfo setDisableLevel(int disableLevel) {
|
||||
this.disableLevel = disableLevel;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,14 +15,14 @@
|
||||
*/
|
||||
package cn.dev33.satoken.stp;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import cn.dev33.satoken.SaManager;
|
||||
import cn.dev33.satoken.config.SaTokenConfig;
|
||||
import cn.dev33.satoken.dao.SaTokenDao;
|
||||
import cn.dev33.satoken.util.SaTokenConsts;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 在调用 `StpUtil.login()` 时的 配置参数 Model,决定登录的一些细节行为 <br>
|
||||
*
|
||||
@@ -236,26 +236,6 @@ public class SaLoginModel {
|
||||
return isLastingCookie;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return timeout 值 (如果此配置项尚未配置,则取全局配置的值)
|
||||
*/
|
||||
public Long getTimeoutOrGlobalConfig() {
|
||||
if(timeout == null) {
|
||||
timeout = SaManager.getConfig().getTimeout();
|
||||
}
|
||||
return timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 是否在登录后将 Token 写入到响应头 (如果此配置项尚未配置,则取全局配置的值)
|
||||
*/
|
||||
public Boolean getIsWriteHeaderOrGlobalConfig() {
|
||||
if(isWriteHeader == null) {
|
||||
isWriteHeader = SaManager.getConfig().getIsWriteHeader();
|
||||
}
|
||||
return isWriteHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入扩展数据(只在jwt模式下生效)
|
||||
* @param key 键
|
||||
@@ -297,13 +277,11 @@ public class SaLoginModel {
|
||||
if( ! getIsLastingCookieOrFalse()) {
|
||||
return -1;
|
||||
}
|
||||
if(getTimeoutOrGlobalConfig() == SaTokenDao.NEVER_EXPIRE) {
|
||||
long _timeout = getTimeout();
|
||||
if(_timeout == SaTokenDao.NEVER_EXPIRE || _timeout > Integer.MAX_VALUE) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
if (timeout > Integer.MAX_VALUE) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
return (int)(long)timeout;
|
||||
return (int)_timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -353,4 +331,31 @@ public class SaLoginModel {
|
||||
return new SaLoginModel();
|
||||
}
|
||||
|
||||
|
||||
// ---------------- 过期方法
|
||||
|
||||
/**
|
||||
* 请改为 getTimeout
|
||||
* @return timeout 值 (如果此配置项尚未配置,则取全局配置的值)
|
||||
*/
|
||||
@Deprecated
|
||||
public Long getTimeoutOrGlobalConfig() {
|
||||
if(timeout == null) {
|
||||
timeout = SaManager.getConfig().getTimeout();
|
||||
}
|
||||
return timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请改为 getIsWriteHeader
|
||||
* @return 是否在登录后将 Token 写入到响应头 (如果此配置项尚未配置,则取全局配置的值)
|
||||
*/
|
||||
@Deprecated
|
||||
public Boolean getIsWriteHeaderOrGlobalConfig() {
|
||||
if(isWriteHeader == null) {
|
||||
isWriteHeader = SaManager.getConfig().getIsWriteHeader();
|
||||
}
|
||||
return isWriteHeader;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
*/
|
||||
package cn.dev33.satoken.stp;
|
||||
|
||||
import cn.dev33.satoken.model.wrapperInfo.SaDisableWrapperInfo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 权限数据加载源接口
|
||||
* 权限数据源加载接口
|
||||
*
|
||||
* <p>
|
||||
* 在使用权限校验 API 之前,你必须实现此接口,告诉框架哪些用户拥有哪些权限。<br>
|
||||
@@ -48,4 +50,15 @@ public interface StpInterface {
|
||||
*/
|
||||
List<String> getRoleList(Object loginId, String loginType);
|
||||
|
||||
/**
|
||||
* 返回指定账号 id 是否被封禁
|
||||
*
|
||||
* @param loginId 账号id
|
||||
* @param service 业务标识符
|
||||
* @return 描述该账号是否封禁的包装信息对象
|
||||
*/
|
||||
default SaDisableWrapperInfo isDisabled(Object loginId, String service) {
|
||||
return SaDisableWrapperInfo.createNotDisabled();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import cn.dev33.satoken.error.SaErrorCode;
|
||||
import cn.dev33.satoken.exception.*;
|
||||
import cn.dev33.satoken.fun.SaFunction;
|
||||
import cn.dev33.satoken.listener.SaTokenEventCenter;
|
||||
import cn.dev33.satoken.model.wrapperInfo.SaDisableWrapperInfo;
|
||||
import cn.dev33.satoken.session.SaSession;
|
||||
import cn.dev33.satoken.session.TokenSign;
|
||||
import cn.dev33.satoken.strategy.SaStrategy;
|
||||
@@ -203,7 +204,7 @@ public class StpLogic {
|
||||
}
|
||||
|
||||
// 3. 将 token 写入到当前请求的响应头中
|
||||
if(loginModel.getIsWriteHeaderOrGlobalConfig()) {
|
||||
if(loginModel.getIsWriteHeader()) {
|
||||
setTokenValueToResponseHeader(tokenValue);
|
||||
}
|
||||
}
|
||||
@@ -248,6 +249,7 @@ public class StpLogic {
|
||||
.setSecure(cfg.getSecure())
|
||||
.setHttpOnly(cfg.getHttpOnly())
|
||||
.setSameSite(cfg.getSameSite())
|
||||
.setExtraAttrs(cfg.getExtraAttrs())
|
||||
;
|
||||
SaHolder.getResponse().addCookie(cookie);
|
||||
}
|
||||
@@ -471,7 +473,7 @@ public class StpLogic {
|
||||
String tokenValue = distUsableToken(id, loginModel);
|
||||
|
||||
// 4、获取此账号的 Account-Session , 续期
|
||||
SaSession session = getSessionByLoginId(id, true, loginModel.getTimeoutOrGlobalConfig());
|
||||
SaSession session = getSessionByLoginId(id, true, loginModel.getTimeout());
|
||||
session.updateMinTimeout(loginModel.getTimeout());
|
||||
|
||||
// 5、在 Account-Session 上记录本次登录的 token 签名
|
||||
@@ -483,18 +485,24 @@ public class StpLogic {
|
||||
|
||||
// 7、写入这个 token 的最后活跃时间 token-last-active
|
||||
if(isOpenCheckActiveTimeout()) {
|
||||
setLastActiveToNow(tokenValue, loginModel.getActiveTimeout(), loginModel.getTimeoutOrGlobalConfig());
|
||||
setLastActiveToNow(tokenValue, loginModel.getActiveTimeout(), loginModel.getTimeout());
|
||||
}
|
||||
|
||||
// 8、$$ 发布全局事件:账号 xxx 登录成功
|
||||
// 8、如果该 token 对应的 Token-Session 已经存在,则需要给其续期
|
||||
SaSession tokenSession = getTokenSessionByToken(tokenValue, false);
|
||||
if(tokenSession != null) {
|
||||
tokenSession.updateMinTimeout(loginModel.getTimeout());
|
||||
}
|
||||
|
||||
// 9、$$ 发布全局事件:账号 xxx 登录成功
|
||||
SaTokenEventCenter.doLogin(loginType, id, tokenValue, loginModel);
|
||||
|
||||
// 9、检查此账号会话数量是否超出最大值,如果超过,则按照登录时间顺序,把最开始登录的给注销掉
|
||||
// 10、检查此账号会话数量是否超出最大值,如果超过,则按照登录时间顺序,把最开始登录的给注销掉
|
||||
if(config.getMaxLoginCount() != -1) {
|
||||
logoutByMaxLoginCount(id, session, null, config.getMaxLoginCount());
|
||||
}
|
||||
|
||||
// 10、一切处理完毕,返回会话凭证 token
|
||||
// 11、一切处理完毕,返回会话凭证 token
|
||||
return tokenValue;
|
||||
}
|
||||
|
||||
@@ -976,16 +984,23 @@ public class StpLogic {
|
||||
throw NotLoginException.newInstance(loginType, KICK_OUT, KICK_OUT_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11015);
|
||||
}
|
||||
|
||||
// 7、检查此 token 的最后活跃时间是否已经超过了 active-timeout 的限制,如果是则代表其已被冻结,需要抛出:token 已被冻结
|
||||
// 7、token 活跃频率检查
|
||||
if(isOpenCheckActiveTimeout()) {
|
||||
checkActiveTimeout(tokenValue);
|
||||
// storage.get(key, () -> {}) 可以避免一次请求多次校验,造成不必要的性能消耗
|
||||
SaHolder.getStorage().get(SaTokenConsts.TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY, () -> {
|
||||
|
||||
// ------ 至此,loginId 已经是一个合法的值,代表当前会话是一个正常的登录状态了
|
||||
// 7.1、检查此 token 的最后活跃时间是否已经超过了 active-timeout 的限制,如果是则代表其已被冻结,需要抛出:token 已被冻结
|
||||
checkActiveTimeout(tokenValue);
|
||||
|
||||
// 8、如果配置了自动续签功能, 则: 更新这个 token 的最后活跃时间 (注意此处的续签是在续 active-timeout,而非 timeout)
|
||||
if(getConfigOrGlobal().getAutoRenew()) {
|
||||
updateLastActiveToNow(tokenValue);
|
||||
}
|
||||
// ------ 至此,loginId 已经是一个合法的值,代表当前会话是一个正常的登录状态了
|
||||
|
||||
// 7.2、如果配置了自动续签功能, 则: 更新这个 token 的最后活跃时间 (注意此处的续签是在续 active-timeout,而非 timeout)
|
||||
if(getConfigOrGlobal().getAutoRenew()) {
|
||||
updateLastActiveToNow(tokenValue);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// 9、返回 loginId
|
||||
@@ -1522,26 +1537,18 @@ public class StpLogic {
|
||||
*/
|
||||
public void checkActiveTimeout(String tokenValue) {
|
||||
|
||||
// storage.get(key, () -> {}) 可以避免一次请求多次校验,造成不必要的性能消耗
|
||||
SaStorage storage = SaHolder.getStorage();
|
||||
storage.get(SaTokenConsts.TOKEN_ACTIVE_TIMEOUT_CHECKED_KEY, () -> {
|
||||
// 1、获取这个 token 的剩余活跃有效期
|
||||
long activeTimeout = getTokenActiveTimeoutByToken(tokenValue);
|
||||
|
||||
// 1、获取这个 token 的剩余活跃有效期
|
||||
long activeTimeout = getTokenActiveTimeoutByToken(tokenValue);
|
||||
// 2、值为 -1 代表此 token 已经被设置永不冻结,无须继续验证
|
||||
if(activeTimeout == SaTokenDao.NEVER_EXPIRE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2、值为 -1 代表此 token 已经被设置永不冻结,无须继续验证
|
||||
if(activeTimeout == SaTokenDao.NEVER_EXPIRE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3、值为 -2 代表已被冻结,此时需要抛出异常
|
||||
if(activeTimeout == SaTokenDao.NOT_VALUE_EXPIRE) {
|
||||
throw NotLoginException.newInstance(loginType, TOKEN_FREEZE, TOKEN_FREEZE_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11016);
|
||||
}
|
||||
|
||||
// --- 验证通过
|
||||
return true;
|
||||
});
|
||||
// 3、值为 -2 代表已被冻结,此时需要抛出异常
|
||||
if(activeTimeout == SaTokenDao.NOT_VALUE_EXPIRE) {
|
||||
throw NotLoginException.newInstance(loginType, TOKEN_FREEZE, TOKEN_FREEZE_MESSAGE, tokenValue).setCode(SaErrorCode.CODE_11016);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2407,8 +2414,8 @@ public class StpLogic {
|
||||
if(SaFoxUtil.isEmpty(service)) {
|
||||
throw new SaTokenException("请提供要封禁的服务").setCode(SaErrorCode.CODE_11063);
|
||||
}
|
||||
if(level < SaTokenConsts.MIN_DISABLE_LEVEL) {
|
||||
throw new SaTokenException("封禁等级不可以小于最小值:" + SaTokenConsts.MIN_DISABLE_LEVEL).setCode(SaErrorCode.CODE_11064);
|
||||
if(level < SaTokenConsts.MIN_DISABLE_LEVEL && level != 0) {
|
||||
throw new SaTokenException("封禁等级不可以小于最小值:" + SaTokenConsts.MIN_DISABLE_LEVEL + " (0除外)").setCode(SaErrorCode.CODE_11064);
|
||||
}
|
||||
|
||||
// 打上封禁标记
|
||||
@@ -2467,13 +2474,12 @@ public class StpLogic {
|
||||
*/
|
||||
public void checkDisableLevel(Object loginId, String service, int level) {
|
||||
// 1、先前置检查一下这个账号是否被封禁了
|
||||
String value = getSaTokenDao().get(splicingKeyDisable(loginId, service));
|
||||
if(SaFoxUtil.isEmpty(value)) {
|
||||
int disableLevel = getDisableLevel(loginId, service);
|
||||
if(disableLevel == SaTokenConsts.NOT_DISABLE_LEVEL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2、再判断被封禁的等级是否达到了指定级别
|
||||
Integer disableLevel = SaFoxUtil.getValueByType(value, int.class);
|
||||
if(disableLevel >= level) {
|
||||
throw new DisableServiceException(loginType, loginId, service, disableLevel, level, getDisableTime(loginId, service))
|
||||
.setCode(SaErrorCode.CODE_11061);
|
||||
@@ -2498,14 +2504,22 @@ public class StpLogic {
|
||||
* @return /
|
||||
*/
|
||||
public int getDisableLevel(Object loginId, String service) {
|
||||
// 1、判断是否被封禁了,如果尚未被封禁,返回-2
|
||||
// 1、先从缓存中查询数据,缓存中有值,以缓存值优先
|
||||
String value = getSaTokenDao().get(splicingKeyDisable(loginId, service));
|
||||
if(SaFoxUtil.isEmpty(value)) {
|
||||
return SaTokenConsts.NOT_DISABLE_LEVEL;
|
||||
if(SaFoxUtil.isNotEmpty(value)) {
|
||||
return SaFoxUtil.getValueByType(value, int.class);
|
||||
}
|
||||
|
||||
// 2、转为 int 类型返回
|
||||
return SaFoxUtil.getValueByType(value, int.class);
|
||||
// 2、如果缓存中无数据,则从"数据加载器"中再次查询
|
||||
SaDisableWrapperInfo disableWrapperInfo = SaManager.getStpInterface().isDisabled(loginId, service);
|
||||
|
||||
// 如果返回值 disableTime 有效,则代表返回结果需要写入缓存
|
||||
if(disableWrapperInfo.disableTime == SaTokenDao.NEVER_EXPIRE || disableWrapperInfo.disableTime > 0) {
|
||||
disableLevel(loginId, service, disableWrapperInfo.disableLevel, disableWrapperInfo.disableTime);
|
||||
}
|
||||
|
||||
// 返回查询结果
|
||||
return disableWrapperInfo.disableLevel;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ package cn.dev33.satoken.strategy;
|
||||
|
||||
import cn.dev33.satoken.annotation.*;
|
||||
import cn.dev33.satoken.annotation.handler.*;
|
||||
import cn.dev33.satoken.fun.strategy.SaCheckELRootMapExtendFunction;
|
||||
import cn.dev33.satoken.fun.strategy.SaCheckMethodAnnotationFunction;
|
||||
import cn.dev33.satoken.fun.strategy.SaGetAnnotationFunction;
|
||||
import cn.dev33.satoken.fun.strategy.SaIsAnnotationPresentFunction;
|
||||
@@ -130,4 +131,13 @@ public final class SaAnnotationStrategy {
|
||||
instance.getAnnotation.apply(method.getDeclaringClass(), annotationClass) != null;
|
||||
};
|
||||
|
||||
/**
|
||||
* SaCheckELRootMap 扩展函数
|
||||
*/
|
||||
public SaCheckELRootMapExtendFunction checkELRootMapExtendFunction = rootMap -> {
|
||||
// 默认不做任何处理
|
||||
};
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2020-2099 sa-token.cc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package cn.dev33.satoken.strategy;
|
||||
|
||||
import cn.dev33.satoken.exception.RequestPathInvalidException;
|
||||
import cn.dev33.satoken.fun.strategy.SaCheckRequestPathFunction;
|
||||
import cn.dev33.satoken.fun.strategy.SaRequestPathInvalidHandleFunction;
|
||||
|
||||
/**
|
||||
* Sa-Token 防火墙策略
|
||||
*
|
||||
* @author click33
|
||||
* @since 1.40.0
|
||||
*/
|
||||
public final class SaFirewallStrategy {
|
||||
|
||||
private SaFirewallStrategy() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局单例引用
|
||||
*/
|
||||
public static final SaFirewallStrategy instance = new SaFirewallStrategy();
|
||||
|
||||
|
||||
// ----------------------- 所有策略
|
||||
|
||||
|
||||
/**
|
||||
* 请求 path 黑名单
|
||||
*/
|
||||
public String[] blackPaths = {};
|
||||
|
||||
/**
|
||||
* 请求 path 白名单
|
||||
*/
|
||||
public String[] whitePaths = {};
|
||||
|
||||
/**
|
||||
* 请求 path 不允许出现的字符
|
||||
*/
|
||||
public String[] invalidCharacter = {
|
||||
"//", // //
|
||||
"\\", // \
|
||||
"%2e", "%2E", // .
|
||||
"%2f", "%2F", // /
|
||||
"%5c", "%5C", // \
|
||||
";", "%3b", "%3B", // ; // 参考资料:https://mp.weixin.qq.com/s/77CIDZbgBwRunJeluofPTA
|
||||
"%25" // 空格
|
||||
};
|
||||
|
||||
/**
|
||||
* 校验请求 path 的算法
|
||||
*/
|
||||
public SaCheckRequestPathFunction checkRequestPath = (requestPath, extArg1, extArg2) -> {
|
||||
// 1、如果在白名单里,则直接放行
|
||||
for (String item : whitePaths) {
|
||||
if (requestPath.equals(item)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2、如果在黑名单里,则抛出异常
|
||||
for (String item : blackPaths) {
|
||||
if (requestPath.equals(item)) {
|
||||
throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 3、检查是否包含非法字符
|
||||
|
||||
// 不允许为null
|
||||
if(requestPath == null) {
|
||||
throw new RequestPathInvalidException("非法请求:null", null);
|
||||
}
|
||||
// 不允许包含非法字符
|
||||
for (String item : invalidCharacter) {
|
||||
if (requestPath.contains(item)) {
|
||||
throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath);
|
||||
}
|
||||
}
|
||||
// 不允许出现跨目录字符
|
||||
if(requestPath.contains("/.") || requestPath.contains("\\.")) {
|
||||
throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 当请求 path 校验不通过时处理方案的算法,自定义示例:
|
||||
* <pre>
|
||||
* SaFirewallStrategy.instance.requestPathInvalidHandle = (e, extArg1, extArg2) -> {
|
||||
* // 自定义处理逻辑 ...
|
||||
* };
|
||||
* </pre>
|
||||
*/
|
||||
public SaRequestPathInvalidHandleFunction requestPathInvalidHandle = null;
|
||||
|
||||
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
package cn.dev33.satoken.strategy;
|
||||
|
||||
import cn.dev33.satoken.SaManager;
|
||||
import cn.dev33.satoken.exception.RequestPathInvalidException;
|
||||
import cn.dev33.satoken.exception.SaTokenException;
|
||||
import cn.dev33.satoken.fun.strategy.*;
|
||||
import cn.dev33.satoken.session.SaSession;
|
||||
@@ -164,49 +163,6 @@ public final class SaStrategy {
|
||||
return new StpLogic(loginType);
|
||||
};
|
||||
|
||||
/**
|
||||
* 请求 path 不允许出现的字符
|
||||
*/
|
||||
public static String[] INVALID_CHARACTER = {
|
||||
"//", "\\",
|
||||
"%2e", "%2E", // .
|
||||
"%2f", "%2F", // /
|
||||
"%5c", "%5C", // \
|
||||
"%25" // 空格
|
||||
};
|
||||
|
||||
/**
|
||||
* 校验请求 path 的算法
|
||||
*/
|
||||
public SaCheckRequestPathFunction checkRequestPath = (requestPath, extArg1, extArg2) -> {
|
||||
|
||||
// 不允许为null
|
||||
if(requestPath == null) {
|
||||
throw new RequestPathInvalidException("非法请求:null", null);
|
||||
}
|
||||
// 不允许包含非法字符
|
||||
for (String item : INVALID_CHARACTER) {
|
||||
if (requestPath.contains(item)) {
|
||||
throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath);
|
||||
}
|
||||
}
|
||||
// 不允许出现跨目录
|
||||
if(requestPath.contains("/.") || requestPath.contains("\\.")) {
|
||||
throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 当请求 path 校验不通过时处理方案的算法,自定义示例:
|
||||
* <pre>
|
||||
* SaStrategy.instance.requestPathInvalidHandle = (e, extArg1, extArg2) -> {
|
||||
* // 自定义处理逻辑 ...
|
||||
* };
|
||||
* </pre>
|
||||
*/
|
||||
public SaRequestPathInvalidHandleFunction requestPathInvalidHandle = null;
|
||||
|
||||
|
||||
// ----------------------- 重写策略 set连缀风格
|
||||
|
||||
|
||||
@@ -443,15 +443,15 @@ public class SaFoxUtil {
|
||||
url = "";
|
||||
}
|
||||
int index = url.lastIndexOf('#');
|
||||
// ? 不存在
|
||||
// # 不存在
|
||||
if(index == -1) {
|
||||
return url + '#' + paramStr;
|
||||
}
|
||||
// ? 是最后一位
|
||||
// # 是最后一位
|
||||
if(index == url.length() - 1) {
|
||||
return url + paramStr;
|
||||
}
|
||||
// ? 是其中一位
|
||||
// # 是其中一位
|
||||
if(index < url.length() - 1) {
|
||||
String separatorChar = "&";
|
||||
// 如果最后一位是 不是&, 且 paramStr 第一位不是 &, 就赠送一个 &
|
||||
|
||||
@@ -36,7 +36,7 @@ public class SaTokenConsts {
|
||||
/**
|
||||
* Sa-Token 当前版本号
|
||||
*/
|
||||
public static final String VERSION_NO = "v1.39.0";
|
||||
public static final String VERSION_NO = "v1.40.0";
|
||||
|
||||
/**
|
||||
* Sa-Token 开源地址 Gitee
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -73,7 +73,7 @@
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-bom</artifactId>
|
||||
<version>1.39.0</version>
|
||||
<version>1.40.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -45,7 +45,14 @@
|
||||
<artifactId>sa-token-redis-jackson</artifactId>
|
||||
<version>${sa-token.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- Sa-Token 注解鉴权使用 EL 表达式 -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-el</artifactId>
|
||||
<version>${sa-token.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 提供Redis连接池 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package com.pj.cases.more;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckEL;
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* SaCheckEL EL表达式注解鉴权示例
|
||||
*
|
||||
* @author click33
|
||||
* @since 2022-10-13
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/check-el/")
|
||||
public class SaCheckELController {
|
||||
|
||||
// 登录校验 ---- http://localhost:8081/check-el/test1
|
||||
@SaCheckEL("stp.checkLogin()")
|
||||
@RequestMapping("test1")
|
||||
public SaResult test1() {
|
||||
return SaResult.ok();
|
||||
}
|
||||
|
||||
// 角色校验 ---- http://localhost:8081/check-el/test2
|
||||
@SaCheckEL("stp.checkRole('dev-admin')")
|
||||
@RequestMapping("test2")
|
||||
public SaResult test2() {
|
||||
return SaResult.ok();
|
||||
}
|
||||
|
||||
// 权限校验 ---- http://localhost:8081/check-el/test3
|
||||
@SaCheckEL("stp.checkPermission('user:edit')")
|
||||
@RequestMapping("test3")
|
||||
public SaResult test3() {
|
||||
return SaResult.ok();
|
||||
}
|
||||
|
||||
// 二级认证 ---- http://localhost:8081/check-el/test4
|
||||
@SaCheckEL("stp.checkSafe()")
|
||||
@RequestMapping("test4")
|
||||
public SaResult test4() {
|
||||
return SaResult.ok();
|
||||
}
|
||||
|
||||
// 参数长度校验 ---- http://localhost:8081/check-el/test5?name=zhangsan
|
||||
@SaCheckEL("NEED( #name.length() > 3 )")
|
||||
@RequestMapping("test5")
|
||||
public SaResult test5(@RequestParam(defaultValue = "") String name) {
|
||||
return SaResult.ok().set("name", name);
|
||||
}
|
||||
|
||||
// 参数长度校验,并自定义异常描述信息 ---- http://localhost:8081/check-el/test6?name=z
|
||||
@SaCheckEL("NEED( #name !=null && #name.length() > 3, 'name长度不够' )")
|
||||
@RequestMapping("test6")
|
||||
public SaResult test6(String name) {
|
||||
return SaResult.ok().set("name", name);
|
||||
}
|
||||
|
||||
// 已登录, 或者查询数据在公开范围内 ---- http://localhost:8081/check-el/test7?id=10044
|
||||
@SaCheckEL("NEED( stp.isLogin() or (#id != null and #id > 10010) )")
|
||||
@RequestMapping("test7")
|
||||
public SaResult test7(long id) {
|
||||
return SaResult.ok().set("id", id);
|
||||
}
|
||||
|
||||
// SaSession 里取值校验 ---- http://localhost:8081/check-el/test8
|
||||
@SaCheckEL("NEED( stp.getSession().get('name') == 'zhangsan' )")
|
||||
@RequestMapping("test8")
|
||||
public SaResult test8() {
|
||||
return SaResult.ok();
|
||||
}
|
||||
|
||||
// 多账号体系鉴权测试 ---- http://localhost:8081/check-el/test9
|
||||
@SaCheckEL("stpUser.checkLogin()")
|
||||
@RequestMapping("test9")
|
||||
public SaResult test9() {
|
||||
return SaResult.ok();
|
||||
}
|
||||
|
||||
// 本模块需要鉴权的权限码
|
||||
public String permissionCode = "article:add";
|
||||
|
||||
// 调用本类的成员变量 ---- http://localhost:8081/check-el/test10
|
||||
@SaCheckEL("stp.checkPermission( this.permissionCode )")
|
||||
@RequestMapping("test10")
|
||||
public SaResult test10() {
|
||||
return SaResult.ok();
|
||||
}
|
||||
|
||||
// 忽略鉴权测试 ---- http://localhost:8081/check-el/test11
|
||||
@SaIgnore
|
||||
@SaCheckEL("stp.checkPermission( 'abc' )")
|
||||
@RequestMapping("test11")
|
||||
public SaResult test11() {
|
||||
return SaResult.ok();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+10
-1
@@ -120,6 +120,15 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
SaAnnotationStrategy.instance.getAnnotation = (element, annotationClass) -> {
|
||||
return AnnotatedElementUtils.getMergedAnnotation(element, annotationClass);
|
||||
};
|
||||
|
||||
// 重写 SaCheckELRootMap 扩展函数,增加注解鉴权 EL 表达式可使用的根对象
|
||||
SaAnnotationStrategy.instance.checkELRootMapExtendFunction = rootMap -> {
|
||||
System.out.println("--------- 执行 SaCheckELRootMap 增强,目前已包含的的跟对象包括:" + rootMap.keySet());
|
||||
// 新增 stpUser 根对象,使之可以在表达式中通过 stpUser.checkLogin() 方式进行多账号体系鉴权
|
||||
rootMap.put("stpUser", StpUserUtil.getStpLogic());
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package com.pj.satoken;
|
||||
|
||||
import cn.dev33.satoken.stp.StpInterface;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import cn.dev33.satoken.stp.StpInterface;
|
||||
|
||||
/**
|
||||
* 自定义权限认证接口扩展,Sa-Token 将从此实现类获取每个账号拥有的权限码
|
||||
*
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
<dubbo.version>2.7.21</dubbo.version>
|
||||
<nacos.version>1.4.2</nacos.version>
|
||||
</properties>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
<dubbo.version>2.7.21</dubbo.version>
|
||||
<nacos.version>1.4.2</nacos.version>
|
||||
</properties>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
<dubbo.version>3.2.2</dubbo.version>
|
||||
<nacos.version>2.2.2</nacos.version>
|
||||
</properties>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
<dubbo.version>3.2.2</dubbo.version>
|
||||
<nacos.version>2.2.2</nacos.version>
|
||||
</properties>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<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-freemarker</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
|
||||
<!-- SpringBoot -->
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.5.14</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- Freemarker 视图引擎 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-freemarker</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Sa-Token 权限认证, 在线文档:https://sa-token.cc/ -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot-starter</artifactId>
|
||||
<version>${sa-token.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 在 Freemarker 页面中使用 Sa-Token 自定义标签 -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-freemarker</artifactId>
|
||||
<version>${sa-token.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 热刷新 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- @ConfigurationProperties -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
</project>
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.pj;
|
||||
|
||||
import cn.dev33.satoken.SaManager;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* 参考链接:https://blog.csdn.net/m0_64210833/article/details/135994864
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class SaTokenFreemarkerDemoApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SaTokenFreemarkerDemoApplication.class, args);
|
||||
System.out.println("\n启动成功,Sa-Token 配置如下:" + SaManager.getConfig());
|
||||
System.out.println("\n测试访问:http://localhost:8081/");
|
||||
}
|
||||
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.pj.satoken;
|
||||
|
||||
import cn.dev33.satoken.freemarker.dialect.SaTokenTemplateModel;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import freemarker.template.TemplateModelException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
|
||||
/**
|
||||
* [Sa-Token 权限认证] 配置类
|
||||
*
|
||||
* @author click33
|
||||
*/
|
||||
@Configuration
|
||||
public class SaTokenConfigure {
|
||||
|
||||
@Autowired
|
||||
FreeMarkerConfigurer configurer;
|
||||
|
||||
/**
|
||||
* 注入 Sa-Token Freemarker 标签模板模型 对象
|
||||
*/
|
||||
@PostConstruct
|
||||
public void setSaTokenTemplateModel() throws TemplateModelException {
|
||||
|
||||
// 注入 Sa-Token Freemarker 标签模板模型,使之可以在 xxx.ftl 文件中使用 sa 标签,
|
||||
// 例如:<#if sa.login()>...</#if>
|
||||
configurer.getConfiguration().setSharedVariable("sa", new SaTokenTemplateModel());
|
||||
|
||||
// 注入 Sa-Token Freemarker 全局对象,使之可以在 xxx.ftl 文件中调用 StpLogic 相关方法,
|
||||
// 例如:<span>${stp.getSession().get('name')}</span>
|
||||
configurer.getConfiguration().setSharedVariable("stp", StpUtil.stpLogic);
|
||||
}
|
||||
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.pj.satoken;
|
||||
|
||||
import cn.dev33.satoken.stp.StpInterface;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 自定义权限验证接口扩展
|
||||
*/
|
||||
@Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展
|
||||
public class StpInterfaceImpl implements StpInterface {
|
||||
|
||||
/**
|
||||
* 返回一个账号所拥有的权限码集合
|
||||
*/
|
||||
@Override
|
||||
public List<String> getPermissionList(Object loginId, String loginType) {
|
||||
// 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限
|
||||
List<String> list = new ArrayList<String>();
|
||||
list.add("101");
|
||||
list.add("user-add");
|
||||
list.add("user-delete");
|
||||
list.add("user-update");
|
||||
list.add("user-get");
|
||||
list.add("article-get");
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回一个账号所拥有的角色标识集合
|
||||
*/
|
||||
@Override
|
||||
public List<String> getRoleList(Object loginId, String loginType) {
|
||||
// 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色
|
||||
List<String> list = new ArrayList<String>();
|
||||
list.add("admin");
|
||||
list.add("super-admin");
|
||||
return list;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.pj.test;
|
||||
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 全局异常处理
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
public class GlobalException {
|
||||
|
||||
// 全局异常拦截(拦截项目中的所有异常)
|
||||
@ExceptionHandler
|
||||
public SaResult handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
e.printStackTrace();
|
||||
return SaResult.error(e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.pj.test;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
/**
|
||||
* 测试 Controller
|
||||
*
|
||||
* @author click33
|
||||
*/
|
||||
@RestController
|
||||
public class TestController {
|
||||
|
||||
// 首页
|
||||
@RequestMapping("/")
|
||||
public Object index() {
|
||||
return new ModelAndView("index");
|
||||
}
|
||||
|
||||
// 登录
|
||||
@RequestMapping("login")
|
||||
public SaResult login(@RequestParam(defaultValue="10001") String id) {
|
||||
StpUtil.login(id);
|
||||
StpUtil.getSession().set("name", "zhangsan");
|
||||
return SaResult.ok();
|
||||
}
|
||||
|
||||
// 注销
|
||||
@RequestMapping("logout")
|
||||
public SaResult logout() {
|
||||
StpUtil.logout();
|
||||
return SaResult.ok();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# 端口
|
||||
server:
|
||||
port: 8081
|
||||
|
||||
spring:
|
||||
# Freemarker 相关配置
|
||||
freemarker:
|
||||
# 指定模板文件的目录
|
||||
template-loader-path: classpath:/templates
|
||||
# 指定Freemarker模板文件的后缀名
|
||||
suffix: .ftl
|
||||
# 关闭模板缓存,方便测试
|
||||
cache: false
|
||||
# 检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试
|
||||
settings:
|
||||
template_update_delay: 0
|
||||
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<title>Sa-Token 集成 Freemarker 标签方言</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
</head>
|
||||
<body>
|
||||
<div class="view-box" style="padding: 30px;">
|
||||
<h2>Sa-Token 集成 Freemarker 标签方言 —— 测试页面</h2>
|
||||
<p>当前是否登录:<#if stp.isLogin()>是<#else>否</#if></p>
|
||||
<p>
|
||||
<a href="login" target="_blank">登录</a>
|
||||
<a href="logout" target="_blank">注销</a>
|
||||
</p>
|
||||
|
||||
<p>登录之后才能显示:<@sa.login>value</@sa.login></p>
|
||||
<p>不登录才能显示:<@sa.notLogin>value</@sa.notLogin></p>
|
||||
|
||||
<p>具有角色 admin 才能显示:<@sa.hasRole value="admin">value</@sa.hasRole></p>
|
||||
<p>同时具备多个角色才能显示:<@sa.hasRoleAnd value="admin, ceo, cto">value</@sa.hasRoleAnd></p>
|
||||
<p>只要具有其中一个角色就能显示:<@sa.hasRoleOr value="admin, ceo, cto">value</@sa.hasRoleOr></p>
|
||||
<p>不具有角色 admin 才能显示:<@sa.notRole value="admin">value</@sa.notRole></p>
|
||||
|
||||
<p>具有权限 user-add 才能显示:<@sa.hasPermission value="user-add">value</@sa.hasPermission></p>
|
||||
<p>同时具备多个权限才能显示:<@sa.hasPermissionAnd value="user-add, user-delete, user-get">value</@sa.hasPermissionAnd></p>
|
||||
<p>只要具有其中一个权限就能显示:<@sa.hasPermissionOr value="user-add, user-delete, user-get">value</@sa.hasPermissionOr></p>
|
||||
<p>不具有权限 user-add 才能显示:<@sa.notPermission value="user-add">value</@sa.notPermission></p>
|
||||
|
||||
<p>
|
||||
从SaSession中取值:
|
||||
<#if stp.isLogin()>
|
||||
<span>${stp.getSession().get('name')}</span>
|
||||
</#if>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -27,7 +27,7 @@
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<lombok.version>1.18.10</lombok.version>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -0,0 +1,633 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Sa-Token-OAuth2 Client 端 - 测试页</title>
|
||||
<style type="text/css">
|
||||
body{background-color: #F0F9EB;}
|
||||
*{margin: 0px; padding: 0px;}
|
||||
.login-box{max-width: 1000px; margin: 30px auto; padding: 1em;}
|
||||
/* 全局配置盒子 */
|
||||
.in-cfg-box{line-height: 30px; padding-top: 10px;}
|
||||
.in-cfg-box .in-cfg-div{display: flex;}
|
||||
.in-cfg-box .in-cfg-div>span{width: 280px;}
|
||||
.in-cfg-box .in-cfg-div>input{flex: 1;}
|
||||
|
||||
.login-box textarea{width: calc(100% - 1em); padding: 0.5em; min-height: 4em; box-sizing: border-box;}
|
||||
.login-box button{padding: 5px 15px; margin-top: 5px; margin-bottom: 5px; cursor: pointer; }
|
||||
.login-box select{ height: 30px; cursor: pointer; }
|
||||
|
||||
.login-box h4{margin-top: 20px;}
|
||||
|
||||
.btn-box{margin-top: 10px; margin-bottom: 15px;}
|
||||
.btn-box a{margin-right: 10px;}
|
||||
.btn-box a:hover{text-decoration:underline !important;}
|
||||
.login-box input{line-height: 25px; margin-bottom: 10px; padding-left: 5px;}
|
||||
.login-box a{text-decoration: none;}
|
||||
.pst{color: #666; margin-top: 15px;}
|
||||
.ps{color: #666; margin-bottom: 5px;}
|
||||
|
||||
.oauth2-server-urls-box{ padding: 0.5em 0; padding-bottom: 0; margin-bottom: 0.5em; background-color: #ddd;}
|
||||
.oauth2-server-urls-box .in-cfg-div>span{text-indent: 0.5em;}
|
||||
|
||||
/* loading框样式 */
|
||||
.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}
|
||||
.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}
|
||||
.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-box">
|
||||
<h2>Sa-Token-OAuth2 Client 端 测试页 </h2>
|
||||
<p class="pst">注:为方便测试,此处将应用秘钥等敏感信息采用前端填写式,真实项目应该改为后端配置。</p>
|
||||
<br><hr><br>
|
||||
<h3>配置信息</h3>
|
||||
<div class="in-cfg-box">
|
||||
<div class="in-cfg-div"><span>OAuth2 Server 主机地址:</span><input class="in-cfg" name="oauth2_server_url" /></div>
|
||||
<div class="oauth2-server-urls-box">
|
||||
<div class="in-cfg-div"><span>OAuth2 Server 授权页地址:</span><input class="in-cfg" name="oauth2_server_auth_url" /></div>
|
||||
<div class="in-cfg-div"><span>OAuth2 Server 获取 token 地址:</span><input class="in-cfg" name="oauth2_server_token_url" /></div>
|
||||
<div class="in-cfg-div"><span>OAuth2 Server 刷新 token 地址:</span><input class="in-cfg" name="oauth2_server_refresh_token_url" /></div>
|
||||
<div class="in-cfg-div"><span>OAuth2 Server 获取 userinfo 地址:</span><input class="in-cfg" name="oauth2_server_userinfo_url" /></div>
|
||||
<div class="in-cfg-div" style="margin-top: -5px;"> <button onclick="autoSplicingUrl();">根据主机 URL 一键拼接授权页等地址</button></div>
|
||||
</div>
|
||||
<div class="in-cfg-div"><span>Client Id:</span><input class="in-cfg" name="client_id" /></div>
|
||||
<div class="in-cfg-div"><span>Client Secret:</span><input class="in-cfg" name="client_secret" /></div>
|
||||
<div class="in-cfg-div"><span>重定向授权地址:</span><input class="in-cfg" name="redirect_uri" /></div>
|
||||
<div class="in-cfg-div"><span>请求 Scope (多个用逗号/空格隔开):</span><input class="in-cfg" name="scope" /></div>
|
||||
</div>
|
||||
<div class="btn-box">
|
||||
<button onclick="clearLocalCfg()">清空配置</button>
|
||||
<button onclick="clearLocalCfg(); readLocalCfg();">恢复默认配置</button>
|
||||
<button onclick="autoFillGiteeUrl();">一键填写 Gitee 参数样例</button>
|
||||
<button onclick="autoFillSaSsoMaxUrl();">一键填写 Sa-Sso-Max 参数样例</button>
|
||||
<a href="javascript: location.href = location.href.split('?')[0].split('#')[0];">回到首页</a>
|
||||
</div>
|
||||
|
||||
<hr><br>
|
||||
<h3>模式一:授权码(Authorization Code)</h3>
|
||||
<p class="pst">授权码:OAuth2.0标准授权流程,先 (重定向) 获取Code授权码,再 (Rest API) 获取 Access-Token 和 Openid 等信息</p>
|
||||
|
||||
<h4>1、获取授权码</h4>
|
||||
<button onclick="buildAuthorizationCodeUrl()">构建授权地址</button>
|
||||
<textarea class="auth-code-url"></textarea>
|
||||
<button onclick="jumpAuthCodeUrl()">→ 访问上述授权地址</button> <br>
|
||||
<span>从 URL 上读取到的 code 为:<span class="show-url-code" style="color: green;"></span></span>
|
||||
|
||||
<h4>2、获取 Access-Token </h4>
|
||||
<button onclick="buildCodeTakeTokenUrl()">构建 code 换 Access-Token 接口地址</button>
|
||||
<textarea class="code-take-token-url"></textarea>
|
||||
<button onclick="ajaxCodeToAccessToken()">→ 请求上述地址,获取 Access-Token 数据</button> 请求结果显示如下:
|
||||
<textarea class="code-take-token-result"></textarea>
|
||||
|
||||
<h4>3、刷新 Access-Token </h4>
|
||||
<button onclick="buildRefreshTokenUrl()">构建刷新 Access-Token 接口地址 </button>
|
||||
请先填写 Refresh-Token 值:<input name="refresh-token-input" style="width: 500px;">
|
||||
<textarea class="refresh-token-url"></textarea>
|
||||
<p class="ps">我们可以拿着 Refresh-Token 去刷新我们的 Access-Token,每次刷新后旧Token将作废</p>
|
||||
<button onclick="ajaxRefreshToken()">→ 请求上述地址,刷新 Access-Token </button> 请求结果显示如下:
|
||||
<textarea class="refresh-token-result"></textarea>
|
||||
|
||||
<h4>4、获取 Userinfo </h4>
|
||||
<button onclick="buildUserinfoUrl()">构建刷新 Userinfo 接口地址 </button>
|
||||
请先填写 Access-Token 值:<input name="access-token-input" style="width: 500px;">
|
||||
<textarea class="userinfo-url"></textarea>
|
||||
<p class="ps">使用 Access-Token 置换资源: 获取账号昵称、头像、性别等信息 (Access-Token具备userinfo权限时才可以获取成功) </p>
|
||||
<button onclick="ajaxUserinfoUrl()">→ 请求上述地址,获取 Userinfo 信息 </button>
|
||||
(
|
||||
请求 Method:
|
||||
<select name="userinfo-ajax-method">
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
<option value="DELETE">DELETE</option>
|
||||
<option value="HEAD">HEAD</option>
|
||||
<option value="OPTIONS">OPTIONS</option>
|
||||
</select>
|
||||
)
|
||||
请求结果显示如下:
|
||||
<textarea class="userinfo-result"></textarea>
|
||||
|
||||
<h4>5、回收 Access-Token </h4>
|
||||
<button onclick="buildRevokeTokenUrl()">构建回收 Access-Token 接口地址 </button>
|
||||
<!-- 请先填写 Access-Token 值:<input name="access-token-input" style="width: 500px;"> -->
|
||||
<textarea class="revoke-token-url"></textarea>
|
||||
<p class="ps">回收后,该 Access-Token 将无法再使用(点击上面的 Userinfo 接口试一试)</p>
|
||||
<button onclick="ajaxRevokeTokenUrl()">→ 请求上述地址,回收 Access-Token </button> 请求结果显示如下:
|
||||
<textarea class="revoke-token-result"></textarea>
|
||||
|
||||
|
||||
<br><br>
|
||||
<h3>模式二:隐藏式(Implicit)</h3>
|
||||
<p class="pst">越过授权码的步骤,直接返回token到前端页面( 格式:http://domain.com#token=xxxx-xxxx )</p>
|
||||
|
||||
<button onclick="buildImplicitUrl()">构建授权地址</button>
|
||||
<textarea class="implicit-url"></textarea>
|
||||
<button onclick="jumpImplicitUrl()">→ 访问上述授权地址</button> <br>
|
||||
<span>从 URL 上读取到的 Access-Token 为:<span class="show-url-access-token" style="color: green;"></span></span>
|
||||
|
||||
|
||||
<br><br>
|
||||
<h3>模式三:密码式(Password)</h3>
|
||||
<p class="pst">注解在 OAuth2-Client 端,输入用户名和密码获取 Access-Token,此模式只适用于高度信任的客户端</p>
|
||||
|
||||
<button onclick="buildPasswordUrl()">构建授权地址</button>
|
||||
 账号:<input class="in-cfg" name="username">
|
||||
 密码:<input class="in-cfg" name="password">
|
||||
<textarea class="password-url"></textarea>
|
||||
<button onclick="ajaxPasswordUrl()">→ 请求上述地址,获取 Access-Token 数据</button> 请求结果显示如下:
|
||||
<textarea class="password-result"></textarea>
|
||||
|
||||
<br><br>
|
||||
<h3>模式四:凭证式(Client Credentials)</h3>
|
||||
<p class="pst">以上三种模式获取的都是用户的 Access-Token,代表用户对第三方应用的授权,在OAuth2.0中还有一种针对 Client级别的授权,
|
||||
即:Client-Token,代表应用自身的资源授权</p>
|
||||
|
||||
<button onclick="buildClientTokenUrl()">构建 Client-Token 授权地址</button>
|
||||
<textarea class="client-token-url"></textarea>
|
||||
<button onclick="ajaxClientTokenUrl()">→ 请求上述地址,获取 Access-Token 数据</button> 请求结果显示如下:
|
||||
<textarea class="client-token-result"></textarea>
|
||||
|
||||
|
||||
<br><br>
|
||||
<span>更多资料请参考 Sa-Token 官方文档地址:</span>
|
||||
<a href="https://sa-token.cc/">https://sa-token.cc/</a>
|
||||
|
||||
<div style="height: 200px;"></div>
|
||||
</div>
|
||||
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
|
||||
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
|
||||
<script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')</script>
|
||||
<!-- 配置缓存读取 -->
|
||||
<script>
|
||||
// 缓存前缀
|
||||
var prefix = "IN_CFG_";
|
||||
function getLocalCfg(key) {
|
||||
return localStorage.getItem(prefix + key);
|
||||
}
|
||||
function setLocalCfg(key, value) {
|
||||
localStorage.setItem(prefix + key, value);
|
||||
}
|
||||
|
||||
// 全局配置变动时,存储到本地
|
||||
$('.in-cfg').bind('input propertychange', function(){
|
||||
var name = $(this).attr('name');
|
||||
var value = $(this).val();
|
||||
setLocalCfg(name, value);
|
||||
})
|
||||
|
||||
// 默认配置
|
||||
var defaultCfg = {
|
||||
oauth2_server_url: 'http://sa-oauth-server.com:8000', // OAuth2 服务端主机地址
|
||||
oauth2_server_auth_url: 'http://sa-oauth-server.com:8000/oauth2/authorize', // OAuth2 授权页地址
|
||||
oauth2_server_token_url: 'http://sa-oauth-server.com:8000/oauth2/token', // OAuth2 获取 token 地址
|
||||
oauth2_server_refresh_token_url: 'http://sa-oauth-server.com:8000/oauth2/refresh', // OAuth2 刷新 token 地址
|
||||
oauth2_server_userinfo_url: 'http://sa-oauth-server.com:8000/oauth2/userinfo', // OAuth2 获取 userinfo 地址
|
||||
client_id: '1001',
|
||||
client_secret: 'aaaa-bbbb-cccc-dddd-eeee',
|
||||
redirect_uri: location.href.split('?')[0].split('#')[0],
|
||||
scope: 'userinfo,userid,openid,unionid,oidc',
|
||||
username: 'sa',
|
||||
password: '123456'
|
||||
}
|
||||
|
||||
// 打开页面时,加载本地缓存数据,本地缓存无数据时加载默认配置
|
||||
function readLocalCfg() {
|
||||
$('.in-cfg').each(function(){
|
||||
var name = $(this).attr('name');
|
||||
var value = getLocalCfg(name) || defaultCfg[name];
|
||||
$(this).val(value);
|
||||
})
|
||||
}
|
||||
readLocalCfg();
|
||||
|
||||
// 清空配置
|
||||
function clearLocalCfg() {
|
||||
$('.in-cfg').each(function(){
|
||||
$(this).val('');
|
||||
setLocalCfg($(this).attr('name'), '');
|
||||
})
|
||||
}
|
||||
|
||||
// 将所有配置保存到本地缓存
|
||||
function saveAllCfgToLocal() {
|
||||
$('.in-cfg').each(function(){
|
||||
setLocalCfg($(this).attr('name'), $(this).val());
|
||||
})
|
||||
}
|
||||
|
||||
// 根据主机 URL 一键拼接授权页等地址
|
||||
function autoSplicingUrl() {
|
||||
var oauth2_server_url = $('[name=oauth2_server_url]').val();
|
||||
if(!oauth2_server_url) {
|
||||
return layer.alert('请先配置 OAuth2 Server 主机地址!')
|
||||
}
|
||||
$('[name=oauth2_server_auth_url]').val(oauth2_server_url + '/oauth2/authorize');
|
||||
$('[name=oauth2_server_token_url]').val(oauth2_server_url + '/oauth2/token');
|
||||
$('[name=oauth2_server_refresh_token_url]').val(oauth2_server_url + '/oauth2/refresh');
|
||||
$('[name=oauth2_server_userinfo_url]').val(oauth2_server_url + '/oauth2/userinfo');
|
||||
saveAllCfgToLocal();
|
||||
layer.msg('已自动拼接:OAuth2 Server 授权页地址、获取 token 地址、刷新 token 地址、获取 userinfo 地址');
|
||||
}
|
||||
|
||||
// 一键填写 Gitee 参数样例
|
||||
function autoFillGiteeUrl() {
|
||||
$('[name=oauth2_server_url]').val('https://gitee.com')
|
||||
$('[name=oauth2_server_auth_url]').val('https://gitee.com/oauth/authorize');
|
||||
$('[name=oauth2_server_token_url]').val('https://gitee.com/oauth/token');
|
||||
$('[name=oauth2_server_refresh_token_url]').val('https://gitee.com/oauth/token');
|
||||
$('[name=oauth2_server_userinfo_url]').val('https://gitee.com/api/v5/user');
|
||||
$('[name=client_id]').val('<待填写>');
|
||||
$('[name=client_secret]').val('<待填写>');
|
||||
$('[name=redirect_uri]').val(defaultCfg.redirect_uri);
|
||||
$('[name=scope]').val('user_info');
|
||||
saveAllCfgToLocal();
|
||||
layer.msg('填写成功');
|
||||
}
|
||||
|
||||
// 一键填写 Sa-Sso-Max 参数样例,参考:http://sa-pro.dev33.cn/
|
||||
function autoFillSaSsoMaxUrl() {
|
||||
$('[name=oauth2_server_url]').val('http://sspx-server.dev33.cn')
|
||||
$('[name=oauth2_server_auth_url]').val('http://sspx-center.dev33.cn/oauth2/authorize');
|
||||
$('[name=oauth2_server_token_url]').val('http://sspx-server.dev33.cn/oauth2/token');
|
||||
$('[name=oauth2_server_refresh_token_url]').val('http://sspx-server.dev33.cn/oauth2/refresh');
|
||||
$('[name=oauth2_server_userinfo_url]').val('http://sspx-server.dev33.cn/oauth2/userinfo');
|
||||
$('[name=client_id]').val('100001');
|
||||
$('[name=client_secret]').val('CQ0Nf1LmaYq7Ads8EdmKMtEnZmTVIicAEl2trBi0zVKufmOVY5G5Tu2epfu4');
|
||||
$('[name=redirect_uri]').val(defaultCfg.redirect_uri);
|
||||
$('[name=scope]').val('userinfo,openid,unionid');
|
||||
saveAllCfgToLocal();
|
||||
layer.msg('填写成功');
|
||||
}
|
||||
|
||||
</script>
|
||||
<!-- 工具方法 -->
|
||||
<script>
|
||||
// 从url中查询到指定名称的参数值
|
||||
function getParam(name, defaultValue){
|
||||
var query = window.location.search.substring(1);
|
||||
var vars = query.split("&");
|
||||
for (var i=0;i<vars.length;i++) {
|
||||
var pair = vars[i].split("=");
|
||||
if(pair[0] == name){return pair[1];}
|
||||
}
|
||||
return(defaultValue == undefined ? null : defaultValue);
|
||||
}
|
||||
|
||||
// 从url中查询到指定名称的锚参数值
|
||||
function getSharpParam(name, defaultValue){
|
||||
var query = window.location.hash.substring(1);
|
||||
var vars = query.split("&");
|
||||
for (var i=0;i<vars.length;i++) {
|
||||
var pair = vars[i].split("=");
|
||||
if(pair[0] == name){return pair[1];}
|
||||
}
|
||||
return(defaultValue == undefined ? null : defaultValue);
|
||||
}
|
||||
|
||||
var sa = {};
|
||||
|
||||
// 打开loading
|
||||
sa.loading = function(msg) {
|
||||
if(window.layer) {
|
||||
layer.closeAll(); // 开始前先把所有弹窗关了
|
||||
layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load' });
|
||||
}
|
||||
};
|
||||
|
||||
// 隐藏loading
|
||||
sa.hideLoading = function() {
|
||||
if(window.layer) {
|
||||
layer.closeAll();
|
||||
}
|
||||
};
|
||||
|
||||
// 封装一下Ajax
|
||||
sa.ajax = function(url, data, successFn, cfg) {
|
||||
cfg = cfg || {};
|
||||
sa.loading("正在努力加载...");
|
||||
setTimeout(function() {
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: cfg.method || "post",
|
||||
data: data,
|
||||
dataType: 'json',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'satoken': localStorage.getItem('satoken')
|
||||
},
|
||||
success: function(res){
|
||||
console.log('返回数据:', res);
|
||||
sa.hideLoading();
|
||||
successFn(res);
|
||||
},
|
||||
error: function(xhr, type, errorThrown){
|
||||
sa.hideLoading();
|
||||
if(xhr.status == 0){
|
||||
return layer.alert('无法连接到服务器,请检查网络');
|
||||
}
|
||||
return layer.alert("异常:" + JSON.stringify(xhr));
|
||||
}
|
||||
});
|
||||
}, 400);
|
||||
}
|
||||
|
||||
|
||||
// 从url中查询到指定名称的参数值
|
||||
function getParam(name, defaultValue){
|
||||
var query = window.location.search.substring(1);
|
||||
var vars = query.split("&");
|
||||
for (var i=0;i<vars.length;i++) {
|
||||
var pair = vars[i].split("=");
|
||||
if(pair[0] == name){return pair[1];}
|
||||
}
|
||||
return(defaultValue == undefined ? null : defaultValue);
|
||||
}
|
||||
|
||||
// 监听 textarea 内容变化时,高度随之变化
|
||||
$('textarea').keyup(function(){
|
||||
var textarea = this;
|
||||
textarea.style.height = 'auto'; // 去除之前的高度限制
|
||||
textarea.style.height = textarea.scrollHeight + 14 + 'px';
|
||||
})
|
||||
|
||||
// 重设指定 textarea 的高度
|
||||
function resizeTextarea(selected){
|
||||
$(selected).each(function(){
|
||||
var textarea = this;
|
||||
textarea.style.height = 'auto'; // 去除之前的高度限制
|
||||
textarea.style.height = textarea.scrollHeight + 14 + 'px';
|
||||
})
|
||||
}
|
||||
|
||||
// 重设所有 textarea 的高度
|
||||
function resizeAllTextarea(){
|
||||
$('textarea').each(function(){
|
||||
var textarea = this;
|
||||
textarea.style.height = 'auto'; // 去除之前的高度限制
|
||||
textarea.style.height = textarea.scrollHeight + 14 + 'px';
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
// --------------------- 模式一 ---------------------
|
||||
|
||||
// 构建授权码地址
|
||||
function buildAuthorizationCodeUrl() {
|
||||
var url = $('[name=oauth2_server_auth_url]').val()
|
||||
+ '?response_type=code'
|
||||
+ '&client_id=' + $('[name=client_id]').val()
|
||||
+ '&redirect_uri=' + $('[name=redirect_uri]').val()
|
||||
+ '&scope=' + $('[name=scope]').val()
|
||||
$('.auth-code-url').val(url);
|
||||
resizeTextarea('.auth-code-url');
|
||||
}
|
||||
|
||||
// 跳转到授权码授权地址
|
||||
function jumpAuthCodeUrl() {
|
||||
var url = $('.auth-code-url').val();
|
||||
if(!url) {
|
||||
return layer.msg('请先构建地址');
|
||||
}
|
||||
location.href = url;
|
||||
}
|
||||
|
||||
// 默认尝试读取一下 code
|
||||
var code = getParam('code');
|
||||
if(code) {
|
||||
$('.show-url-code').text(code);
|
||||
}
|
||||
|
||||
// 构建 code 换 token 地址
|
||||
function buildCodeTakeTokenUrl() {
|
||||
var code = getParam('code');
|
||||
if(!code) {
|
||||
return layer.msg('未能获取到 code 参数,请先点击上方的授权地址获取 code ');
|
||||
}
|
||||
// var url = $('[name=oauth2_server_url]').val() + '/oauth2/token'
|
||||
var url = $('[name=oauth2_server_token_url]').val()
|
||||
+ '?grant_type=authorization_code'
|
||||
+ '&client_id=' + $('[name=client_id]').val()
|
||||
+ '&client_secret=' + $('[name=client_secret]').val()
|
||||
+ '&redirect_uri=' + $('[name=redirect_uri]').val()
|
||||
+ '&code=' + code
|
||||
$('.code-take-token-url').val(url);
|
||||
resizeTextarea('.code-take-token-url');
|
||||
}
|
||||
|
||||
// code 换 Access-Token
|
||||
function ajaxCodeToAccessToken() {
|
||||
var url = $('.code-take-token-url').val();
|
||||
if(!url) {
|
||||
return layer.msg('请先构建地址');
|
||||
}
|
||||
sa.ajax(url, {}, function(res){
|
||||
if(res.access_token) {
|
||||
$('[name=access-token-input]').val(res.access_token);
|
||||
}
|
||||
if(res.refresh_token) {
|
||||
$('[name=refresh-token-input]').val(res.refresh_token);
|
||||
}
|
||||
var jsonStr = JSON.stringify(res, null, '\t');
|
||||
$('.code-take-token-result').val(jsonStr);
|
||||
resizeTextarea('.code-take-token-result');
|
||||
})
|
||||
}
|
||||
|
||||
// --------- 刷新令牌
|
||||
|
||||
// 构建 Tefresh-Token 刷新 Access-Token 地址
|
||||
function buildRefreshTokenUrl() {
|
||||
var refresh_token = $('[name=refresh-token-input]').val();
|
||||
if(!refresh_token) {
|
||||
return layer.msg('未能获取到 refresh_token 参数,请先点击上方的授权地址获取 refresh_token ');
|
||||
}
|
||||
// var url = $('[name=oauth2_server_url]').val() + '/oauth2/refresh'
|
||||
var url = $('[name=oauth2_server_refresh_token_url]').val()
|
||||
+ '?grant_type=refresh_token'
|
||||
+ '&client_id=' + $('[name=client_id]').val()
|
||||
+ '&client_secret=' + $('[name=client_secret]').val()
|
||||
+ '&refresh_token=' + refresh_token
|
||||
$('.refresh-token-url').val(url);
|
||||
resizeTextarea('.refresh-token-url');
|
||||
}
|
||||
|
||||
// 使用 Tefresh-Token 刷新 Access-Token
|
||||
function ajaxRefreshToken() {
|
||||
var url = $('.refresh-token-url').val();
|
||||
if(!url) {
|
||||
return layer.msg('请先构建地址');
|
||||
}
|
||||
sa.ajax(url, {}, function(res){
|
||||
if(res.access_token) {
|
||||
$('[name=access-token-input]').val(res.access_token);
|
||||
}
|
||||
if(res.refresh_token) {
|
||||
$('[name=refresh-token-input]').val(res.refresh_token);
|
||||
}
|
||||
var jsonStr = JSON.stringify(res, null, '\t');
|
||||
$('.refresh-token-result').val(jsonStr);
|
||||
resizeTextarea('.refresh-token-result');
|
||||
})
|
||||
}
|
||||
|
||||
// --------- 用户资料
|
||||
|
||||
// 构建 Access-Token 获取 Userinfo 地址
|
||||
function buildUserinfoUrl() {
|
||||
var access_token = $('[name=access-token-input]').val();
|
||||
if(!access_token) {
|
||||
return layer.msg('未能获取到 access_token 参数,请先点击上方的授权地址获取 access_token ');
|
||||
}
|
||||
// var url = $('[name=oauth2_server_url]').val() + '/oauth2/userinfo'
|
||||
var url = $('[name=oauth2_server_userinfo_url]').val()
|
||||
+ '?access_token=' + access_token
|
||||
$('.userinfo-url').val(url);
|
||||
resizeTextarea('.userinfo-url');
|
||||
}
|
||||
|
||||
// 使用 Access-Token 获取 Userinfo
|
||||
function ajaxUserinfoUrl() {
|
||||
var url = $('.userinfo-url').val();
|
||||
if(!url) {
|
||||
return layer.msg('请先构建地址');
|
||||
}
|
||||
var method = $('[name=userinfo-ajax-method]').val();
|
||||
sa.ajax(url, {}, function(res){
|
||||
var jsonStr = JSON.stringify(res, null, '\t');
|
||||
$('.userinfo-result').val(jsonStr);
|
||||
resizeTextarea('.userinfo-result');
|
||||
}, {method: method})
|
||||
}
|
||||
|
||||
// --------- 回收令牌
|
||||
|
||||
// 构建回收 Access-Token 地址
|
||||
function buildRevokeTokenUrl() {
|
||||
var access_token = $('[name=access-token-input]').val();
|
||||
if(!access_token) {
|
||||
return layer.msg('未能获取到 access_token 参数,请先点击上方的授权地址获取 access_token ');
|
||||
}
|
||||
var url = $('[name=oauth2_server_url]').val() + '/oauth2/revoke'
|
||||
+ '?client_id=' + $('[name=client_id]').val()
|
||||
+ '&client_secret=' + $('[name=client_secret]').val()
|
||||
+ '&access_token=' + access_token
|
||||
$('.revoke-token-url').val(url);
|
||||
resizeTextarea('.revoke-token-url');
|
||||
}
|
||||
|
||||
// 回收 Access-Token
|
||||
function ajaxRevokeTokenUrl() {
|
||||
var url = $('.revoke-token-url').val();
|
||||
if(!url) {
|
||||
return layer.msg('请先构建地址');
|
||||
}
|
||||
sa.ajax(url, {}, function(res){
|
||||
var jsonStr = JSON.stringify(res, null, '\t');
|
||||
$('.revoke-token-result').val(jsonStr);
|
||||
resizeTextarea('.revoke-token-result');
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// --------------------- 模式二 ---------------------
|
||||
|
||||
// 构建隐藏式 Implicit 地址
|
||||
function buildImplicitUrl() {
|
||||
var url = $('[name=oauth2_server_auth_url]').val()
|
||||
+ '?response_type=token'
|
||||
+ '&client_id=' + $('[name=client_id]').val()
|
||||
+ '&redirect_uri=' + $('[name=redirect_uri]').val()
|
||||
+ '&scope=' + $('[name=scope]').val()
|
||||
$('.implicit-url').val(url);
|
||||
resizeTextarea('.implicit-url');
|
||||
}
|
||||
|
||||
// 跳转到 Implicit 授权地址
|
||||
function jumpImplicitUrl() {
|
||||
var url = $('.implicit-url').val();
|
||||
if(!url) {
|
||||
return layer.msg('请先构建地址');
|
||||
}
|
||||
location.href = url;
|
||||
}
|
||||
|
||||
// 默认尝试读取一下 Access-Token
|
||||
var accessToken = getSharpParam('token');
|
||||
if(accessToken) {
|
||||
$('.show-url-access-token').text(accessToken);
|
||||
$('[name=access-token-input]').val(accessToken);
|
||||
}
|
||||
|
||||
|
||||
// --------------------- 模式三 ---------------------
|
||||
|
||||
// 构建密码 Password 授权地址
|
||||
function buildPasswordUrl() {
|
||||
var url = $('[name=oauth2_server_url]').val() + '/oauth2/token'
|
||||
+ '?grant_type=password'
|
||||
+ '&client_id=' + $('[name=client_id]').val()
|
||||
+ '&client_secret=' + $('[name=client_secret]').val()
|
||||
+ '&username=' + $('[name=username]').val()
|
||||
+ '&password=' + $('[name=password]').val()
|
||||
+ '&scope=' + $('[name=scope]').val()
|
||||
$('.password-url').val(url);
|
||||
resizeTextarea('.password-url');
|
||||
}
|
||||
|
||||
// 请求密码式 Password 授权地址
|
||||
function ajaxPasswordUrl() {
|
||||
var url = $('.password-url').val();
|
||||
if(!url) {
|
||||
return layer.msg('请先构建地址');
|
||||
}
|
||||
sa.ajax(url, {}, function(res){
|
||||
if(res.access_token) {
|
||||
$('[name=access-token-input]').val(res.access_token);
|
||||
}
|
||||
if(res.refresh_token) {
|
||||
$('[name=refresh-token-input]').val(res.refresh_token);
|
||||
}
|
||||
var jsonStr = JSON.stringify(res, null, '\t');
|
||||
$('.password-result').val(jsonStr);
|
||||
resizeTextarea('.password-result');
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// --------------------- 模式四 ---------------------
|
||||
|
||||
// 构建密码 Client-Token 授权地址
|
||||
function buildClientTokenUrl() {
|
||||
var url = $('[name=oauth2_server_url]').val() + '/oauth2/client_token'
|
||||
+ '?grant_type=client_credentials'
|
||||
+ '&client_id=' + $('[name=client_id]').val()
|
||||
+ '&client_secret=' + $('[name=client_secret]').val()
|
||||
+ '&scope=' + $('[name=scope]').val()
|
||||
$('.client-token-url').val(url);
|
||||
resizeTextarea('.client-token-url');
|
||||
}
|
||||
|
||||
// 请求 Client-Token 授权地址
|
||||
function ajaxClientTokenUrl() {
|
||||
var url = $('.client-token-url').val();
|
||||
if(!url) {
|
||||
return layer.msg('请先构建地址');
|
||||
}
|
||||
sa.ajax(url, {}, function(res){
|
||||
var jsonStr = JSON.stringify(res, null, '\t');
|
||||
$('.client-token-result').val(jsonStr);
|
||||
resizeTextarea('.client-token-result');
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -17,7 +17,7 @@
|
||||
<java.version>1.8</java.version>
|
||||
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
*{margin: 0; padding: 0;}
|
||||
body{font-family: Helvetica Neue,Helvetica,PingFang SC,Tahoma,Arial,sans-serif;}
|
||||
::-webkit-input-placeholder{color: #ccc;}
|
||||
|
||||
/* 视图盒子 */
|
||||
.view-box{position: relative; width: 100vw; height: 100vh; overflow: hidden;}
|
||||
/* 背景 EAEFF3 */
|
||||
.bg-1{height: 100%; background: #E4B17F;}
|
||||
|
||||
/* 内容盒子 */
|
||||
.content-box{position: absolute; width: 100vw; height: 100vh; top: 0px;}
|
||||
.content-box{display: none;}
|
||||
.region-default{display: block;}
|
||||
.message-box{width: 100%;; text-align: center;}
|
||||
|
||||
/* 登录盒子 */
|
||||
/* .login-box{width: 400px; height: 400px; position: absolute; left: calc(50% - 200px); top: calc(50% - 200px); max-width: 90%; } */
|
||||
.login-box{width: 450px; margin: auto; max-width: 90%; height: 100%;}
|
||||
.login-box{display: flex; align-items: center; text-align: center;}
|
||||
|
||||
/* 表单 */
|
||||
.from-box{flex: 1; padding: 20px 50px; background-color: #FFF;}
|
||||
.from-box{border-radius: 1px; box-shadow: 1px 1px 20px #666;}
|
||||
.from-title{margin-top: 20px; margin-bottom: 30px; text-align: center;}
|
||||
|
||||
/* 输入框 */
|
||||
.from-item{border: 0px #000 solid; margin-bottom: 15px;}
|
||||
.s-input{width: 100%; line-height: 32px; height: 32px; text-indent: 1em; outline: 0; border: 1px #ccc solid; border-radius: 3px; transition: all 0.2s;}
|
||||
.s-input{font-size: 12px;}
|
||||
.s-input:focus{border-color: #409eff}
|
||||
|
||||
/* 登录按钮 */
|
||||
.s-btn{ text-indent: 0; cursor: pointer; background-color: #409EFF; border-color: #409EFF; color: #FFF;}
|
||||
.s-btn:hover{background-color: #50aEFF;}
|
||||
|
||||
/* 重置按钮 */
|
||||
.reset-box{text-align: left; font-size: 12px;}
|
||||
.reset-box a{text-decoration: none;}
|
||||
.reset-box a:hover{text-decoration: underline;}
|
||||
|
||||
/* 确认授权按钮 */
|
||||
.confirm-btn{text-indent: 0; cursor: pointer; background-color: #409EFF; border: 1px #409EFF solid; color: #FFF; padding: 5px 15px;}
|
||||
.confirm-btn:hover{background-color: #50aEFF;}
|
||||
|
||||
/* loading框样式 */
|
||||
.ajax-layer-load.layui-layer-dialog{min-width: 0px !important; background-color: rgba(0,0,0,0.85);}
|
||||
.ajax-layer-load.layui-layer-dialog .layui-layer-content{padding: 10px 20px 10px 40px; color: #FFF;}
|
||||
.ajax-layer-load.layui-layer-dialog .layui-layer-content .layui-layer-ico{width: 20px; height: 20px; background-size: 20px 20px; top: 12px; }
|
||||
@@ -0,0 +1,194 @@
|
||||
// OAuth-Server 后端 接口地址
|
||||
var baseUrl = "http://sa-oauth-server.com:8000";
|
||||
|
||||
|
||||
// ----------------------------------- 相关事件 -----------------------------------
|
||||
|
||||
// 显示默认区域
|
||||
function showDefaultRegion(){
|
||||
$('.content-box').hide();
|
||||
$('.region-default').show();
|
||||
}
|
||||
// 显示登录框区域
|
||||
function showLoginRegion(){
|
||||
$('.content-box').hide();
|
||||
$('.region-login').show();
|
||||
}
|
||||
// 显示确认授权框区域
|
||||
function showConfirmRegion(){
|
||||
$('.content-box').hide();
|
||||
$('.region-confirm').show();
|
||||
$('.show-clientId').text(getParam('client_id'));
|
||||
$('.show-scope').text(getParam('scope'));
|
||||
}
|
||||
|
||||
// 检查当前是否已经登录,如果已登录则直接开始跳转,如果未登录则等待用户输入账号密码
|
||||
function tryJump(){
|
||||
var data = location.search.substr(1);
|
||||
sa.ajax("/oauth2/getRedirectUri", data, function(res) {
|
||||
// 情况1:客户端未登录,返回 code=401,提示用户登录
|
||||
if(res.code === 401) {
|
||||
showLoginRegion();
|
||||
return;
|
||||
}
|
||||
|
||||
// 情况2:请求的 scope 需要客户端手动确认授权,返回 code=411,提示用户手动确认
|
||||
if(res.code === 411) {
|
||||
showConfirmRegion();
|
||||
return;
|
||||
}
|
||||
|
||||
// 情况3:已登录且请求的 scope 已确认授权,返回 code=200,data=最终重定向 url 地址(携带code码参数)
|
||||
if(res.code == 200) {
|
||||
console.log('跳转:', res.redirect_uri);
|
||||
location.href = res.redirect_uri;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('未知状态码,', res.code, res);
|
||||
layer.alert('错误:' + JSON.stringify(res))
|
||||
})
|
||||
}
|
||||
|
||||
// 登录事件
|
||||
function doLogin() {
|
||||
// 开始登录
|
||||
var data = {
|
||||
name: $('[name=name]').val(),
|
||||
pwd: $('[name=pwd]').val()
|
||||
};
|
||||
sa.ajax("/oauth2/doLogin", data, function(res) {
|
||||
if(res.code == 200) {
|
||||
localStorage.setItem('satoken', res.satoken);
|
||||
layer.msg('登录成功', {anim: 0, icon: 6 });
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 800);
|
||||
} else {
|
||||
layer.msg(res.msg, {anim: 6, icon: 2 });
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 确认授权事件
|
||||
function yes() {
|
||||
var data = location.search.substr(1) + '&build_redirect_uri=true';
|
||||
sa.ajax("/oauth2/doConfirm", data, function(res) {
|
||||
if(res.code == 200) {
|
||||
layer.msg('确认授权成功,即将跳转...', {anim: 0, icon: 6 });
|
||||
setTimeout(function() {
|
||||
console.log('跳转:', res.redirect_uri);
|
||||
location.href = res.redirect_uri;
|
||||
}, 800);
|
||||
} else {
|
||||
layer.msg(res.msg, {anim: 6, icon: 2 });
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 拒绝授权事件
|
||||
function no() {
|
||||
var url = joinParam(getParam('redirect_uri'), "handle=refuse&msg=用户拒绝了授权");
|
||||
location.href = url;
|
||||
}
|
||||
|
||||
// 页面加载完毕后触发
|
||||
window.onload = function() {
|
||||
tryJump();
|
||||
|
||||
// 绑定回车事件
|
||||
$('[name=name],[name=pwd]').bind('keypress', function(event){
|
||||
if(event.keyCode == "13") {
|
||||
$('.login-btn').click();
|
||||
}
|
||||
});
|
||||
|
||||
// 输入框获取焦点
|
||||
$("[name=name]").focus();
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ----------------------------------- 工具函数封装 -----------------------------------
|
||||
|
||||
// sa
|
||||
var sa = {};
|
||||
|
||||
// 打开loading
|
||||
sa.loading = function(msg) {
|
||||
layer.closeAll(); // 开始前先把所有弹窗关了
|
||||
return layer.msg(msg, {icon: 16, shade: 0.3, time: 1000 * 20, skin: 'ajax-layer-load'});
|
||||
};
|
||||
|
||||
// 隐藏loading
|
||||
sa.hideLoading = function() {
|
||||
layer.closeAll();
|
||||
};
|
||||
|
||||
// 封装一下Ajax
|
||||
sa.ajax = function(url, data, successFn) {
|
||||
sa.loading("加载中...");
|
||||
$.ajax({
|
||||
url: baseUrl + url,
|
||||
type: "post",
|
||||
data: data,
|
||||
dataType: 'json',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'satoken': localStorage.getItem('satoken')
|
||||
},
|
||||
success: function(res){
|
||||
sa.hideLoading();
|
||||
console.log('返回数据:', res);
|
||||
successFn(res);
|
||||
},
|
||||
error: function(xhr, type, errorThrown){
|
||||
sa.hideLoading();
|
||||
if(xhr.status == 0){
|
||||
return alert('无法连接到服务器,请检查网络');
|
||||
}
|
||||
return alert("异常:" + JSON.stringify(xhr));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 从url中查询到指定名称的参数值
|
||||
function getParam(name, defaultValue){
|
||||
var query = window.location.search.substring(1);
|
||||
var vars = query.split("&");
|
||||
for (var i=0;i<vars.length;i++) {
|
||||
var pair = vars[i].split("=");
|
||||
if(pair[0] == name){return pair[1] + (pair[2] ? '=' + pair[2] : '');}
|
||||
}
|
||||
return(defaultValue == undefined ? null : defaultValue);
|
||||
}
|
||||
|
||||
// 在url上拼接上kv参数并返回
|
||||
function joinParam(url, parameStr) {
|
||||
if(parameStr == null || parameStr.length == 0) {
|
||||
return url;
|
||||
}
|
||||
var index = url.indexOf('?');
|
||||
// ? 不存在
|
||||
if(index == -1) {
|
||||
return url + '?' + parameStr;
|
||||
}
|
||||
// ? 是最后一位
|
||||
if(index == url.length - 1) {
|
||||
return url + parameStr;
|
||||
}
|
||||
// ? 是其中一位
|
||||
if(index > -1 && index < url.length - 1) {
|
||||
// 如果最后一位是 不是&, 且 parameStr 第一位不是 &, 就增送一个 &
|
||||
if(url.lastIndexOf('&') != url.length - 1 && parameStrindexOf('&') != 0) {
|
||||
return url + '&' + parameStr;
|
||||
} else {
|
||||
return url + parameStr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 打印信息
|
||||
var str = "This page is provided by Sa-Token, Please refer to: " + "https://sa-token.cc/";
|
||||
console.log(str);
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<title>Sa-OAuth2-Server 认证中心(前后端分离版)</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<link rel="stylesheet" href="./login.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="view-box">
|
||||
<div class="bg-1"></div>
|
||||
|
||||
<!--
|
||||
将页面分为三块区域:
|
||||
- 未登录时显示区域2:登录框。
|
||||
- 已登录但请求的 scope 尚未手动确认授权,显示区域3:确认授权框。
|
||||
- 默认显示区域1:提示文字。
|
||||
-->
|
||||
|
||||
<!-- 区域1:默认显示 -->
|
||||
<div class="content-box region-default">
|
||||
<div class="login-box">
|
||||
<div class="message-box">
|
||||
加载中...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 区域2:登录框 -->
|
||||
<div class="content-box region-login">
|
||||
<div class="login-box">
|
||||
<div class="from-box">
|
||||
<h2 class="from-title">Sa-OAuth2-Server 认证中心(前后端分离版)</h2>
|
||||
<div class="from-item">
|
||||
<input class="s-input" name="name" placeholder="请输入账号" />
|
||||
</div>
|
||||
<div class="from-item">
|
||||
<input class="s-input" name="pwd" type="password" placeholder="请输入密码" />
|
||||
</div>
|
||||
<div class="from-item">
|
||||
<button class="s-input s-btn login-btn" onclick="doLogin()">登录</button>
|
||||
</div>
|
||||
<div class="from-item reset-box">
|
||||
<a href="javascript: location.reload();" >刷新</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 区域3:确认授权框 -->
|
||||
<div class="content-box region-confirm">
|
||||
<div class="login-box">
|
||||
<div class="from-box">
|
||||
<h2 class="from-title">Sa-OAuth2-Server 认证中心(前后端分离版)</h2><br>
|
||||
<div>
|
||||
<div><b>应用ID:</b><span class="show-clientId"></span></div>
|
||||
<div><b>请求授权:</b><span class="show-scope"></span></div>
|
||||
<br><br><div>------------- 是否同意授权 -------------</div><br>
|
||||
<div>
|
||||
<button class="confirm-btn" onclick="yes()">同意</button>
|
||||
<button class="confirm-btn" onclick="no()">拒绝</button>
|
||||
</div>
|
||||
<div style="height: 10px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部 版权 -->
|
||||
<div style="position: absolute; bottom: 40px; width: 100%; text-align: center; color: #666;">
|
||||
This page is provided by Sa-Token-OAuth2
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- scripts -->
|
||||
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
|
||||
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
|
||||
<script src="./login.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -17,7 +17,7 @@
|
||||
<java.version>1.8</java.version>
|
||||
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package com.pj.mock;
|
||||
|
||||
import cn.dev33.satoken.oauth2.consts.GrantType;
|
||||
import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* SaClientModel 模拟查询操作
|
||||
*
|
||||
* @author click33
|
||||
* @since 2024/11/15
|
||||
*/
|
||||
@Component
|
||||
public class SaClientMockDao {
|
||||
|
||||
public List<SaClientModel> list = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 构造方法,添加三个模拟应用
|
||||
*/
|
||||
public SaClientMockDao(){
|
||||
// 模拟应用1
|
||||
SaClientModel client1 = new SaClientModel()
|
||||
.setClientId("1001") // client id
|
||||
.setClientSecret("aaaa-bbbb-cccc-dddd-eeee") // client 秘钥
|
||||
.addAllowRedirectUris("*") // 所有允许授权的 url
|
||||
.addContractScopes("openid", "unionid", "userid", "userinfo", "oidc") // 所有签约的权限
|
||||
.setSubjectId("1000001") // 主体 id (可选)
|
||||
.addAllowGrantTypes( // 所有允许的授权模式
|
||||
GrantType.authorization_code, // 授权码式
|
||||
GrantType.implicit, // 隐藏式
|
||||
GrantType.refresh_token, // 刷新令牌
|
||||
GrantType.password, // 密码式
|
||||
GrantType.client_credentials, // 客户端模式
|
||||
"phone_code" // 自定义授权模式 手机号验证码登录
|
||||
);
|
||||
list.add(client1);
|
||||
|
||||
// 模拟应用2
|
||||
SaClientModel client2 = new SaClientModel()
|
||||
.setClientId("1002")
|
||||
.setClientSecret("aaaa-bbbb-cccc-dddd-eeee")
|
||||
.addAllowRedirectUris("*")
|
||||
.addContractScopes("openid", "unionid", "userid", "userinfo", "oidc")
|
||||
.setSubjectId("1000001") // 主体 id (可选)
|
||||
.addAllowGrantTypes(
|
||||
GrantType.authorization_code,
|
||||
GrantType.implicit,
|
||||
GrantType.refresh_token,
|
||||
GrantType.password,
|
||||
GrantType.client_credentials
|
||||
);
|
||||
list.add(client2);
|
||||
|
||||
// 模拟应用3
|
||||
SaClientModel client3 = new SaClientModel()
|
||||
.setClientId("1003")
|
||||
.setClientSecret("aaaa-bbbb-cccc-dddd-eeee")
|
||||
.addAllowRedirectUris("*")
|
||||
.addContractScopes("openid", "unionid", "userid", "userinfo", "oidc")
|
||||
.addAllowGrantTypes(
|
||||
GrantType.authorization_code,
|
||||
GrantType.implicit,
|
||||
GrantType.refresh_token,
|
||||
GrantType.password,
|
||||
GrantType.client_credentials
|
||||
);
|
||||
list.add(client3);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据应用 id 查找对应的应用,找不到则返回 null
|
||||
* @param clientId 应用 id
|
||||
* @return 应用对象
|
||||
*/
|
||||
public SaClientModel getClientModel(String clientId) {
|
||||
return list.stream()
|
||||
.filter(e -> e.getClientId().equals(clientId))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
}
|
||||
+9
-21
@@ -1,8 +1,9 @@
|
||||
package com.pj.oauth2;
|
||||
|
||||
import cn.dev33.satoken.oauth2.consts.GrantType;
|
||||
import cn.dev33.satoken.oauth2.data.loader.SaOAuth2DataLoader;
|
||||
import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel;
|
||||
import com.pj.mock.SaClientMockDao;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
@@ -12,34 +13,21 @@ import org.springframework.stereotype.Component;
|
||||
*/
|
||||
@Component
|
||||
public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader {
|
||||
|
||||
|
||||
@Autowired
|
||||
SaClientMockDao saClientMockDao;
|
||||
|
||||
// 根据 clientId 获取 Client 信息
|
||||
@Override
|
||||
public SaClientModel getClientModel(String clientId) {
|
||||
// 此为模拟数据,真实环境需要从数据库查询
|
||||
if("1001".equals(clientId)) {
|
||||
return new SaClientModel()
|
||||
.setClientId("1001") // client id
|
||||
.setClientSecret("aaaa-bbbb-cccc-dddd-eeee") // client 秘钥
|
||||
.addAllowRedirectUris("*") // 所有允许授权的 url
|
||||
.addContractScopes("openid", "userid", "userinfo", "oidc") // 所有签约的权限
|
||||
.addAllowGrantTypes( // 所有允许的授权模式
|
||||
GrantType.authorization_code, // 授权码式
|
||||
GrantType.implicit, // 隐式式
|
||||
GrantType.refresh_token, // 刷新令牌
|
||||
GrantType.password, // 密码式
|
||||
GrantType.client_credentials, // 客户端模式
|
||||
"phone_code" // 自定义授权模式 手机号验证码登录
|
||||
)
|
||||
;
|
||||
}
|
||||
return null;
|
||||
// 此为模拟数据,真实环境需要从数据库查询
|
||||
return saClientMockDao.getClientModel(clientId);
|
||||
}
|
||||
|
||||
// 根据 clientId 和 loginId 获取 openid
|
||||
@Override
|
||||
public String getOpenid(String clientId, Object loginId) {
|
||||
// 此处使用框架默认算法生成 openid,真实环境建议改为从数据库查询
|
||||
// 此处使用框架默认算法生成 openid,真实项目建议改为从数据库查询
|
||||
return SaOAuth2DataLoader.super.getOpenid(clientId, loginId);
|
||||
}
|
||||
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.pj.oauth2;
|
||||
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.oauth2.SaOAuth2Manager;
|
||||
import cn.dev33.satoken.oauth2.template.SaOAuth2Util;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Sa-Token OAuth2 Resources 端 Controller
|
||||
*
|
||||
* <p> Resources 端:OAuth2 资源端,允许 Client 端根据 Access-Token 置换相关资源 </p>
|
||||
*
|
||||
* <p> 在 OAuth2 中,认证端和资源端:
|
||||
* 1、可以在一个 Controller 中,也可以在不同的 Controller 中
|
||||
* 2、可以在同一个项目中,也可以在不同的项目中(在不同项目中时需要两端连同一个 Redis )
|
||||
* </p>
|
||||
*
|
||||
* @author click33
|
||||
* @since 2024/12/6
|
||||
*/
|
||||
@RestController
|
||||
public class SaOAuth2ResourcesController {
|
||||
|
||||
// 示例:获取 userinfo 信息:昵称、头像、性别等等
|
||||
@RequestMapping("/oauth2/userinfo")
|
||||
public SaResult userinfo() {
|
||||
// 获取 Access-Token 对应的账号id
|
||||
String accessToken = SaOAuth2Manager.getDataResolver().readAccessToken(SaHolder.getRequest());
|
||||
Object loginId = SaOAuth2Util.getLoginIdByAccessToken(accessToken);
|
||||
System.out.println("-------- 此Access-Token对应的账号id: " + loginId);
|
||||
|
||||
// 校验 Access-Token 是否具有权限: userinfo
|
||||
SaOAuth2Util.checkAccessTokenScope(accessToken, "userinfo");
|
||||
|
||||
// 模拟账号信息 (真实环境需要查询数据库获取信息)
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
// map.put("userId", loginId); 一般原则下,oauth2-server 不能把 userId 返回给 oauth2-client
|
||||
map.put("nickname", "林小林");
|
||||
map.put("avatar", "http://xxx.com/1.jpg");
|
||||
map.put("age", "18");
|
||||
map.put("sex", "男");
|
||||
map.put("address", "山东省 青岛市 城阳区");
|
||||
return SaResult.ok().setMap(map);
|
||||
}
|
||||
|
||||
}
|
||||
+2
-30
@@ -1,10 +1,8 @@
|
||||
package com.pj.oauth2;
|
||||
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.oauth2.SaOAuth2Manager;
|
||||
import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig;
|
||||
import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor;
|
||||
import cn.dev33.satoken.oauth2.template.SaOAuth2Util;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -13,11 +11,10 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Sa-Token-OAuth2 Server端 Controller
|
||||
* Sa-Token-OAuth2 Server 认证端 Controller
|
||||
*
|
||||
* @author click33
|
||||
*/
|
||||
@@ -43,7 +40,7 @@ public class SaOAuth2ServerController {
|
||||
oauth2Server.doLoginHandle = (name, pwd) -> {
|
||||
if("sa".equals(name) && "123456".equals(pwd)) {
|
||||
StpUtil.login(10001);
|
||||
return SaResult.ok();
|
||||
return SaResult.ok().set("satoken", StpUtil.getTokenValue());
|
||||
}
|
||||
return SaResult.error("账号名或密码错误");
|
||||
};
|
||||
@@ -58,29 +55,4 @@ public class SaOAuth2ServerController {
|
||||
|
||||
}
|
||||
|
||||
|
||||
// ---------- 开放相关资源接口: Client端根据 Access-Token ,置换相关资源 ------------
|
||||
|
||||
// 获取 userinfo 信息:昵称、头像、性别等等
|
||||
@RequestMapping("/oauth2/userinfo")
|
||||
public SaResult userinfo() {
|
||||
// 获取 Access-Token 对应的账号id
|
||||
String accessToken = SaOAuth2Manager.getDataResolver().readAccessToken(SaHolder.getRequest());
|
||||
Object loginId = SaOAuth2Util.getLoginIdByAccessToken(accessToken);
|
||||
System.out.println("-------- 此Access-Token对应的账号id: " + loginId);
|
||||
|
||||
// 校验 Access-Token 是否具有权限: userinfo
|
||||
SaOAuth2Util.checkAccessTokenScope(accessToken, "userinfo");
|
||||
|
||||
// 模拟账号信息 (真实环境需要查询数据库获取信息)
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
// map.put("userId", loginId); 一般原则下,oauth2-server 不能把 userId 返回给 oauth2-client
|
||||
map.put("nickname", "林小林");
|
||||
map.put("avatar", "http://xxx.com/1.jpg");
|
||||
map.put("age", "18");
|
||||
map.put("sex", "男");
|
||||
map.put("address", "山东省 青岛市 城阳区");
|
||||
return SaResult.ok().setMap(map);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@
|
||||
// ra.scopes = scopes;
|
||||
//
|
||||
// // 5、生成 Access-Token
|
||||
// AccessTokenModel at = SaOAuth2Manager.getDataGenerate().generateAccessToken(ra, true);
|
||||
// AccessTokenModel at = SaOAuth2Manager.getDataGenerate().generateAccessToken(ra, true, atm -> atm.grantType = "phone_code");
|
||||
// return at;
|
||||
// }
|
||||
//}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package com.pj.oauth2.h5;
|
||||
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.context.model.SaRequest;
|
||||
import cn.dev33.satoken.oauth2.SaOAuth2Manager;
|
||||
import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig;
|
||||
import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts;
|
||||
import cn.dev33.satoken.oauth2.data.generate.SaOAuth2DataGenerate;
|
||||
import cn.dev33.satoken.oauth2.data.model.AccessTokenModel;
|
||||
import cn.dev33.satoken.oauth2.data.model.CodeModel;
|
||||
import cn.dev33.satoken.oauth2.data.model.request.RequestAuthModel;
|
||||
import cn.dev33.satoken.oauth2.error.SaOAuth2ErrorCode;
|
||||
import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception;
|
||||
import cn.dev33.satoken.oauth2.processor.SaOAuth2ServerProcessor;
|
||||
import cn.dev33.satoken.oauth2.template.SaOAuth2Template;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Sa-Token OAuth2 Server端 控制器 (前后端分离情形下所需要的接口)
|
||||
*/
|
||||
@RestController
|
||||
public class SaOAuth2ServerH5Controller {
|
||||
|
||||
/**
|
||||
* 获取最终授权重定向地址,形如:http://xxx.com/xxx?code=xxxxx
|
||||
*
|
||||
* <p> 情况1:客户端未登录,返回 code=401,提示用户登录 <p/>
|
||||
* <p> 情况2:请求的 scope 需要客户端手动确认授权,返回 code=411,提示用户手动确认 <p/>
|
||||
* <p> 情况3:已登录且请求的 scope 已确认授权,返回 code=200,redirect_uri=最终重定向 url 地址(携带code码参数) <p/>
|
||||
*
|
||||
* @return /
|
||||
*/
|
||||
@PostMapping("/oauth2/getRedirectUri")
|
||||
public Object getRedirectUri() {
|
||||
|
||||
// 获取变量
|
||||
SaRequest req = SaHolder.getRequest();
|
||||
SaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig();
|
||||
SaOAuth2DataGenerate dataGenerate = SaOAuth2Manager.getDataGenerate();
|
||||
SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate();
|
||||
String responseType = req.getParamNotNull(SaOAuth2Consts.Param.response_type);
|
||||
|
||||
// 1、先判断是否开启了指定的授权模式
|
||||
SaOAuth2ServerProcessor.instance.checkAuthorizeResponseType(responseType, req, cfg);
|
||||
|
||||
// 2、如果尚未登录, 则先去登录
|
||||
long loginId = SaOAuth2Manager.getStpLogic().getLoginId(0L);
|
||||
if(loginId == 0L) {
|
||||
return SaResult.get(401, "need login", null);
|
||||
}
|
||||
|
||||
// 3、构建请求 Model
|
||||
RequestAuthModel ra = SaOAuth2Manager.getDataResolver().readRequestAuthModel(req, loginId);
|
||||
|
||||
// 4、校验:重定向域名是否合法
|
||||
oauth2Template.checkRedirectUri(ra.clientId, ra.redirectUri);
|
||||
|
||||
// 5、校验:此次申请的Scope,该Client是否已经签约
|
||||
oauth2Template.checkContractScope(ra.clientId, ra.scopes);
|
||||
|
||||
// 6、判断:如果此次申请的Scope,该用户尚未授权,则转到授权页面
|
||||
boolean isNeedCarefulConfirm = oauth2Template.isNeedCarefulConfirm(ra.loginId, ra.clientId, ra.scopes);
|
||||
if(isNeedCarefulConfirm) {
|
||||
// code=411,需要用户手动确认授权
|
||||
return SaResult.get(411, "need confirm", null);
|
||||
}
|
||||
|
||||
// 7、判断授权类型,重定向到不同地址
|
||||
// 如果是 授权码式,则:开始重定向授权,下放code
|
||||
if(SaOAuth2Consts.ResponseType.code.equals(ra.responseType)) {
|
||||
CodeModel codeModel = dataGenerate.generateCode(ra);
|
||||
String redirectUri = dataGenerate.buildRedirectUri(ra.redirectUri, codeModel.code, ra.state);
|
||||
return SaResult.ok().set("redirect_uri", redirectUri);
|
||||
}
|
||||
|
||||
// 如果是 隐藏式,则:开始重定向授权,下放 token
|
||||
if(SaOAuth2Consts.ResponseType.token.equals(ra.responseType)) {
|
||||
AccessTokenModel at = dataGenerate.generateAccessToken(ra, false, null);
|
||||
String redirectUri = dataGenerate.buildImplicitRedirectUri(ra.redirectUri, at.accessToken, ra.state);
|
||||
return SaResult.ok().set("redirect_uri", redirectUri);
|
||||
}
|
||||
|
||||
// 默认返回
|
||||
throw new SaOAuth2Exception("无效 response_type: " + ra.responseType).setCode(SaOAuth2ErrorCode.CODE_30125);
|
||||
}
|
||||
|
||||
}
|
||||
+52
-1
@@ -1,6 +1,14 @@
|
||||
package com.pj.satoken;
|
||||
|
||||
import cn.dev33.satoken.SaManager;
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.filter.SaServletFilter;
|
||||
import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||
import cn.dev33.satoken.router.SaHttpMethod;
|
||||
import cn.dev33.satoken.router.SaRouter;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
@@ -21,7 +29,50 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 注册 Sa-Token 拦截器打开注解鉴权功能
|
||||
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册 [Sa-Token 全局过滤器]
|
||||
*/
|
||||
@Bean
|
||||
public SaServletFilter getSaServletFilter() {
|
||||
return new SaServletFilter()
|
||||
|
||||
// 指定 [拦截路由] 与 [放行路由]
|
||||
.addInclude("/**").addExclude("/favicon.ico")
|
||||
|
||||
// 认证函数: 每次请求执行
|
||||
.setAuth(obj -> {
|
||||
SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());
|
||||
// ...
|
||||
})
|
||||
|
||||
// 异常处理函数:每次认证函数发生异常时执行此函数
|
||||
.setError(e -> {
|
||||
return SaResult.error(e.getMessage());
|
||||
})
|
||||
|
||||
// 前置函数:在每次认证函数之前执行
|
||||
.setBeforeAuth(obj -> {
|
||||
SaHolder.getResponse()
|
||||
|
||||
// ---------- 设置跨域响应头 ----------
|
||||
// 允许指定域访问跨域资源
|
||||
.setHeader("Access-Control-Allow-Origin", "*")
|
||||
// 允许所有请求方式
|
||||
.setHeader("Access-Control-Allow-Methods", "*")
|
||||
// 允许的header参数
|
||||
.setHeader("Access-Control-Allow-Headers", "*")
|
||||
// 有效时间
|
||||
.setHeader("Access-Control-Max-Age", "3600")
|
||||
;
|
||||
|
||||
// 如果是预检请求,则立即返回到前端
|
||||
SaRouter.match(SaHttpMethod.OPTIONS)
|
||||
.free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
|
||||
.back();
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
|
||||
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
|
||||
<script src="https://www.layuicdn.com/layer-v3.1.0/layer.js"></script>
|
||||
<script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')</script>
|
||||
<script type="text/javascript">
|
||||
|
||||
|
||||
+2
-2
@@ -17,10 +17,10 @@
|
||||
账号:<input name="name" /> <br>
|
||||
密码:<input name="pwd" type="password" /> <br>
|
||||
<button onclick="doLogin()">登录</button>
|
||||
<span style="color: #666;">(测试账号: sa 123456)</span>
|
||||
<span style="color: #666;">(测试账号: sa / 123456)</span>
|
||||
</div>
|
||||
<script src="https://unpkg.zhimg.com/jquery@3.4.1/dist/jquery.min.js"></script>
|
||||
<script src="https://www.layuicdn.com/layer-v3.1.1/layer.js"></script>
|
||||
<script src="https://www.layuicdn.com/layer-v3.1.0/layer.js"></script>
|
||||
<script>window.jQuery || alert('当前页面CDN服务商已宕机,请将所有js包更换为本地依赖')</script>
|
||||
<script type="text/javascript">
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
<parent>
|
||||
<groupId>org.noear</groupId>
|
||||
<artifactId>solon-parent</artifactId>
|
||||
<version>2.7.0</version>
|
||||
<version>3.0.1</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
</properties>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<parent>
|
||||
<groupId>org.noear</groupId>
|
||||
<artifactId>solon-parent</artifactId>
|
||||
<version>2.9.1</version>
|
||||
<version>3.0.4</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<java.version>17</java.version>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.traget>17</maven.compiler.traget>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
</properties>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<!--<spring.version>4.2.5.RELEASE</spring.version>-->
|
||||
<spring.version>5.3.7</spring.version>
|
||||
<jackson.version>2.16.1</jackson.version>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -10,14 +10,13 @@
|
||||
<parent>
|
||||
<groupId>org.noear</groupId>
|
||||
<artifactId>solon-parent</artifactId>
|
||||
<version>2.7.0</version>
|
||||
<version>3.0.1</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<solon.version>2.7.0</solon.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
<parent>
|
||||
<groupId>org.noear</groupId>
|
||||
<artifactId>solon-parent</artifactId>
|
||||
<version>2.7.0</version>
|
||||
<version>3.0.1</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
<parent>
|
||||
<groupId>org.noear</groupId>
|
||||
<artifactId>solon-parent</artifactId>
|
||||
<version>2.7.0</version>
|
||||
<version>3.0.1</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
<parent>
|
||||
<groupId>org.noear</groupId>
|
||||
<artifactId>solon-parent</artifactId>
|
||||
<version>2.7.0</version>
|
||||
<version>3.0.1</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<title>Sa-SSO-Server 认证中心-登录</title>
|
||||
<meta charset="utf-8">
|
||||
<base th:href="@{/}" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<link rel="stylesheet" href="./login.css">
|
||||
</head>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
package com.pj.h5;
|
||||
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 跨域过滤器
|
||||
* @author click33
|
||||
*/
|
||||
@Component
|
||||
@Order(-200)
|
||||
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;
|
||||
|
||||
// 允许指定域访问跨域资源
|
||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||
// 允许所有请求方式
|
||||
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");
|
||||
|
||||
// 如果是预检请求,直接返回
|
||||
if (OPTIONS.equals(request.getMethod())) {
|
||||
System.out.println("=======================浏览器发来了OPTIONS预检请求==========");
|
||||
response.getWriter().print("");
|
||||
return;
|
||||
}
|
||||
|
||||
chain.doFilter(req, res);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
}
|
||||
|
||||
}
|
||||
+3
-12
@@ -1,14 +1,12 @@
|
||||
package com.pj.h5;
|
||||
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import cn.dev33.satoken.sso.util.SaSsoConsts;
|
||||
import cn.dev33.satoken.sso.template.SaSsoUtil;
|
||||
import cn.dev33.satoken.sso.util.SaSsoConsts;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaFoxUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 前后台分离架构下集成SSO所需的代码 (SSO-Server端)
|
||||
@@ -41,11 +39,4 @@ public class H5Controller {
|
||||
}
|
||||
}
|
||||
|
||||
// 全局异常拦截
|
||||
@ExceptionHandler
|
||||
public SaResult handlerException(Exception e) {
|
||||
e.printStackTrace();
|
||||
return SaResult.error(e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.pj.h5;
|
||||
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.filter.SaServletFilter;
|
||||
import cn.dev33.satoken.router.SaHttpMethod;
|
||||
import cn.dev33.satoken.router.SaRouter;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* [Sa-Token 权限认证] 配置类 (解决跨域问题)
|
||||
*
|
||||
* @author click33
|
||||
*/
|
||||
@Configuration
|
||||
public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* 注册 [Sa-Token 全局过滤器]
|
||||
*/
|
||||
@Bean
|
||||
public SaServletFilter getSaServletFilter() {
|
||||
return new SaServletFilter()
|
||||
|
||||
// 指定 [拦截路由] 与 [放行路由]
|
||||
.addInclude("/**").addExclude("/favicon.ico")
|
||||
|
||||
// 认证函数: 每次请求执行
|
||||
.setAuth(obj -> {
|
||||
// SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());
|
||||
// ...
|
||||
})
|
||||
|
||||
// 异常处理函数:每次认证函数发生异常时执行此函数
|
||||
.setError(e -> {
|
||||
return SaResult.error(e.getMessage());
|
||||
})
|
||||
|
||||
// 前置函数:在每次认证函数之前执行
|
||||
.setBeforeAuth(obj -> {
|
||||
SaHolder.getResponse()
|
||||
|
||||
// ---------- 设置跨域响应头 ----------
|
||||
// 允许指定域访问跨域资源
|
||||
.setHeader("Access-Control-Allow-Origin", "*")
|
||||
// 允许所有请求方式
|
||||
.setHeader("Access-Control-Allow-Methods", "*")
|
||||
// 允许的header参数
|
||||
.setHeader("Access-Control-Allow-Headers", "*")
|
||||
// 有效时间
|
||||
.setHeader("Access-Control-Max-Age", "3600")
|
||||
;
|
||||
|
||||
// 如果是预检请求,则立即返回到前端
|
||||
SaRouter.match(SaHttpMethod.OPTIONS)
|
||||
.free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
|
||||
.back();
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
package com.pj.h5;
|
||||
|
||||
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.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 跨域过滤器
|
||||
* @author click33
|
||||
*/
|
||||
@Component
|
||||
@Order(-200)
|
||||
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;
|
||||
|
||||
// 允许指定域访问跨域资源
|
||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||
// 允许所有请求方式
|
||||
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");
|
||||
|
||||
// 如果是预检请求,直接返回
|
||||
if (OPTIONS.equals(request.getMethod())) {
|
||||
System.out.println("=======================浏览器发来了OPTIONS预检请求==========");
|
||||
response.getWriter().print("");
|
||||
return;
|
||||
}
|
||||
|
||||
// System.out.println("*********************************过滤器被使用**************************");
|
||||
chain.doFilter(req, res);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
}
|
||||
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.pj.h5;
|
||||
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.filter.SaServletFilter;
|
||||
import cn.dev33.satoken.router.SaHttpMethod;
|
||||
import cn.dev33.satoken.router.SaRouter;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* [Sa-Token 权限认证] 配置类 (解决跨域问题)
|
||||
*
|
||||
* @author click33
|
||||
*/
|
||||
@Configuration
|
||||
public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* 注册 [Sa-Token 全局过滤器]
|
||||
*/
|
||||
@Bean
|
||||
public SaServletFilter getSaServletFilter() {
|
||||
return new SaServletFilter()
|
||||
|
||||
// 指定 [拦截路由] 与 [放行路由]
|
||||
.addInclude("/**").addExclude("/favicon.ico")
|
||||
|
||||
// 认证函数: 每次请求执行
|
||||
.setAuth(obj -> {
|
||||
// SaManager.getLog().debug("----- 请求path={} 提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());
|
||||
// ...
|
||||
})
|
||||
|
||||
// 异常处理函数:每次认证函数发生异常时执行此函数
|
||||
.setError(e -> {
|
||||
return SaResult.error(e.getMessage());
|
||||
})
|
||||
|
||||
// 前置函数:在每次认证函数之前执行
|
||||
.setBeforeAuth(obj -> {
|
||||
SaHolder.getResponse()
|
||||
|
||||
// ---------- 设置跨域响应头 ----------
|
||||
// 允许指定域访问跨域资源
|
||||
.setHeader("Access-Control-Allow-Origin", "*")
|
||||
// 允许所有请求方式
|
||||
.setHeader("Access-Control-Allow-Methods", "*")
|
||||
// 允许的header参数
|
||||
.setHeader("Access-Control-Allow-Headers", "*")
|
||||
// 有效时间
|
||||
.setHeader("Access-Control-Max-Age", "3600")
|
||||
;
|
||||
|
||||
// 如果是预检请求,则立即返回到前端
|
||||
SaRouter.match(SaHttpMethod.OPTIONS)
|
||||
.free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
|
||||
.back();
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -8,7 +8,7 @@ sa-token:
|
||||
sso-client:
|
||||
# SSO-Server 端主机地址
|
||||
server-url: http://sa-sso-server.com:9000
|
||||
# 前后端分离时打开这个
|
||||
# 在 sso-server 端前后端分离时打开这个(上面的不要注释,auth-url 配置项和 server-url 要同时存在)
|
||||
# auth-url: http://127.0.0.1:8848/sa-token-demo-sso-server-h5/sso-auth.html
|
||||
sign:
|
||||
# API 接口调用秘钥
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
<java.run.main.class>com.pj.SaTokenApplication</java.run.main.class>
|
||||
</properties>
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package com.pj.satoken;
|
||||
|
||||
import cn.dev33.satoken.stp.StpInterface;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import cn.dev33.satoken.stp.StpInterface;
|
||||
|
||||
/**
|
||||
* 自定义权限验证接口扩展
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.pj.test;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 测试专用Controller
|
||||
* @author click33
|
||||
*
|
||||
*/
|
||||
@RestController
|
||||
public class Test2Controller {
|
||||
|
||||
// 测试登录 ---- http://localhost:8081/test
|
||||
@RequestMapping("/test")
|
||||
public SaResult test2() {
|
||||
|
||||
StpUtil.login(30003);
|
||||
System.out.println(StpUtil.getSession().getTimeout());
|
||||
System.out.println(StpUtil.getStpLogic().getTokenSession(false));
|
||||
|
||||
return SaResult.ok();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -9,9 +9,9 @@ import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
|
||||
/**
|
||||
* Sa-Token-SSO Server端 Controller
|
||||
* @author click33
|
||||
* 测试 Controller
|
||||
*
|
||||
* @author click33
|
||||
*/
|
||||
@RestController
|
||||
public class TestController {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<!-- 定义 Sa-Token 版本号 -->
|
||||
<properties>
|
||||
<sa-token.version>1.39.0</sa-token.version>
|
||||
<sa-token.version>1.40.0</sa-token.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<description>Sa-Token Dependencies</description>
|
||||
|
||||
<properties>
|
||||
<revision>1.39.0</revision>
|
||||
<revision>1.40.0</revision>
|
||||
|
||||
<!-- 统一定义依赖版本号 -->
|
||||
<springboot.version>2.5.15</springboot.version>
|
||||
@@ -23,7 +23,8 @@
|
||||
<servlet-api.version>3.1.0</servlet-api.version>
|
||||
<jakarta-servlet-api.version>6.0.0</jakarta-servlet-api.version>
|
||||
<thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
|
||||
<solon.version>2.7.0</solon.version>
|
||||
<freemarker.version>2.3.34</freemarker.version>
|
||||
<solon.version>3.0.4</solon.version>
|
||||
<noear-redisx.version>1.6.2</noear-redisx.version>
|
||||
<noear-snack3.version>3.2.88</noear-snack3.version>
|
||||
<jfinal.version>4.9.17</jfinal.version>
|
||||
@@ -200,8 +201,15 @@
|
||||
<artifactId>thymeleaf</artifactId>
|
||||
<version>${thymeleaf.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 视图引擎 -->
|
||||
|
||||
<!-- freemarker -->
|
||||
<dependency>
|
||||
<groupId>org.freemarker</groupId>
|
||||
<artifactId>freemarker</artifactId>
|
||||
<version>${freemarker.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- spring-boot-starter-thymeleaf -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<p align="center">
|
||||
<img alt="logo" src="https://sa-token.cc/logo.png" width="150" height="150">
|
||||
</p>
|
||||
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">Sa-Token v1.39.0</h1>
|
||||
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">Sa-Token v1.40.0</h1>
|
||||
<h5 align="center">一个轻量级 Java 权限认证框架,让鉴权变得简单、优雅!</h5>
|
||||
<p align="center" class="badge-box">
|
||||
<a href="https://gitee.com/dromara/sa-token/stargazers"><img src="https://gitee.com/dromara/sa-token/badge/star.svg?theme=gvp"></a>
|
||||
<a href="https://gitee.com/dromara/sa-token/members"><img src="https://gitee.com/dromara/sa-token/badge/fork.svg?theme=gvp"></a>
|
||||
<a href="https://gitcode.com/dromara/sa-token/overview"><img src="https://gitcode.com/dromara/Sa-Token/star/badge.svg"></a>
|
||||
<a href="https://github.com/dromara/sa-token/stargazers"><img src="https://img.shields.io/github/stars/dromara/sa-token?style=flat-square&logo=GitHub"></a>
|
||||
<a href="https://github.com/dromara/sa-token/network/members"><img src="https://img.shields.io/github/forks/dromara/sa-token?style=flat-square&logo=GitHub"></a>
|
||||
<a href="https://github.com/dromara/sa-token/watchers"><img src="https://img.shields.io/github/watchers/dromara/sa-token?style=flat-square&logo=GitHub"></a>
|
||||
<a href="https://github.com/dromara/sa-token/issues"><img src="https://img.shields.io/github/issues/dromara/sa-token.svg?style=flat-square&logo=GitHub"></a>
|
||||
<!-- <a href="https://github.com/dromara/sa-token/issues"><img src="https://img.shields.io/github/issues/dromara/sa-token.svg?style=flat-square&logo=GitHub"></a> -->
|
||||
<a href="https://github.com/dromara/sa-token/blob/master/LICENSE"><img src="https://img.shields.io/github/license/dromara/sa-token.svg?style=flat-square"></a>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
- [定制化登录页面](/sso/sso-custom-login)
|
||||
- [自定义API路由](/sso/sso-custom-api)
|
||||
- [前后端分离下的整合方案](/sso/sso-h5)
|
||||
- [NoSdk 模式与非 java 项目](/sso/sso-nosdk)
|
||||
- [平台中心跳转模式](/sso/sso-home-jump)
|
||||
- [不同 Client 不同秘钥](/sso/sso-diff-key)
|
||||
- [用户数据同步 / 迁移](/sso/user-data-sync)
|
||||
@@ -62,6 +63,8 @@
|
||||
- [自定义 grant_type](/oauth2/oauth2-custom-grant_type)
|
||||
- [定制化登录页面与授权页面](/oauth2/oauth2-custom-login)
|
||||
- [自定义 API 路由 ](/oauth2/oauth2-custom-api)
|
||||
- [OAuth2-Server端前后台分离](/oauth2/oauth2-h5)
|
||||
- [OpenId 与 UnionId](/oauth2/oauth2-openid)
|
||||
- [开启 OIDC 协议](/oauth2/oauth2-oidc)
|
||||
- [使用注解校验 Access-Token](/oauth2/oauth2-at-check)
|
||||
- [OAuth2-与登录会话实现数据互通](/oauth2/oauth2-interworking)
|
||||
@@ -84,6 +87,8 @@
|
||||
- [Alone独立Redis插件](/plugin/alone-redis)
|
||||
- [持久层扩展](/plugin/dao-extend)
|
||||
- [和 Thymeleaf 集成](/plugin/thymeleaf-extend)
|
||||
- [和 Freemarker 集成](/plugin/freemarker-extend)
|
||||
- [注解鉴权 SpEL 表达式](/plugin/spel-at)
|
||||
- [和 jwt 集成](/plugin/jwt-extend)
|
||||
- [和 Dubbo 集成](/plugin/dubbo-extend)
|
||||
- [和 gRPC 集成](/plugin/grpc-extend)
|
||||
@@ -127,11 +132,14 @@
|
||||
- [解决反向代理 uri 丢失的问题](/fun/curr-domain)
|
||||
- [解决跨域问题](/fun/cors-filter)
|
||||
- [技术选型:SSO 与 OAuth2 对比](/fun/sso-vs-oauth2)
|
||||
- [集成 MongoDB 参考一](/up/integ-spring-mongod-1)
|
||||
- [集成 MongoDB 参考二](/up/integ-spring-mongod-2)
|
||||
<!-- - [框架源码所有技术栈](/fun/tech-stack) -->
|
||||
- [从 Shiro、SpringSecurity、JWT 迁移](/fun/auth-framework-function-test)
|
||||
- [issue 提问模板](/fun/issue-template)
|
||||
- [为Sa-Token贡献代码](/fun/git-pr)
|
||||
- [Sa-Token开源大事记](/fun/timeline)
|
||||
- [团队成员](/fun/team)
|
||||
- [Sa-Token框架掌握度--在线考试](/fun/sa-token-test)
|
||||
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ StpUtil.hasPermission(permission); // 判断:当前账号是否拥有指定
|
||||
StpUtil.hasPermission(loginId, permission); // 判断:指定账号是否含有指定权限标识, 返回true或false
|
||||
StpUtil.hasPermissionAnd(...permissionArray); // 判断:当前账号是否含有指定权限标识 [指定多个,必须全部验证通过]
|
||||
StpUtil.hasPermissionOr(...permissionArray); // 判断:当前账号是否含有指定权限标识 [指定多个,只要其一验证通过即可]
|
||||
StpUtil.checkPermission(permission); // 校验:当前账号是否含有指定权限标识, 如果验证未通过,则抛出异常: NotRoleException
|
||||
StpUtil.checkPermission(permission); // 校验:当前账号是否含有指定权限标识, 如果验证未通过,则抛出异常: NotRPermissionException
|
||||
StpUtil.checkPermissionAnd(...permissionArray); // 校验:当前账号是否含有指定权限标识 [指定多个,必须全部验证通过]
|
||||
StpUtil.checkPermissionOr(...permissionArray); // 校验:当前账号是否含有指定权限标识 [指定多个,只要其一验证通过即可]
|
||||
```
|
||||
|
||||
+16
-3
@@ -18,7 +18,7 @@
|
||||
<div class="logo-box">
|
||||
<img src="logo.png" title="logo" />
|
||||
<h1 class="logo-text">Sa-Token</h1>
|
||||
<sub>v1.39.0</sub>
|
||||
<sub>v1.40.0</sub>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@@ -28,6 +28,7 @@
|
||||
</div>
|
||||
<select class="select-version p-none" onchange="location.href=this.value">
|
||||
<option value="doc.html">最新版</option>
|
||||
<option value="v/v1.39.0/doc.html">v1.39.0</option>
|
||||
<option value="v/v1.38.0/doc.html">v1.38.0</option>
|
||||
<option value="v/v1.37.0/doc.html">v1.37.0</option>
|
||||
<option value="v/v1.36.0/doc.html">v1.36.0</option>
|
||||
@@ -109,10 +110,16 @@
|
||||
</a>
|
||||
<div class="zk-context">
|
||||
<div>
|
||||
<a href="https://www.bilibili.com/video/BV1uZUpYVEst/" target="_blank">fox说技术(7集)</a>
|
||||
<a href="https://www.bilibili.com/video/BV1eFtRezERp?p=87" target="_blank">架构驿站(11集)</a>
|
||||
<a href="https://www.bilibili.com/video/BV1Zt421u7gk/" target="_blank">王清江唷(99集)</a>
|
||||
<a href="https://www.bilibili.com/video/BV1kG411o7Ms/" target="_blank">筑梦信仰-joy(20集)</a>
|
||||
<a href="https://www.bilibili.com/video/BV11u4y197JL/" target="_blank">达达-Java(26集)</a>
|
||||
<a href="https://space.bilibili.com/473679148/video" target="_blank">晒太阳的盐(22集)</a>
|
||||
<a href="https://space.bilibili.com/473679148/video" target="_blank">晒太阳的盐(22集)</a>
|
||||
<div class="zk-fengexian"></div>
|
||||
<a href="javascript: layer.alert('如您有 Sa-Token 相关课程录制,请联系官网文档右侧 < sa-token 小助手 > 进行提交');">
|
||||
[ + 课程提交 ]
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -183,6 +190,12 @@
|
||||
<!-- help 按钮 -->
|
||||
<div class="help-btn">技术求助</div>
|
||||
|
||||
<!-- ew-wa -->
|
||||
<div class="ew-wa">
|
||||
<p>如果 Sa-Token 帮助到了你,希望你可以向同事、朋友推荐了解本框架,这对我们非常重要,感谢支持! <!-- 🤗 --> </p>
|
||||
<p>加油,工程师!</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -220,7 +233,7 @@
|
||||
<script src="./static/is-star-plugin.js?v=7"></script>
|
||||
<script src="./static/is-fill-in-wj-plugin.js?v=7"></script>
|
||||
<script>
|
||||
var saTokenTopVersion = '1.39.0'; // Sa-Token最新版本
|
||||
var saTokenTopVersion = '1.40.0'; // Sa-Token最新版本
|
||||
var name = '<img style="width: 60px; height: 60px; vertical-align: middle;" src="logo.png" alt="logo" /> ';
|
||||
name += '<b style="font-size: 28px; vertical-align: middle;">Sa-Token</b> <sub>v' + saTokenTopVersion + '</sub>';
|
||||
window.$docsify = {
|
||||
|
||||
@@ -37,14 +37,14 @@ public @interface CheckAccount {
|
||||
|
||||
#### 1.2、第二步,创建注解处理器
|
||||
|
||||
实现 `SaAnnotationAbstractHandler` 接口,指定泛型为刚才自定义的注解
|
||||
实现 `SaAnnotationHandlerInterface` 接口,指定泛型为刚才自定义的注解
|
||||
|
||||
``` java
|
||||
/**
|
||||
* 注解 CheckAccount 的处理器
|
||||
*/
|
||||
@Component
|
||||
public class CheckAccountHandler implements SaAnnotationAbstractHandler<CheckAccount> {
|
||||
public class CheckAccountHandler implements SaAnnotationHandlerInterface<CheckAccount> {
|
||||
|
||||
// 指定这个处理器要处理哪个注解
|
||||
@Override
|
||||
@@ -156,7 +156,7 @@ public @interface SaUserCheckLogin {
|
||||
* 注解 SaUserCheckLogin 的处理器
|
||||
*/
|
||||
@Component
|
||||
public class SaUserCheckLoginHandler implements SaAnnotationAbstractHandler<SaUserCheckLogin> {
|
||||
public class SaUserCheckLoginHandler implements SaAnnotationHandlerInterface<SaUserCheckLogin> {
|
||||
|
||||
@Override
|
||||
public Class<SaUserCheckLogin> getHandlerAnnotationClass() {
|
||||
|
||||
@@ -230,18 +230,37 @@ clientId + loginId 反查 code
|
||||
|
||||
``` js
|
||||
{
|
||||
"@class": "cn.dev33.satoken.oauth2.model.AccessTokenModel", // java class 信息
|
||||
"accessToken": "CqRVp2HrgyklE0BXYWszskGJWAGY7xhGu9Zaco4zJECzGYagCCFWj0jOlHoU", // 资源令牌值
|
||||
"refreshToken": "EAubykIqRLwbvvi0wfZqnWxoC1bLhPguIfTqX3S1aoTe6pCLKsV9jU3OEI8U", // 刷新令牌值
|
||||
"expiresTime": 1722422031510, // 资源令牌到期时间
|
||||
"refreshExpiresTime": 1725006831511, // 刷新令牌到期时间
|
||||
"@class": "cn.dev33.satoken.oauth2.data.model.AccessTokenModel", // java class 信息
|
||||
"accessToken": "Pu3t55dJIgvkmVoHz50FqaVQOJ6Flggjr2eHTiS74Ooai8e3nNyYPq78K80P", // 资源令牌值
|
||||
"refreshToken": "baGyl6PHK304tPojnpxd1SpW12oJcOGv7gFaDAAkjLWbJG1J1WLUIGobsw7m", // 刷新令牌值
|
||||
"expiresTime": 1738280553695, // 资源令牌到期时间
|
||||
"refreshExpiresTime": 1740865353760, // 刷新令牌到期时间
|
||||
"clientId": "1001", // 对应的应用id
|
||||
"loginId": "10001", // 对应的loginId
|
||||
"openid": "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__", // 对应的 openid
|
||||
"scope": "", // 所具有的权限列表,多个用逗号隔开
|
||||
"expiresIn": 7199, // 资源令牌剩余有效时间,单位秒
|
||||
"refreshExpiresIn": 2592000 // 刷新令牌剩余有效时间,单位秒
|
||||
"scopes": [ // 所具有的权限列表
|
||||
"java.util.ArrayList",
|
||||
[
|
||||
"userinfo",
|
||||
"userid",
|
||||
"openid",
|
||||
"unionid",
|
||||
"oidc"
|
||||
]
|
||||
],
|
||||
"tokenType": "bearer", // tokenType
|
||||
"grantType": "authorization_code", // 授权方式
|
||||
"extraData": { // 扩展数据
|
||||
"@class": "java.util.LinkedHashMap",
|
||||
"userid": "10001",
|
||||
"openid": "ded91dc189a437dd1bac2274be167d50",
|
||||
"unionid": "11d48faa74c4e5f19355ccc53c1c5c7a",
|
||||
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2Etb2F1dGgtc2VydmVyLmNvbTo4MDAwIiwic3ViIjoiMTAwMDEiLCJhdWQiOiIxMDAxIiwiZXhwIjoxNzM4MjczOTUzLCJpYXQiOjE3MzgyNzMzNTMsImF1dGhfdGltZSI6MTczODI3MzM0Miwibm9uY2UiOiJZQTlPQjJzYkpGanZkUlFjN0E3V1pnTUFhTDFVRjE5OSIsImF6cCI6IjEwMDEifQ.pvoj6CR7tdhOblvYJoGUfvam9egSiL5Uw3tflLLMb5g"
|
||||
},
|
||||
"createTime": 1738273353694, // 创建时间
|
||||
"expiresIn": 7199 // 资源令牌剩余有效时间,单位秒
|
||||
"refreshExpiresIn": 2592000, // 刷新令牌剩余有效时间,单位秒
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -264,13 +283,29 @@ clientId + loginId 反查 Access-Token
|
||||
|
||||
``` js
|
||||
{
|
||||
"@class": "cn.dev33.satoken.oauth2.model.RefreshTokenModel", // java class 信息
|
||||
"refreshToken": "EAubykIqRLwbvvi0wfZqnWxoC1bLhPguIfTqX3S1aoTe6pCLKsV9jU3OEI8U", // 刷新令牌值
|
||||
"expiresTime": 1725006831511, // 刷新令牌到期时间
|
||||
"@class": "cn.dev33.satoken.oauth2.data.model.RefreshTokenModel", // java class 信息
|
||||
"refreshToken": "baGyl6PHK304tPojnpxd1SpW12oJcOGv7gFaDAAkjLWbJG1J1WLUIGobsw7m", // 刷新令牌值
|
||||
"expiresTime": 1740865353760, // 刷新令牌到期时间
|
||||
"clientId": "1001", // 对应的应用id
|
||||
"scope": "", // 所具有的权限列表,多个用逗号隔开
|
||||
"loginId": "10001", // 对应的loginId
|
||||
"openid": "gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__", // 对应的 openid
|
||||
"scopes": [ // 所具有的权限列表
|
||||
"java.util.ArrayList",
|
||||
[
|
||||
"userinfo",
|
||||
"userid",
|
||||
"openid",
|
||||
"unionid",
|
||||
"oidc"
|
||||
]
|
||||
],
|
||||
"extraData": { // 扩展数据
|
||||
"@class": "java.util.LinkedHashMap",
|
||||
"userid": "10001",
|
||||
"openid": "ded91dc189a437dd1bac2274be167d50",
|
||||
"unionid": "11d48faa74c4e5f19355ccc53c1c5c7a",
|
||||
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2Etb2F1dGgtc2VydmVyLmNvbTo4MDAwIiwic3ViIjoiMTAwMDEiLCJhdWQiOiIxMDAxIiwiZXhwIjoxNzM4MjczOTUzLCJpYXQiOjE3MzgyNzMzNTMsImF1dGhfdGltZSI6MTczODI3MzM0Miwibm9uY2UiOiJZQTlPQjJzYkpGanZkUlFjN0E3V1pnTUFhTDFVRjE5OSIsImF6cCI6IjEwMDEifQ.pvoj6CR7tdhOblvYJoGUfvam9egSiL5Uw3tflLLMb5g"
|
||||
},
|
||||
"createTime": 1738273353760, // 创建时间
|
||||
"expiresIn": 2591999 // 刷新令牌剩余有效时间,单位秒
|
||||
}
|
||||
```
|
||||
@@ -295,12 +330,27 @@ clientId + loginId 反查 Refresh-Token
|
||||
|
||||
``` js
|
||||
{
|
||||
"@class": "cn.dev33.satoken.oauth2.model.ClientTokenModel", // java class 信息
|
||||
"clientToken": "fWQjBKxprSslmYFLbzen0oa95rOvqnqYKZW3sD8mzamNbabG8b6MPKPP5uCu", // 应用令牌值
|
||||
"expiresTime": 1722425237153, // 应用令牌到期时间
|
||||
"@class": "cn.dev33.satoken.oauth2.data.model.ClientTokenModel", // java class 信息
|
||||
"clientToken": "lIpS3fKEACKMFauEWVpR7Zmzh7SoFetPVuB9aDzISnqzHKu8R3OwpWFy5nLv", // 应用令牌值
|
||||
"expiresTime": 1738280930646, // 应用令牌到期时间
|
||||
"clientId": "1001", // 对应的应用id
|
||||
"scope": null, // 所具有的权限列表,多个用逗号隔开
|
||||
"expiresIn": 7200 // 应用令牌剩余有效时间,单位秒
|
||||
"scopes": [ // 所具有的权限列表
|
||||
"java.util.ArrayList",
|
||||
[
|
||||
"userinfo",
|
||||
"userid",
|
||||
"openid",
|
||||
"unionid",
|
||||
"oidc"
|
||||
]
|
||||
],
|
||||
"tokenType": "bearer", // tokenType
|
||||
"grantType": "client_credentials", // 授权类型
|
||||
"extraData": { // 扩展数据
|
||||
"@class": "java.util.LinkedHashMap"
|
||||
},
|
||||
"createTime": 1738273730646, // 创建时间
|
||||
"expiresIn": 7199 // 应用令牌剩余有效时间,单位秒
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# 团队成员
|
||||
|
||||
|
||||
### 开发
|
||||
|
||||
负责:代码开发、社区维护、issue 处理、pr 审核等。
|
||||
|
||||
<table class="team-table">
|
||||
<tr>
|
||||
<th>头像</th>
|
||||
<th>昵称</th>
|
||||
<th>个人主页</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-xiaofengzheng.jpg" /></td>
|
||||
<td>小风筝(作者)</td>
|
||||
<td><a href="https://gitee.com/click33" target="_blank">https://gitee.com/click33</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-AppleOfGray.png" /></td>
|
||||
<td>AppleOfGray</td>
|
||||
<td><a href="https://gitee.com/appleOfGray" target="_blank">https://gitee.com/appleOfGray</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-ly-chn.png" /></td>
|
||||
<td>ly-chn</td>
|
||||
<td><a href="https://gitee.com/ly-chn" target="_blank">https://gitee.com/ly-chn</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
### 提案讨论组成员
|
||||
|
||||
负责:提案新增、讨论、投票。
|
||||
|
||||
<table class="team-table">
|
||||
<tr>
|
||||
<th>头像</th>
|
||||
<th>昵称</th>
|
||||
<th>个人主页</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-xiaofengzheng.jpg" /></td>
|
||||
<td>刘潇</td>
|
||||
<td><a href="https://gitee.com/click33" target="_blank">https://gitee.com/click33</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-AppleOfGray.png" /></td>
|
||||
<td>AppleOfGray</td>
|
||||
<td><a href="https://gitee.com/appleOfGray" target="_blank">https://gitee.com/appleOfGray</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-ly-chn.png" /></td>
|
||||
<td>ly-chn</td>
|
||||
<td><a href="https://gitee.com/ly-chn" target="_blank">https://gitee.com/ly-chn</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-moli.jpg" /></td>
|
||||
<td>茉莉</td>
|
||||
<td><a href="https://gitee.com/kidoldman" target="_blank">https://gitee.com/kidoldman</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-yaoshui.jpg" /></td>
|
||||
<td>药水</td>
|
||||
<td><a href="https://gitee.com/java_pioneer" target="_blank">https://gitee.com/java_pioneer</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-daimouren.png" /></td>
|
||||
<td>呆某人</td>
|
||||
<td><a href="https://gitee.com/zhubj0510" target="_blank">https://gitee.com/zhubj0510</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-chunkunqiufa.jpg" /></td>
|
||||
<td>春困夏倦秋乏</td>
|
||||
<td><a href="https://gitee.com/uncarbon97" target="_blank">https://gitee.com/uncarbon97</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oss.dev33.cn/sa-token/team/avatar-danmo.jpg" /></td>
|
||||
<td>淡墨</td>
|
||||
<td><a href="https://gitee.com/jinan-jimeng-network_0" target="_blank">https://gitee.com/jinan-jimeng-network_0</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -27,5 +27,11 @@
|
||||
- **2022-05-20:** 成为 [可信开源社区共同体] 预备成员。
|
||||
- **2022-08-01:** 加入 [中国开源社区 landscape]。
|
||||
- **2022-08-18:** GitHub 第 10000 个 star 里程碑!
|
||||
- **2023-01-09:** 荣获 OSC 2022 年度最热开源项目社区。
|
||||
- **2023-11-21:** 被评为“开放原子基金会2023快速成长开源项目”。
|
||||
- **2024-04-25:** 42.9k star 登顶 Gitee 开源项目推荐榜 Top 1。
|
||||
- **2024-08-19:** 成为 GitCode G-Star 开源摘星计划毕业项目。
|
||||
- **2024-11-22:** 所在开源社区 “Dromara” 荣获《2024中国互联网发展创新与投资大赛(开源)》二等奖。
|
||||
|
||||
|
||||
|
||||
|
||||
+153
-41
@@ -63,10 +63,16 @@
|
||||
</a>
|
||||
<div class="zk-context">
|
||||
<div>
|
||||
<a href="https://www.bilibili.com/video/BV1uZUpYVEst/" target="_blank">fox说技术(7集)</a>
|
||||
<a href="https://www.bilibili.com/video/BV1eFtRezERp?p=87" target="_blank">架构驿站(11集)</a>
|
||||
<a href="https://www.bilibili.com/video/BV1Zt421u7gk/" target="_blank">王清江唷(99集)</a>
|
||||
<a href="https://www.bilibili.com/video/BV1kG411o7Ms/" target="_blank">筑梦信仰-joy(20集)</a>
|
||||
<a href="https://www.bilibili.com/video/BV11u4y197JL/" target="_blank">达达-Java(26集)</a>
|
||||
<a href="https://space.bilibili.com/473679148/video" target="_blank">晒太阳的盐(22集)</a>
|
||||
<div class="zk-fengexian"></div>
|
||||
<a href="javascript: layer.alert('如您有 Sa-Token 相关课程录制,请联系官网文档右侧 < sa-token 小助手 > 进行提交');">
|
||||
[ + 课程提交 ]
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,16 +122,18 @@
|
||||
<div class="main-box">
|
||||
<div class="content-box">
|
||||
<!-- <div class="fenge"></div> -->
|
||||
<h1>Sa-Token<small>v1.39.0</small></h1>
|
||||
<h1>Sa-Token<small>v1.40.0</small></h1>
|
||||
<div class="sub-title">
|
||||
<span class="sub-title-nr">一个轻量级 java 权限认证框架,让鉴权变得简单、优雅!</span>
|
||||
<div class="gb-cursor"> </div>
|
||||
</div>
|
||||
<div class="btn-box">
|
||||
<a class="abtn" href="https://gitee.com/dromara/sa-token" target="_blank">Gitee</a>
|
||||
<a class="abtn" href="https://github.com/dromara/sa-token" target="_blank">GitHub</a>
|
||||
<!-- <a class="abtn" href="https://gitee.com/dromara/sa-token" target="_blank">Gitee</a>
|
||||
<a class="abtn" href="https://github.com/dromara/sa-token" target="_blank">GitHub</a> -->
|
||||
<a class="abtn" href="doc.html#/more/demand-commit" target="_self">需求提交</a>
|
||||
<a class="abtn" href="https://gitee.com/sa-tokens/awesome-sa-token" target="_blank">开源案例</a>
|
||||
<a class="abtn" href="doc.html#/more/join-group" target="_self">加入讨论群</a>
|
||||
<a class="abtn doc-btn" href="doc.html" target="_self">现在出发 →</a>
|
||||
<a class="abtn doc-btn" href="doc.html" target="_self">在线文档 →</a>
|
||||
<!-- <a href="https://gitee.com/dromara/sa-token" target="_blank">集成案例</a> -->
|
||||
</div>
|
||||
<h4 align="center" class="badge-box">
|
||||
@@ -144,6 +152,44 @@
|
||||
<a href="https://github.com/dromara/sa-token/blob/master/LICENSE"><img class="lazy"
|
||||
data-original="https://img.shields.io/github/license/dromara/sa-token.svg?style=flat-square"></a>
|
||||
</h4>
|
||||
<div class="qt-pt-box">
|
||||
<a href="https://gitee.com/dromara/sa-token" target="_blank">
|
||||
<img src="https://oss.dev33.cn/sa-token/img/gitee.png" alt="">
|
||||
</a>
|
||||
<a href="https://gitcode.com/dromara/sa-token" target="_blank">
|
||||
<img src="https://oss.dev33.cn/sa-token/img/gitcode.png" alt="">
|
||||
</a>
|
||||
<a href="https://github.com/dromara/sa-token" target="_blank">
|
||||
<img src="https://oss.dev33.cn/sa-token/img/github.png" alt="">
|
||||
</a>
|
||||
<span class="dmt-link">
|
||||
<img class="dmt-img" src="https://oss.dev33.cn/sa-token/img/zong-4.png" alt="">
|
||||
<span class="dmt-tips">B站、抖音、视频号 ...</span>
|
||||
<div class="dmt-detail">
|
||||
<h4>关注我们 → 分享“权限认证架构设计”干货视频</h4>
|
||||
<div class="dmt-item-box">
|
||||
<div class="dmt-item dmt-item-bilibili">
|
||||
<a href="https://space.bilibili.com/3546758575557094" target="_blank">
|
||||
<img class="dmt-qr-img" src="https://oss.dev33.cn/sa-token/img/bilibili-qr-fang.png" alt="">
|
||||
<img class="dmt-logo-img" src="https://oss.dev33.cn/sa-token/img/bilibili-light.png" alt="">
|
||||
</a>
|
||||
</div>
|
||||
<div class="dmt-item dmt-item-douyin">
|
||||
<a href="https://www.douyin.com/user/MS4wLjABAAAArVqj2lGRurfj-9eO0T12q6_vrbIK-Om9bi3eo4OwB2g" target="_blank">
|
||||
<img class="dmt-qr-img" src="https://oss.dev33.cn/sa-token/img/douyin-qr-fang.png" alt="">
|
||||
<img class="dmt-logo-img" src="https://oss.dev33.cn/sa-token/img/douyin-light.png" alt="">
|
||||
</a>
|
||||
</div>
|
||||
<div class="dmt-item dmt-item-wxsph">
|
||||
<a href="javascript: layer.msg('微信视频号暂未提供PC网站,请在手机微信扫码订阅');">
|
||||
<img class="dmt-qr-img" src="https://oss.dev33.cn/sa-token/img/wxsph-qr-fang.png" alt="">
|
||||
<img class="dmt-logo-img" src="https://oss.dev33.cn/sa-token/img/wxsph-light.png" alt="">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -209,19 +255,19 @@
|
||||
<img src="https://oss.dev33.cn/sa-token/awards/gpv.jpg?x-oss-process=style/st" /> <br>
|
||||
<p>GVP - Gitee 最有价值开源项目</p>
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<img src="https://oss.dev33.cn/sa-token/awards/g-star.jpg?x-oss-process=style/st" /> <br>
|
||||
<p>GitCode G-Star 优质开源项目</p>
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<img src="https://oss.dev33.cn/sa-token/awards/osc-2021.jpg?x-oss-process=style/st"/> <br>
|
||||
<p>OSCHINA 2021 人气指数 TOP 30 开源项目</p>
|
||||
</div>
|
||||
<div class="swiper-slide swiper-slide-tx1">
|
||||
<img src="https://oss.dev33.cn/sa-token/awards/osc-2022.jpg?x-oss-process=style/st" /> <br>
|
||||
<img src="https://oss.dev33.cn/sa-token/awards/osc-2022--2.jpg?x-oss-process=style/st" /> <br>
|
||||
<p>OSCHINA 2022 年度最火热中国开源项目社区</p>
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<img src="https://oss.dev33.cn/sa-token/awards/kexin.jpg?x-oss-process=style/st" /> <br>
|
||||
<p>可信开源社区共同体预备成员</p>
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<div class="swiper-slide swiper-slide-tx1">
|
||||
<img src="https://oss.dev33.cn/sa-token/awards/kaifangyuanzi2.jpg?x-oss-process=style/st" /> <br>
|
||||
<p>开放原子基金会2023快速成长开源项目</p>
|
||||
</div>
|
||||
@@ -229,6 +275,14 @@
|
||||
<img src="https://oss.dev33.cn/sa-token/awards/dromara.jpg?x-oss-process=style/st" /> <br>
|
||||
<p>Dromara 组织顶尖项目(之一)</p>
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<img src="https://oss.dev33.cn/sa-token/awards/kexin.jpg?x-oss-process=style/st" /> <br>
|
||||
<p>可信开源社区共同体预备成员</p>
|
||||
</div>
|
||||
<div class="swiper-slide">
|
||||
<img src="https://oss.dev33.cn/sa-token/awards/dromara-2024-tzds.jpg?x-oss-process=style/st" /> <br>
|
||||
<p>所在开源社区 “Dromara” 荣获《2024中国互联网发展创新与投资大赛(开源)》二等奖。</p>
|
||||
</div>
|
||||
<div class="swiper-slide" style="width: 750px;">
|
||||
<img src="https://oss.dev33.cn/sa-token/awards/gitee-top-1.png" /> <br>
|
||||
<p>Gitee 项目推荐榜 top 1</p>
|
||||
@@ -264,7 +318,7 @@
|
||||
<th>赞助人</th>
|
||||
<th>赞助金额</th>
|
||||
<th>留言</th>
|
||||
<th>时间</th>
|
||||
<th style="width: 100px;">时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -301,7 +355,16 @@
|
||||
<div class="s-fenge"></div>
|
||||
<h2 class="s-title" style="margin-top: 80px;">优秀开源集成案例</h2>
|
||||
<div class="feature-box s-case-box">
|
||||
<!-- mall4j 15.2k -->
|
||||
<!-- Snowy 19.8K -->
|
||||
<div class="s-case">
|
||||
<a href="https://gitee.com/xiaonuobase/snowy" target="_blank" class="s-case-link">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/case/case--snowy.png">
|
||||
</a>
|
||||
<h3 class="s-case-title">Snowy</h3>
|
||||
<span class="s-author"> 小诺开源技术 </span>
|
||||
<p class="s-case-intro">国内首个国密前后分离快速开发平台,基于Vue3、Antdv、SaToken</p>
|
||||
</div>
|
||||
<!-- mall4j 15.9k -->
|
||||
<div class="s-case">
|
||||
<a href="https://gitee.com/gz-yami/mall4j" target="_blank" class="s-case-link">
|
||||
<img class="lazy"
|
||||
@@ -311,7 +374,7 @@
|
||||
<span class="s-author"> Mall4j商城系统 </span>
|
||||
<p class="s-case-intro">基于Spring Boot 3 JDK17的一个商城手脚架。</p>
|
||||
</div>
|
||||
<!-- RuoYi-Vue-Plus 8.8k -->
|
||||
<!-- RuoYi-Vue-Plus 10.7k -->
|
||||
<div class="s-case">
|
||||
<a href="https://gitee.com/dromara/RuoYi-Vue-Plus" target="_blank" class="s-case-link">
|
||||
<img class="lazy"
|
||||
@@ -321,16 +384,16 @@
|
||||
<span class="s-author"> 疯狂的狮子Li </span>
|
||||
<p class="s-case-intro">重写 RuoYi-Vue 所有功能,集成 Sa-Token、Mybatis-Plus、Hutool 定期同步</p>
|
||||
</div>
|
||||
<!-- Snowy 7.9K -->
|
||||
<!-- Smart-Admin 7.6K -->
|
||||
<div class="s-case">
|
||||
<a href="https://gitee.com/xiaonuobase/snowy" target="_blank" class="s-case-link">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/case/case--snowy.png">
|
||||
<a href="https://gitee.com/lab1024/smart-admin" target="_blank" class="s-case-link">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/case/case--smart-admin.png">
|
||||
</a>
|
||||
<h3 class="s-case-title">Snowy</h3>
|
||||
<span class="s-author"> 小诺开源技术 </span>
|
||||
<p class="s-case-intro">国内首个国密前后分离快速开发平台,基于Vue3、Antdv、SaToken</p>
|
||||
<h3 class="s-case-title">Smart-Admin</h3>
|
||||
<span class="s-author"> 1024创新实验室 </span>
|
||||
<p class="s-case-intro">坚持以「高质量代码」为核心,「简洁、高效、安全」的中后台解决方案!</p>
|
||||
</div>
|
||||
<!-- SpringBoot_v2 5.8k -->
|
||||
<!-- SpringBoot_v2 6k -->
|
||||
<div class="s-case">
|
||||
<a href="https://gitee.com/bdj/SpringBoot_v2" target="_blank" class="s-case-link">
|
||||
<img class="lazy"
|
||||
@@ -340,7 +403,16 @@
|
||||
<span class="s-author">开源oschina</span>
|
||||
<p class="s-case-intro">努力打造 springboot 框架的极致细腻的脚手架,原生纯净。</p>
|
||||
</div>
|
||||
<!-- RuoYi-Cloud-Plus 3.9K -->
|
||||
<!-- Lamp-Cloud 5.4K -->
|
||||
<div class="s-case">
|
||||
<a href="https://gitee.com/dromara/lamp-cloud" target="_blank" class="s-case-link">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/case/case--lamp-cloud.png">
|
||||
</a>
|
||||
<h3 class="s-case-title">灯灯</h3>
|
||||
<span class="s-author"> 最后 </span>
|
||||
<p class="s-case-intro">专注于多租户解决方案的微服务中后台快速开发平台。</p>
|
||||
</div>
|
||||
<!-- RuoYi-Cloud-Plus 4.9K -->
|
||||
<div class="s-case">
|
||||
<a href="https://gitee.com/dromara/RuoYi-Cloud-Plus" target="_blank" class="s-case-link">
|
||||
<img class="lazy"
|
||||
@@ -350,8 +422,16 @@
|
||||
<span class="s-author"> 疯狂的狮子Li </span>
|
||||
<p class="s-case-intro">重写 RuoYi-Cloud 所有功能 整合 SpringCloudAlibaba、Dubbo3.0、Sa-Token</p>
|
||||
</div>
|
||||
|
||||
<!-- 拾壹博客 1.4K -->
|
||||
<!-- Orange-Admin 3.3K -->
|
||||
<div class="s-case">
|
||||
<a href="https://gitee.com/orangeform/orange-admin" target="_blank" class="s-case-link">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/case/case--orange-admin.png">
|
||||
</a>
|
||||
<h3 class="s-case-title">橙单</h3>
|
||||
<span class="s-author"> orange-form </span>
|
||||
<p class="s-case-intro">橙单中台化低代码生成器。多应用、多租户、多渠道、工作流、在线表单等。</p>
|
||||
</div>
|
||||
<!-- 拾壹博客 1.7K -->
|
||||
<div class="s-case">
|
||||
<a href="https://gitee.com/quequnlong/shiyi-blog" target="_blank" class="s-case-link">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/case/case--shiyi-blog.png">
|
||||
@@ -360,8 +440,8 @@
|
||||
<span class="s-author"> bule </span>
|
||||
<p class="s-case-intro">一款 Vue + SpringBoot 前后端分离的博客系统</p>
|
||||
</div>
|
||||
<!-- EasyAdmin 1.2k -->
|
||||
<div class="s-case">
|
||||
<!-- EasyAdmin 1.4k -->
|
||||
<!-- <div class="s-case">
|
||||
<a href="https://gitee.com/lakernote/easy-admin" target="_blank" class="s-case-link">
|
||||
<img class="lazy"
|
||||
data-original="https://oss.dev33.cn/sa-token/case/case--easy-admin.png">
|
||||
@@ -369,25 +449,28 @@
|
||||
<h3 class="s-case-title">EasyAdmin</h3>
|
||||
<span class="s-author"> laker </span>
|
||||
<p class="s-case-intro">轻量级的后台管理系统脚手架,内置代码生成器、权限管理、工作流引擎等</p>
|
||||
</div>
|
||||
<!-- iot-iita 1.2K -->
|
||||
<div class="s-case">
|
||||
</div> -->
|
||||
<!-- iot-iita 1.6K -->
|
||||
<!-- <div class="s-case">
|
||||
<a href="https://gitee.com/open-iita/iotkit-parent" target="_blank" class="s-case-link">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/case/case--iot-iita.png">
|
||||
</a>
|
||||
<h3 class="s-case-title">iot-iita</h3>
|
||||
<span class="s-author"> 铱塔智联开源 </span>
|
||||
<p class="s-case-intro">一个轻量级低门槛的物联网平台,包含了多协议设备接入</p>
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- Sa-Plus 1.1K -->
|
||||
<div class="s-case">
|
||||
<!-- <div class="s-case">
|
||||
<a href="https://gitee.com/click33/sa-plus" target="_blank" class="s-case-link">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/case/case--sa-plus.png">
|
||||
</a>
|
||||
<h3 class="s-case-title">Sa-Plus</h3>
|
||||
<span class="s-author"> 孔明 </span>
|
||||
<p class="s-case-intro">一个基于 SpringBoot 的快速开发框架,内置代码生成器</p>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div class="re-text">
|
||||
<span>
|
||||
@@ -423,9 +506,9 @@
|
||||
<!-- <a href="https://www.suancheng.co/" target="_blank" title="山东酸橙网络科技有限公司">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/com/suanchengdudu.png">
|
||||
</a> -->
|
||||
<a href="https://chykj.com/" target="_blank" title="山东察远信息科技有限公司">
|
||||
<!-- <a href="https://chykj.com/" target="_blank" title="山东察远信息科技有限公司">
|
||||
<img class="lazy" data-original="https://chykj.com/upload/1/cms/content/cylogoc.jpg">
|
||||
</a>
|
||||
</a> -->
|
||||
<a href="https://ms.airsr.com/" target="_blank" title="北京天衢航空服务有限公司">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/com/tianquhangkong.png">
|
||||
</a>
|
||||
@@ -547,6 +630,15 @@
|
||||
<a href="https://www.jcodeyun.com/" target="_blank" title="协卓云动(深圳)数字发展有限公司">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/com/xiezhuoyundong.png">
|
||||
</a>
|
||||
<a href="https://www.fakamiao.com/" target="_blank" title="秦皇岛桃猫信息科技有限责任公司">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/com/taomaoxinxi.png" style="max-height: 50%;">
|
||||
</a>
|
||||
<a href="https://jiagouyizhan.com/" target="_blank" title="可持续架构(菏泽)信息科技有限公司">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/com/jiagouyizhan.png">
|
||||
</a>
|
||||
<a href="https://www.symtc.com/" target="_blank" title="沈阳地铁">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/com/shenyangditie.png">
|
||||
</a>
|
||||
</div>
|
||||
<div style="height: 10px; clear: both;"></div>
|
||||
<p>
|
||||
@@ -686,10 +778,10 @@
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/neutrino-proxy.svg"
|
||||
msg="一款基于 Netty 的、开源的内网穿透神器。">
|
||||
</a>
|
||||
<a href="https://chatgpt.cn.obiscr.com/" target="_blank">
|
||||
<!-- <a href="https://chatgpt.cn.obiscr.com/" target="_blank">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/chatgpt.png"
|
||||
msg="一个支持在 JetBrains 系列 IDE 上运行的 ChatGPT 的插件。">
|
||||
</a>
|
||||
</a> -->
|
||||
<a href="https://gitee.com/dromara/zyplayer-doc" target="_blank">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/zyplayer-doc.png"
|
||||
msg="zyplayer-doc是一款适合团队和个人使用的WIKI文档管理工具,同时还包含数据库文档、Api接口文档。">
|
||||
@@ -774,6 +866,10 @@
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/easyAI.png"
|
||||
msg="Java 傻瓜式 AI 框架。">
|
||||
</a>
|
||||
<a href="https://gitee.com/dromara/tianai-captcha" target="_blank">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/tianai-captcha.png"
|
||||
msg="可能是java界最好的开源行为验证码 captcha、captcha、captcha、captcha、tianai-captcha [滑块验证码、点选验证码、行为验证码、旋转验证码, 滑动验证码]。">
|
||||
</a>
|
||||
<a href="https://gitee.com/dromara/mybatis-plus-ext" target="_blank">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/mybatis-plus-ext.png"
|
||||
msg="mybatis-plus 框架的增强拓展包。">
|
||||
@@ -803,7 +899,7 @@
|
||||
msg="DyJava是一款功能强大的抖音Java开发工具包">
|
||||
</a>
|
||||
<a href="https://gitee.com/dromara/MilvusPlus" target="_blank">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/MilvusPlus.jpg"
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/MilvusPlus-logo.png"
|
||||
msg="MilvusPlus(简称 MP)是一个 Milvus 的操作工具,旨在简化与 Milvus 向量数据库的交互,为开发者提供类似 MyBatis-Plus 注解和方法调用风格的直观 API,提高效率而生。">
|
||||
</a>
|
||||
<a href="http://www.easy-query.com/easy-query-doc/" target="_blank">
|
||||
@@ -811,17 +907,33 @@
|
||||
msg="java下唯一一款同时支持强类型对象关系查询和强类型SQL语法查询的ORM,拥有对象模型筛选、隐式子查询、隐式join、显式子查询、显式join,支持Java/Kotlin">
|
||||
</a>
|
||||
<a href="https://gitee.com/dromara/orion-visor" target="_blank">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/orion-visor.png"
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/horizontal.png"
|
||||
msg="一款高颜值、现代化的智能运维&轻量堡垒机平台。">
|
||||
</a>
|
||||
<a href="https://www.ujcms.com/" target="_blank">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/ujcms.png"
|
||||
msg="Java开源网站内容管理系统(java cms)。使用SpringBoot、MyBatis、Vue3、ElementPlus、Vite、TypeScript等技术开发。">
|
||||
</a>
|
||||
<a href="https://dromara.org/zh/projects/" target="_blank">
|
||||
<a href="https://gitee.com/dromara/skyeye" target="_blank">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/skyeye-logo.png"
|
||||
msg="智能制造一体化,采用Springboot + winUI的低代码平台开发模式。包含30多个应用模块、50多种电子流程">
|
||||
</a>
|
||||
<a href="https://domain-admin.cn/" target="_blank">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/domain-admin.png"
|
||||
msg="SSL证书监测平台,申请证书,自动续签,到期提醒。">
|
||||
</a>
|
||||
<a href="https://gitee.com/dromara/carbon" target="_blank">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/carbon.png"
|
||||
msg="轻量级、语义化、对开发者友好的 golang 时间处理库">
|
||||
</a>
|
||||
<a href="https://gitee.com/dromara/mica-mqtt" target="_blank">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/mica-mqtt.png"
|
||||
msg="java mqtt 基于 java aio 实现,开源、简单、易用、低延迟、高性能百万级 java mqtt client 组件和 java mqtt broker 服务。">
|
||||
</a>
|
||||
<!-- <a href="https://dromara.org/zh/projects/" target="_blank">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/dromara.png"
|
||||
msg="让每一位开源爱好者,体会到开源的快乐。">
|
||||
</a>
|
||||
</a> -->
|
||||
</div>
|
||||
<div style="height: 10px; clear: both;"></div>
|
||||
<p>
|
||||
@@ -843,11 +955,11 @@
|
||||
<br>
|
||||
<h2 class="s-title">友情链接</h2>
|
||||
<div class="com-box com-box-you">
|
||||
<a href="https://okhttps.ejlchina.com/" target="_blank">
|
||||
<a href="https://ok.zhxu.cn/" target="_blank">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/okhttps.png"
|
||||
msg="如艺术一般优雅,像 1、2、3 一样简单,前后端通用,轻量却强大的 HTTP 客户端(同时支持 WebSocket 以及 Stomp 协议)">
|
||||
</a>
|
||||
<a href="https://searcher.ejlchina.com/" target="_blank">
|
||||
<a href="https://bs.zhxu.cn/" target="_blank">
|
||||
<img class="lazy" data-original="https://oss.dev33.cn/sa-token/link/bean-searcher.png"
|
||||
msg="轻量级关系数据库条件检索引擎,使一行代码实现复杂列表检索成为可能!">
|
||||
</a>
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
- [[ 公众号 ] sa-token之@SaIgnore注解失效的真正原因及正确姿势](https://mp.weixin.qq.com/s/c6eckHp2M4oz2x3Hea6pGg) (2025-1-15)
|
||||
|
||||
- [[ 公众号 ] 集成sa-token前后端分离部署配置corsFliter解决跨域失效的真正原因](https://mp.weixin.qq.com/s/bSS4vmKlKM7ov_CUkjxkBg) (2024-07-08)
|
||||
|
||||
- [[ 公众号 ] sa-token前后端分离解决跨域的正确姿势](https://mp.weixin.qq.com/s/96WbWL28T5_-xzyCfJ7Stg) (2024-07-06)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user