This commit is contained in:
kk
2026-07-07 18:01:21 +08:00
commit b259f94d4b
1088 changed files with 121778 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
# http://editorconfig.org
root = true
# 空格替代Tab缩进在各种编辑工具下效果一致
[*]
indent_style = space
indent_size = 4
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
[*.java]
indent_style = tab
[*.{json,yml,yaml}]
indent_size = 2
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false
+37
View File
@@ -0,0 +1,37 @@
# maven #
target
logs
# windows #
Thumbs.db
# Mac #
.DS_Store
# eclipse #
.settings
.project
.classpath
.log
*.class
# vscode #
.vscode
# idea #
.idea
*.iml
# Package Files #
*.jar
*.war
*.ear
/target
# Flattened pom
.flattened-pom.xml
/**/.flattened-pom.xml
# ai
.claude
+444
View File
@@ -0,0 +1,444 @@
# CLAUDE.md
本文件用于指导 Claude Code (claude.ai/code) 在 BladeX-Tool 核心架构工程中工作时的行为规范。
> 本规范适用于 BladeX-Tool 基础架构框架的所有开发任务,为强制性条款。除非用户显式豁免,任何条目都不得忽视或删减。
>
> 作为 AI 助手参与本项目开发时,你必须:
> - 深刻理解本项目的定位:**核心架构工程**,为上层业务工程(BladeX-Boot / BladeX-Cloud)提供基础能力
> - 每一行代码都可能被数百个下游项目引用,必须保证**向后兼容性**和**稳定性**
> - 当回答依赖外部知识时,优先查询 Spring Boot 3.x、Spring Cloud 2025、MyBatis-Plus 等官方文档
> - 引用外部资料须注明来源(链接或版本号)
> - 若需求含糊,先复述已知信息并列出关键澄清问题
> - 仅处理与问题直接相关的内容,避免冗余改动
> - 面对复杂需求,先拆分为可管理的子任务
>
> 所有开发内容必须建立在深度思考过的基础之上,禁止机械生成与错误填充。
> 如果你已了解所有规范,请在用户第一次对话时说明:"我已充分了解 BladeX 微服务平台开发规范。"
## 1. 工程定位与架构
### 1.1 工程性质
BladeX-Tool 是 BladeX 全栈微服务体系的**底层基础架构工程**,职责包括:
- 提供通用工具类、基础实体、统一响应封装等核心能力
- 封装 Spring Boot Starter 自动装配机制,实现开箱即用
- 统一技术栈版本管理(BOM 物料清单)
- 为安全认证、多租户、数据权限、分布式事务等横切关注点提供框架级支持
**本工程不包含任何业务 CRUD 逻辑,所有代码均为框架级基础设施。**
### 1.2 模块全景
```
BladeX-Tool/
├── blade-bom/ # BOM 物料清单,统一依赖版本管理
├── blade-core-tool/ # 核心工具库(100+ 工具类、API 响应封装、Jackson 配置)
├── blade-core-boot/ # Boot 单体应用基础配置(WebMvc、请求过滤、文件处理)
├── blade-core-cloud/ # Cloud 微服务基础配置(Feign、Sentinel、版本路由)
├── blade-core-launch/ # 应用启动器(属性加载、服务发现常量、启动监听)
├── blade-core-context/ # 线程上下文传递(Callable/Runnable 包装、Header 透传)
├── blade-core-auto/ # 编译期注解处理器(自动生成 spring.factories
├── blade-core-db/ # 数据库基础配置
├── blade-core-test/ # 单元测试支持(BladeBootTest、BladeSpringExtension
├── blade-core-log4j2/ # Log4j2 日志集成
├── blade-core-auth/ # 认证授权框架(聚合模块)
│ ├── blade-core-oauth2/ # OAuth2 认证实现(授权端点、令牌管理、异常处理)
│ ├── blade-core-secure/ # 安全拦截框架(权限注解、拦截器、签名认证)
│ ├── blade-starter-auth/ # 认证信息封装(AuthInfo、TokenInfo
│ ├── blade-starter-jwt/ # JWT 令牌支持(生成、解析、Redis 存储)
│ ├── blade-starter-key/ # API Key 认证支持
│ └── blade-starter-social/ # 第三方社交登录集成
├── blade-starter-actuate/ # HTTP 响应缓存
├── blade-starter-api-crypto/ # API 报文加解密(AES / DES / RSA
├── blade-starter-cache/ # 缓存管理工具
├── blade-starter-data-record/ # 数据变更审计记录
├── blade-starter-data-scope/ # 行级数据权限控制
├── blade-starter-db-dynamic/ # 动态数据源路由
├── blade-starter-develop/ # 代码生成器支持
├── blade-starter-ehcache/ # EHCache 本地缓存
├── blade-starter-excel/ # Excel 导入导出(FastExcel
├── blade-starter-flowable/ # Flowable 工作流引擎集成
├── blade-starter-holidays/ # 节假日判定 API
├── blade-starter-http/ # HTTP 客户端封装(OkHttp3
├── blade-starter-i18n/ # 国际化支持
├── blade-starter-liteflow/ # LiteFlow 流程编排引擎
├── blade-starter-literule/ # LiteRule 规则引擎
├── blade-starter-loadbalancer/ # 负载均衡与灰度发布
├── blade-starter-log/ # 日志框架(请求日志、错误日志、ELK、链路追踪)
├── blade-starter-metrics/ # 监控指标采集(Druid、Undertow、Sentinel
├── blade-starter-mongo/ # MongoDB 集成
├── blade-starter-mybatis/ # MyBatis-Plus 集成(基础 Service / Mapper / Entity
├── blade-starter-mybatis-encrypt/ # 字段级透明加解密(AES / DES / SM4
├── blade-starter-oss/ # 对象存储(Minio / 阿里云 / 腾讯云 / 七牛 / 华为云 / S3)
├── blade-starter-powerjob/ # PowerJob 分布式任务调度
├── blade-starter-prometheus/ # Prometheus 监控集成
├── blade-starter-redis/ # Redis 全功能集成(缓存 / 分布式锁 / 限流 / 发布订阅 / Stream
├── blade-starter-report/ # UReport2 报表引擎
├── blade-starter-sharding/ # ShardingSphere 分库分表
├── blade-starter-sms/ # 短信服务(阿里云 / 腾讯云 / 七牛 / 云片)
├── blade-starter-swagger/ # Knife4j API 文档
├── blade-starter-tenant/ # SaaS 多租户支持
├── blade-starter-tenant-dynamic/ # 动态租户数据源路由
├── blade-starter-trace/ # 分布式链路追踪
├── blade-starter-transaction/ # Seata 分布式事务
├── blade-starter-xss/ # XSS 攻击防护
└── pom.xml # Maven 父工程配置
```
### 1.3 核心理解要点
1. **blade-core-auto 是编译期注解处理器**:通过 `@AutoService` 等注解在编译期自动生成 `META-INF/spring.factories``AutoConfiguration.imports`,无需手动维护
2. **blade-bom 统一版本管理**:所有第三方依赖版本在此声明,下游工程通过 `<dependencyManagement>` 引入
3. **blade-core-tool 是最底层模块**:几乎所有其他模块都依赖它,修改时需格外谨慎
4. **blade-core-boot 与 blade-core-cloud 互斥**:分别用于单体应用和微服务应用,不可同时引入
5. **Starter 模块遵循 Spring Boot 自动装配机制**:通过 `@AutoConfiguration` + `@ConditionalOnXxx` 实现按需加载
## 2. 技术栈
| 技术 | 版本 | 说明 |
|------|------|------|
| Java | 17 | 最低要求,可使用至 Java 17 的所有语法特性 |
| Spring Framework | 6.2.15 | 核心框架 |
| Spring Boot | 3.5.9 | 应用框架 |
| Spring Cloud | 2025.0.1 | 微服务框架 |
| Spring Cloud Alibaba | 2025.0.0.0 | 阿里云微服务组件 |
| MyBatis-Plus | 3.5.14 | ORM 框架 |
| Knife4j | 4.5.0 | API 文档(基于 OpenAPI 3 |
| Jackson | 2.18.0 | JSON 序列化 |
| OkHttp3 | 4.12.0 | HTTP 客户端 |
| JJWT | 0.13.0 | JWT 令牌 |
| Lombok | 1.18.42 | 代码简化 |
| LiteFlow | 2.11.3 | 流程编排 |
**构建工具**Maven(使用 `flatten-maven-plugin` 管理 `${revision}` 版本号)
## 3. 编码规范
### 3.1 文件头部许可声明
所有 Java 文件必须包含以下商业许可头部(位于 package 声明之前):
```java
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
```
### 3.2 格式规范
| 项目 | 规范 | 来源 |
|------|------|------|
| Java 缩进 | **Tab 字符**(非空格) | .editorconfig |
| JSON/YAML 缩进 | 2 空格 | .editorconfig |
| 文件编码 | UTF-8 | .editorconfig |
| 换行符 | LFUnix | .editorconfig |
| 尾部空白 | 自动去除 | .editorconfig |
| 文件末尾 | 保留一个空行 | .editorconfig |
| 行宽 | 建议不超过 120 字符 | 约定 |
### 3.3 命名规范
1. **包名**:统一前缀 `org.springblade.core.[模块名]`
- 示例:`org.springblade.core.mp.base``org.springblade.core.redis.cache``org.springblade.core.xss.processor`
2. **类名**
- 配置类:`XxxConfiguration``XxxAutoConfiguration`
- 属性类:`XxxProperties`(配合 `@ConfigurationProperties`
- 工具类:`XxxUtil`(如 `WebUtil``StringUtil``SecureUtil`
- 模板类:`XxxTemplate`(如 `OssTemplate``SmsTemplate``BladeRedis`
- 拦截器:`XxxInterceptor`(如 `AuthInterceptor``XssInterceptor`
- 处理器:`XxxHandler`(如 `BladeSecureHandler``TokenHandler`
- 常量接口:`XxxConstant`(如 `BladeConstant``TokenConstant`
- 基础实体:`BaseEntity``BizEntity``TenantEntity`
- 基础服务:`BaseService` / `BaseServiceImpl``BladeService` / `BladeServiceImpl`
- 注解:描述性命名(如 `@PreAuth``@DataAuth``@FieldEncrypt``@XssIgnore`
3. **变量与方法**
- 必须具有明确语义,禁止无意义缩写
-`Exception chatProcessException``LambdaQueryWrapper<User> queryWrapper`
-`Exception e``LambdaQueryWrapper<User> w`
- 命名冲突时提升语义层级,禁止数字后缀(`cache1``cache2`
4. **常量**`UPPER_SNAKE_CASE`,如 `CONTEXT_KEY``DB_PRIMARY_KEY`
### 3.4 注解使用规范
**类级别注解的推荐顺序**
```java
@Slf4j // Lombok 日志
@AutoConfiguration // Spring Boot 3 自动配置(取代 @Configuration
@RequiredArgsConstructor // Lombok 构造器注入
@EnableConfigurationProperties(XxxProperties.class)
@ConditionalOnProperty(...) // 条件装配
@ConditionalOnWebApplication(type = Type.SERVLET)
public class XxxConfiguration {
```
**字段级别注解的推荐顺序(实体类)**
```java
@JsonSerialize(using = ToStringSerializer.class) // 序列化
@Schema(description = "主键id") // OpenAPI 文档
@TableId(value = "id", type = IdType.ASSIGN_ID) // MyBatis-Plus
@BladeView(Views.Admin.class) // 视图控制
private Long id;
```
### 3.5 Lombok 规范
- 使用 `@Data` 替代手写 getter/setter
- 使用 `@RequiredArgsConstructor` 配合 `final` 字段实现构造器注入,优先于 `@Autowired`
- 使用 `@Slf4j` 注入日志对象
- 实体类使用 `@Data`,配置类使用 `@RequiredArgsConstructor``@AllArgsConstructor`
### 3.6 Java 语言特性
1. 基于 Java 17,可使用以下特性:
- 增强 switch 表达式(Java 12+
- Text Blocks 文本块(Java 13+
- Pattern Matching for instanceofJava 16+
- Sealed Classes 密封类(Java 17
- Records 记录类(Java 16+,视场景使用)
2. **禁止使用 `var` / `val`**,所有变量必须显式声明类型
3. 数据处理优先使用 Stream API,避免传统 for 循环
4. Lambda 保持简洁:
-`() -> "something"``list.forEach(Template::shutdown)`
-`(x) -> { doSomething(x); }``() -> { return "something"; }`
### 3.7 Import 规范
- 禁止使用通配符导入(`import xxx.*`
- 导入分组顺序(组间空行分隔):
1. `jakarta.*` / `javax.*`
2. 第三方库(`com.*``io.*``net.*`
3. `org.springframework.*`
4. `org.springblade.*`
5. `java.*`
### 3.8 Javadoc 规范
```java
/**
* 统一API响应结果封装
*
* @author Chill
*/
```
- 类注释:简洁的一行中文描述 + `@author` 标签
- 方法注释:中文描述 + `@param` + `@return`(工具方法可用英文)
- 字段注释:使用 `/** */` 单行 Javadoc
- 不写显而易见的注释,不添加 `@since``@version` 等冗余标签
## 4. 框架开发规范
### 4.1 自动装配模块开发模式
创建新的 Starter 模块时,遵循以下结构:
```
blade-starter-xxx/
├── src/main/java/org/springblade/core/xxx/
│ ├── config/ # 自动配置类(@AutoConfiguration
│ ├── props/ # 配置属性类(@ConfigurationProperties
│ ├── annotation/ # 自定义注解
│ ├── aspect/ # AOP 切面
│ ├── interceptor/ # 拦截器
│ ├── handler/ # 处理器接口与默认实现
│ ├── constant/ # 常量定义
│ ├── exception/ # 异常定义
│ └── utils/ # 工具类
└── pom.xml
```
**关键要求**
- 配置类使用 `@AutoConfiguration`Spring Boot 3 规范,取代 `@Configuration`
- 必须配合 `@ConditionalOnXxx` 注解实现条件装配,避免强制加载
- 提供 `@ConditionalOnMissingBean` 允许下游覆盖默认实现
- 属性类使用 `@ConfigurationProperties` 并提供合理默认值
- 通过 `blade-core-auto` 注解处理器自动生成 `spring.factories`,无需手动维护
### 4.2 自动配置类示例
```java
@Slf4j
@AutoConfiguration
@RequiredArgsConstructor
@EnableConfigurationProperties(XxxProperties.class)
@ConditionalOnProperty(value = "blade.xxx.enabled", havingValue = "true", matchIfMissing = true)
public class XxxConfiguration {
@Bean
@ConditionalOnMissingBean
public XxxTemplate xxxTemplate(XxxProperties properties) {
return new XxxTemplate(properties);
}
}
```
### 4.3 工具类开发规范
```java
/**
* Xxx工具类
*
* @author Chill
*/
public class XxxUtil {
/**
* 工具方法说明
*
* @param param 参数说明
* @return 返回值说明
*/
public static String doSomething(@Nullable String param) {
// 实现逻辑
}
}
```
- 工具类通常为 `final class` 或含静态方法的普通类
- 可继承 Spring 已有工具类进行扩展(如 `WebUtil extends org.springframework.web.util.WebUtils`
- 参数使用 `@Nullable` 标注可空性
- 优先复用 `blade-core-tool` 中已有的工具方法,禁止重复造轮子
### 4.4 常量定义规范
```java
/**
* 系统常量
*
* @author Chill
*/
public interface BladeConstant {
String UTF_8 = "UTF-8";
String CONTEXT_KEY = "bladeContext";
String DB_PRIMARY_KEY = "id";
}
```
- 使用 `interface` 定义常量(非 `class` + `static final`),便于直接 `implements` 引用
- 按业务领域分组到不同的常量接口
### 4.5 向后兼容性要求
作为核心架构工程,以下变更属于 **Breaking Change**,须特别谨慎:
- 删除或重命名 public 类、方法、字段
- 变更方法签名(参数类型、返回类型、异常声明)
- 修改配置属性 key`blade.xxx.yyy`
- 变更 Bean 名称或条件装配逻辑
- 升级依赖的 major 版本
若必须进行 Breaking Change,需在 commit message 中明确标注影响范围。
## 5. 日志规范
### 5.1 基本要求
- 使用 Lombok `@Slf4j` 注入日志对象
- 使用占位符 `{}` 而非字符串拼接
- 包含关键业务标识(如 tenantId、userId、requestId 等)
- 异常日志必须包含堆栈信息(异常对象作为最后一个参数)
- 禁止打印敏感信息(API Key、密码、令牌等)
### 5.2 示例
```java
@Slf4j
public class TokenGranterFactory {
public TokenGranter getGranter(String grantType) {
TokenGranter granter = granterMap.get(grantType);
if (granter == null) {
log.error("不支持的授权类型: grantType={}", grantType);
throw new GranterInvalidException("不支持的授权类型");
}
log.info("获取令牌授权器: grantType={}, granterClass={}", grantType, granter.getClass().getSimpleName());
return granter;
}
}
```
## 6. 编译与验证
### 6.1 开发完成后必须执行
```bash
mvn clean compile -DskipTests
```
### 6.2 编译规则
1. 编译出现错误必须修复后重新编译,直至通过
2. 引入模块间依赖前,必须检查是否会造成**循环依赖**
3. 若存在循环依赖风险,采用接口抽象、事件机制等方式规避
4. 编译通过后,将完整测试流程交由用户执行,由用户反馈日志进行进一步调整
5. **不得自行执行任何集成测试或启动应用**
6. 除非用户明确要求,不撰写示例代码或额外文档
## 7. 自主学习与风格一致性
1. **模仿优先**:遇到风格不确定的场景,优先查阅同模块现有代码并模仿
2. **查找复用**:编写新功能前,先在 `blade-core-tool` 和相关 Starter 中搜索是否已有可复用的类或方法
3. **禁止替代**:若现有模块已满足需求,禁止自写替代实现
4. **标记不确定性**:确认已学习现有代码后仍不确定的,使用 `// TODO: 确认命名规范` 标记并告知用户
5. **最小改动原则**:修改核心模块时,改动范围必须最小化,避免影响无关功能
## 8. Git 提交规范
### 8.1 提交格式
本项目使用 **Gitmoji** 风格,与现有提交历史保持一致:
```
:gitmoji: 简要描述(中文)
```
### 8.2 常用 Gitmoji
| Emoji | Code | 场景 |
|-------|------|------|
| :sparkles: | `:sparkles:` | 新增功能 / 新增类 |
| :zap: | `:zap:` | 性能优化 / 逻辑优化 |
| :bug: | `:bug:` | 修复 Bug |
| :recycle: | `:recycle:` | 重构代码 |
| :memo: | `:memo:` | 文档变更 |
| :art: | `:art:` | 代码格式化 / 结构调整 |
| :tada: | `:tada:` | 版本发布 |
| :fire: | `:fire:` | 删除代码或文件 |
| :lock: | `:lock:` | 安全相关修复 |
| :arrow_up: | `:arrow_up:` | 升级依赖版本 |
| :wrench: | `:wrench:` | 配置文件变更 |
| :white_check_mark: | `:white_check_mark:` | 测试相关 |
### 8.3 示例
```
:sparkles: 新增BladeView视图序列化机制,支持按角色动态控制JSON输出字段
:zap: 优化动态数据源加载驱动类逻辑
:bug: 修复多租户场景下缓存隔离失效问题
:arrow_up: 升级Spring Boot至3.5.9
```
## 9. 单元测试规范
```java
@ExtendWith(BladeSpringExtension.class)
@BladeBootTest(appName = "blade-test", enableLoader = true)
public class XxxTest {
@Autowired
private XxxService xxxService;
@Test
public void testXxx() {
// 测试逻辑
}
}
```
- 测试类使用 `blade-core-test` 提供的 `@BladeBootTest` 注解
- 测试方法名必须具有描述性
## 10. 交互规范
1. 与用户交互时全程使用**中文**
2. 若用户指定切换语言则遵从,否则始终保持中文
3. 代码注释、Javadoc 使用中文(工具类方法可用英文)
4. commit message 使用中文描述
+35
View File
@@ -0,0 +1,35 @@
BladeX商业授权许可协议
一、 知识产权:
BladeX系列产品知识产权归上海布雷德科技有限公司独立所有
二、 许可:
1. 在您完全接受并遵守本协议的基础上,本协议授予您使用BladeX的某些权利和非独占性许可。
2. 本协议中,将本产品使用用途分为"专业版用途"和"企业版用途"。
3. "专业版用途"定义:指个人在非团体机构中出于任何合法目的使用本产品(任何目的包括商业目的或非盈利目的)。
4. "企业版用途"定义:指拥有合法执照的团体机构(例如公司企业、政府、学校、军队、医院、社会团体等各类组织)(不包含集团,若集团使用则需为各个子公司分别购买企业授权)出于任何合法目的使用本产品(任何目的包括商业目的或非盈利目的)。
5. 若您不能以拥有合法执照的团体机构名义购买企业版,则视为个人名义购买,仅可行使专业版用途。在遵守此协议的前提下,后续有一次机会将企业版授权免费绑定至法人为购买人的新公司,并从专业版用途转为企业版用途。
三、 约束和限制:
1. 本产品只能由您为本协议许可的目的而使用,您不得透露给任何第三方;
2. 从本产品取得的任何信息、软件、产品或服务,您不得对其进行修改、改编或基于以上内容创建同种类别的衍生产品并售卖。
3. 您不得对本产品以及与之关联的商业授权进行发布、出租、销售、分销、抵押、转让、许可或发放子许可证。
4. 本产品商业授权版可能包含一些独立功能或特性,这些功能只有在您购买商业授权后才可以使用。在未取得商业授权的情况下,您不得使用、尝试使用或复制这些授权版独立功能。
5. 若您的客户要求以源码方式交付软件,需缴纳企业版授权费用,否则本产品部分不得提供源码。
四、 不得用于非法或禁止的用途:
您在使用本产品或服务时,不得将本产品产品或服务用于任何非法用途或本协议条款、条件和声明禁止的用途。
五、 免责说明:
1. 本产品按"现状"授予许可,您须自行承担使用本产品的风险。BladeX团队不对此提供任何明示、暗示或任何其它形式的担保和表示。在任何情况下,对于因使用或无法使用本软件而导致的任何损失(包括但不仅限于商业利润损失、业务中断或业务信息丢失),BladeX团队无需向您或任何第三方负责,即使BladeX团队已被告知可能会造成此类损失。在任何情况下, BladeX团队均不就任何直接的、间接的、附带的、后果性的、特别的、惩戒性的和处罚性的损害赔偿承担任何责任,无论该主张是基于保证、合同、侵权(包括疏忽)或是基于其他原因作出。
2. 本产品可能内置有第三方服务,您应自行评估使用这些第三方服务的风险,由使用此类第三方服务而产生的纠纷,全部责任由您自行承担。
3. BladeX团队不对使用本产品构建的网站中任何信息内容以及导致的任何版权纠纷、法律争议和后果承担任何责任,全部责任由您自行承担。
4. BladeX团队可能会经常提供产品更新或升级,但BladeX团队没有为根据本协议许可的产品提供维护或更新的责任。
5. BladeX团队可能会按照官方制定的答疑规则为您进行答疑,但BladeX团队没有为根据本协议许可的产品提供技术支持的义务或责任。
六、 权利和所有权的保留:
BladeX团队保留所有未在本协议中明确授予您的所有权利。BladeX团队保留随时更新本协议的权利,并只需公示于对应产品项目的LICENSE文件,无需征得您的事先同意且无需另行通知,更新后的内容应于公示即时生效。您可以随时访问产品地址并查阅最新版许可条款,在更新生效后您继续使用本产品则被视作您已接受了新的条款。
七、 协议终止
1. 您一旦开始复制、下载、安装或者使用本产品,即被视为完全理解并接受本协议的各项条款,在享有上述条款授予的许可权力同时,也受到相关的约束和限制,本协议许可范围以外的行为,将直接违反本协议并构成侵权。
2. 一旦您违反本协议的条款,BladeX团队随时可能终止本协议、收回许可和授权,并要求您承担相应法律和经济责任。
+43
View File
@@ -0,0 +1,43 @@
## 版权声明
* BladeX是一个商业化软件,系列产品知识产权归**上海布雷德科技有限公司**独立所有
* 您一旦开始复制、下载、安装或者使用本产品,即被视为完全理解并接受本协议的各项条款
* 更多详情请看:[BladeX商业授权许可协议](https://license.bladex.cn)
## 答疑流程
>1. 遇到问题或Bug
>2. 业务型问题打断点调试尝试找出问题所在
>3. 系统型问题通过百度、谷歌、社区查找解决方案
>4. 未解决问题则进入技术社区进行发帖提问:[https://sns.bladex.cn](https://sns.bladex.cn)
>5. 将帖子地址发至商业群,特别简单三言两语就能描述清楚的也可在答疑时间内发至商业群提问
>6. 发帖的时候一定要描述清楚,详细描述遇到问题的**重现步骤**、**报错详细信息**、**相关代码与逻辑**、**使用软件版本**以及**操作系统版本**,否则随意发帖提问将会提高我们的答疑难度。
## 答疑时间
* 工作日:9:00 ~ 17:00 提供答疑,周末、节假日休息,暂停答疑
* 请勿**私聊提问**,以免被其他用户的消息覆盖从而无法获得答疑
* 答疑时间外遇到问题可以将问题发帖至[技术社区](https://sns.bladex.cn),我们后续会逐个回复
## 授权范围
* 专业版:只可用于**个人学习**及**个人私活**项目,不可用于公司或团队,不可泄露给任何第三方
* 企业版:可用于**企业名下**的任何项目,企业版员工在**未购买**专业版授权前,只授权开发**所在授权企业名下**的项目,**不得将BladeX用于个人私活**
* 共同遵守:若甲方需要您提供项目源码,则需代为甲方购买BladeX企业授权,甲方购买后续的所有项目都无需再次购买授权
## 商用权益
* ✔️ 遵守[商业协议](https://license.bladex.cn)的前提下,将BladeX系列产品用于授权范围内的商用项目,并上线运营
* ✔️ 遵守[商业协议](https://license.bladex.cn)的前提下,不限制项目数,不限制服务器数
* ✔️ 遵守[商业协议](https://license.bladex.cn)的前提下,将自行编写的业务代码申请软件著作权
## 何为侵权
* ❌ 不遵守商业协议,私自销售商业源码
* ❌ 以任何理由将BladeX源码用于申请软件著作权
* ❌ 将商业源码以任何途径任何理由泄露给未授权的单位或个人
* ❌ 开发完毕项目,没有为甲方购买企业授权,向甲方提供了BladeX代码
* ❌ 基于BladeX拓展研发与BladeX有竞争关系的衍生框架,并将其开源或销售
## 侵权后果
* 情节较轻:第一次发现警告处理
* 情节较重:封禁账号,踢出商业群,并保留追究法律责任的权利
* 情节严重:与本地律师事务所合作,以公司名义起诉侵犯计算机软件著作权
## 举报有奖
* 向官方提供有用线索并成功捣毁盗版个人或窝点,将会看成果给予 500~10000 不等的现金奖励
* 官方唯一指定QQ1272154962
+42
View File
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>
<parent>
<artifactId>BladeX-Tool</artifactId>
<groupId>org.springblade</groupId>
<version>${revision}</version>
</parent>
<groupId>org.springblade.platform</groupId>
<artifactId>blade-bom</artifactId>
<packaging>pom</packaging>
<description>bladex统一版本配置</description>
<properties>
<module.name>org.springblade.blade.bom</module.name>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>${maven.flatten.version}</version>
<configuration>
<updatePomFile>true</updatePomFile>
<flattenMode>oss</flattenMode>
<pomElements>
<dependencyManagement>expand</dependencyManagement>
<pluginManagement>remove</pluginManagement>
<dependencies>remove</dependencies>
<properties>remove</properties>
<repositories>remove</repositories>
</pomElements>
</configuration>
</plugin>
</plugins>
</build>
</project>
+58
View File
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>
<parent>
<artifactId>blade-core-auth</artifactId>
<groupId>org.springblade</groupId>
<version>${revision}</version>
</parent>
<artifactId>blade-core-oauth2</artifactId>
<name>${project.artifactId}</name>
<version>${project.parent.version}</version>
<packaging>jar</packaging>
<properties>
<module.name>org.springblade.blade.core.oauth2</module.name>
</properties>
<dependencies>
<!--Blade-->
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-core-secure</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-social</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-redis</artifactId>
</dependency>
<!-- captcha -->
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
</dependency>
<!-- session -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Auto -->
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-core-auto</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,470 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.behavior;
import org.springblade.core.oauth2.constant.OAuth2TokenConstant;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import java.awt.*;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Base64;
import java.util.concurrent.ThreadLocalRandom;
/**
* 行为验证码生成器基类
*
* <p>所有图片按逻辑尺寸的整数倍超采样渲染,客户端以逻辑尺寸展示,保证高像素密度屏幕下依然清晰锐利;
* 坐标契约始终使用逻辑尺寸,渲染倍率对调用方透明。底图完全由代码生成,不依赖内置图片资源,
* 由「同色系主题 × 构图风格」两个维度随机组合:主题决定配色,风格决定纹理形态,
* 依赖纹理连续性做对齐线索的模式可限定风格池。</p>
*
* @author Chill
*/
public abstract class AbstractBehaviorGenerator implements BehaviorGenerator {
/**
* 超采样渲染倍率,兼顾高像素密度屏幕清晰度与图片体积
*/
protected static final int RENDER_SCALE = 2;
/**
* JPEG 压缩质量,低于该值时渐变底图会出现明显色带
*/
private static final float JPEG_QUALITY = 0.9F;
/**
* 同色系主题池:{渐变起色, 渐变止色, 光斑色A, 光斑色B, 流线色A, 流线色B, 流线色C}
*/
private static final Color[][] THEMES = {
// 深海蓝
{new Color(0x1B, 0x33, 0x58), new Color(0x0C, 0x17, 0x2B), new Color(0x3E, 0x92, 0xCC), new Color(0x2C, 0x5F, 0x8A),
new Color(0x7F, 0xD8, 0xF5), new Color(0x4F, 0xA3, 0xD9), new Color(0x9A, 0xD1, 0xD4)},
// 暮紫
{new Color(0x33, 0x1F, 0x54), new Color(0x17, 0x0F, 0x2E), new Color(0x7C, 0x5C, 0xBF), new Color(0x4B, 0x3F, 0x72),
new Color(0xB8, 0xA1, 0xE3), new Color(0x8F, 0x6E, 0xD5), new Color(0x6E, 0x8F, 0xF0)},
// 松石绿
{new Color(0x14, 0x40, 0x3C), new Color(0x0A, 0x1F, 0x1E), new Color(0x2E, 0xC4, 0xB6), new Color(0x1B, 0x6F, 0x66),
new Color(0x7B, 0xE0, 0xD3), new Color(0x3D, 0xB8, 0xA5), new Color(0xA8, 0xE6, 0xCF)},
// 绛暮红
{new Color(0x46, 0x1D, 0x3D), new Color(0x20, 0x0E, 0x1D), new Color(0xC8, 0x6B, 0x98), new Color(0x8A, 0x3F, 0x66),
new Color(0xF0, 0xA6, 0xC2), new Color(0xD4, 0x7F, 0xA6), new Color(0xE8, 0xC1, 0x9A)},
// 石墨蓝灰
{new Color(0x25, 0x2F, 0x45), new Color(0x12, 0x17, 0x24), new Color(0x5C, 0x7A, 0xEA), new Color(0x3A, 0x4A, 0x7A),
new Color(0x9B, 0xB5, 0xF0), new Color(0x6C, 0x8A, 0xE8), new Color(0x8F, 0xD6, 0xE8)},
// 琥珀棕
{new Color(0x4A, 0x34, 0x21), new Color(0x1F, 0x15, 0x0C), new Color(0xC9, 0x8A, 0x3B), new Color(0x8A, 0x5A, 0x28),
new Color(0xE8, 0xB2, 0x5E), new Color(0xD1, 0x9A, 0x4A), new Color(0xF0, 0xD0, 0xA0)},
// 橄榄绿
{new Color(0x3A, 0x42, 0x22), new Color(0x17, 0x1C, 0x0E), new Color(0x8A, 0x9A, 0x4A), new Color(0x5A, 0x6B, 0x2E),
new Color(0xC0, 0xD0, 0x7A), new Color(0x9A, 0xB0, 0x55), new Color(0xD8, 0xE0, 0xA8)},
// 酒红金
{new Color(0x4A, 0x1F, 0x28), new Color(0x20, 0x0D, 0x12), new Color(0xC0, 0x5A, 0x6A), new Color(0xB0, 0x8A, 0x3A),
new Color(0xE8, 0x9A, 0xA8), new Color(0xD4, 0xB0, 0x60), new Color(0xF0, 0xC8, 0xD0)},
// 青橙对比
{new Color(0x14, 0x38, 0x3F), new Color(0x0A, 0x1C, 0x20), new Color(0x2E, 0x9C, 0xB8), new Color(0xE0, 0x7A, 0x3A),
new Color(0x6E, 0xD0, 0xE8), new Color(0xF0, 0xA4, 0x68), new Color(0xA8, 0xE0, 0xD8)},
// 紫金
{new Color(0x38, 0x25, 0x4E), new Color(0x17, 0x0F, 0x22), new Color(0x8A, 0x6A, 0xCF), new Color(0xC0, 0xA0, 0x50),
new Color(0xB8, 0x9A, 0xE8), new Color(0xE0, 0xC8, 0x78), new Color(0xD0, 0xB8, 0xF0)},
};
/**
* 底图构图风格
*
* <p>依赖纹理连续性做对齐线索的模式(旋转还原、拼接还原)应限定使用具备贯穿性线条的风格。</p>
*/
protected enum ArtStyle {
/**
* 流线:贯穿画布的贝塞尔曲线
*/
FLOW,
/**
* 波纹:多组正弦波浪线
*/
WAVE,
/**
* 几何:低多边形三角拼接
*/
GEOMETRIC,
/**
* 光点:光点群与星轨弧线
*/
GLOW_DOTS,
/**
* 网格:斜向细网格
*/
GRID
}
/**
* 绘制超采样底图,物理像素为逻辑尺寸的渲染倍率倍
*
* @param width 逻辑宽度
* @param height 逻辑高度
* @return 底图
*/
protected static BufferedImage createArtBackground(int width, int height) {
return createArtBackground(width, height, randomTheme(), ArtStyle.values());
}
/**
* 以指定主题绘制超采样底图,供需要与底图配色联动的前景元素复用同一主题
*
* @param width 逻辑宽度
* @param height 逻辑高度
* @param theme 主题色组
* @return 底图
*/
protected static BufferedImage createArtBackground(int width, int height, Color[] theme) {
return createArtBackground(width, height, theme, ArtStyle.values());
}
/**
* 以指定主题与风格池绘制超采样底图,风格从池中随机选取
*
* @param width 逻辑宽度
* @param height 逻辑高度
* @param theme 主题色组
* @param styles 可选构图风格池
* @return 底图
*/
protected static BufferedImage createArtBackground(int width, int height, Color[] theme, ArtStyle... styles) {
BufferedImage image = new BufferedImage(width * RENDER_SCALE, height * RENDER_SCALE, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics = scaledGraphics(image);
// 同色系斜向渐变与柔光斑构成共通基调,营造空间纵深
graphics.setPaint(new GradientPaint(0, 0, theme[0], width, height, theme[1]));
graphics.fillRect(0, 0, width, height);
int glowCount = randomInt(3, 5);
for (int i = 0; i < glowCount; i++) {
Color glow = theme[2 + randomInt(0, 2)];
int radius = randomInt(Math.min(width, height) / 3, Math.min(width, height));
Point2D center = new Point2D.Double(randomInt(0, width), randomInt(0, height));
graphics.setPaint(new RadialGradientPaint(center, radius, new float[]{0F, 1F},
new Color[]{withAlpha(glow, randomInt(36, 66)), withAlpha(glow, 0)}));
graphics.fillOval((int) center.getX() - radius, (int) center.getY() - radius, radius * 2, radius * 2);
}
// 风格纹理层决定底图形态
ArtStyle style = styles[ThreadLocalRandom.current().nextInt(styles.length)];
switch (style) {
case FLOW -> drawFlowStyle(graphics, width, height, theme);
case WAVE -> drawWaveStyle(graphics, width, height, theme);
case GEOMETRIC -> drawGeometricStyle(graphics, width, height, theme);
case GLOW_DOTS -> drawGlowDotsStyle(graphics, width, height, theme);
case GRID -> drawGridStyle(graphics, width, height, theme);
}
// 极细微噪点提升机器识别成本,密度与透明度均克制以免显脏
int noiseCount = width * height / 600;
for (int i = 0; i < noiseCount; i++) {
graphics.setColor(new Color(255, 255, 255, randomInt(10, 32)));
graphics.fillRect(randomInt(0, width), randomInt(0, height), 1, 1);
}
drawWatermark(graphics, width, height);
graphics.dispose();
return image;
}
/**
* 流线风格:贯穿画布的贝塞尔曲线
*/
private static void drawFlowStyle(Graphics2D graphics, int width, int height, Color[] theme) {
int curveCount = randomInt(3, 6);
for (int i = 0; i < curveCount; i++) {
graphics.setColor(withAlpha(themeLine(theme), randomInt(70, 130)));
graphics.setStroke(new BasicStroke(randomInt(2, 4) + 0.5F, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
graphics.draw(new CubicCurve2D.Double(
randomInt(-40, 20), randomInt(0, height),
randomInt(0, width), randomInt(-50, height + 50),
randomInt(0, width), randomInt(-50, height + 50),
randomInt(width - 20, width + 40), randomInt(0, height)));
}
}
/**
* 波纹风格:多组横贯画布的正弦波浪线,同组内相位递进形成波纹束
*/
private static void drawWaveStyle(Graphics2D graphics, int width, int height, Color[] theme) {
int groupCount = randomInt(3, 5);
for (int i = 0; i < groupCount; i++) {
Color line = themeLine(theme);
int baseY = randomInt(0, height);
double amplitude = randomInt(8, 26);
double wavelength = randomInt(70, 160);
double phase = randomInt(0, 360) * Math.PI / 180;
int lineCount = randomInt(2, 4);
for (int j = 0; j < lineCount; j++) {
graphics.setColor(withAlpha(line, randomInt(50, 110)));
graphics.setStroke(new BasicStroke(randomInt(1, 3) + 0.5F, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
Path2D wave = new Path2D.Double();
int offsetY = baseY + j * randomInt(8, 16);
wave.moveTo(-10, offsetY + amplitude * Math.sin(phase));
for (int x = 0; x <= width + 10; x += 6) {
wave.lineTo(x, offsetY + amplitude * Math.sin(x / wavelength * Math.PI * 2 + phase));
}
graphics.draw(wave);
}
}
}
/**
* 几何风格:抖动网格三角化的低多边形拼接,颜色沿渐变基调作亮度扰动
*/
private static void drawGeometricStyle(Graphics2D graphics, int width, int height, Color[] theme) {
int cell = randomInt(46, 70);
int cols = width / cell + 2;
int rows = height / cell + 2;
// 网格点带随机抖动,三角形因此各不相同
Point[][] points = new Point[rows + 1][cols + 1];
for (int row = 0; row <= rows; row++) {
for (int col = 0; col <= cols; col++) {
int jitter = cell / 3;
points[row][col] = new Point(
col * cell - cell / 2 + randomInt(-jitter, jitter + 1),
row * cell - cell / 2 + randomInt(-jitter, jitter + 1));
}
}
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
Point p1 = points[row][col];
Point p2 = points[row][col + 1];
Point p3 = points[row + 1][col];
Point p4 = points[row + 1][col + 1];
if (randomInt(0, 2) == 0) {
fillShadedTriangle(graphics, width, height, theme, p1, p2, p3);
fillShadedTriangle(graphics, width, height, theme, p2, p4, p3);
} else {
fillShadedTriangle(graphics, width, height, theme, p1, p2, p4);
fillShadedTriangle(graphics, width, height, theme, p1, p4, p3);
}
}
}
}
/**
* 按三角形重心在渐变轴上的位置插值主题基色,叠加亮度扰动后半透明填充,保留底层光斑的透光感
*/
private static void fillShadedTriangle(Graphics2D graphics, int width, int height, Color[] theme, Point p1, Point p2, Point p3) {
double ratio = Math.min(1.0, Math.max(0.0,
((p1.x + p2.x + p3.x) / 3.0 / width + (p1.y + p2.y + p3.y) / 3.0 / height) / 2));
int shift = randomInt(-14, 15);
Color base = blend(theme[0], theme[1], ratio);
graphics.setColor(new Color(
clampChannel(base.getRed() + shift), clampChannel(base.getGreen() + shift), clampChannel(base.getBlue() + shift), 150));
Path2D triangle = new Path2D.Double();
triangle.moveTo(p1.x, p1.y);
triangle.lineTo(p2.x, p2.y);
triangle.lineTo(p3.x, p3.y);
triangle.closePath();
graphics.fill(triangle);
}
/**
* 光点风格:大小错落的光点群与少量星轨弧线
*/
private static void drawGlowDotsStyle(Graphics2D graphics, int width, int height, Color[] theme) {
int dotCount = randomInt(22, 36);
for (int i = 0; i < dotCount; i++) {
Color dot = randomInt(0, 3) == 0 ? Color.WHITE : themeLine(theme);
int size = randomInt(2, 7);
graphics.setColor(withAlpha(dot, randomInt(50, 150)));
graphics.fillOval(randomInt(0, width), randomInt(0, height), size, size);
}
int arcCount = randomInt(1, 3);
for (int i = 0; i < arcCount; i++) {
graphics.setColor(withAlpha(themeLine(theme), randomInt(50, 90)));
graphics.setStroke(new BasicStroke(1.5F, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
int radius = randomInt(width / 2, width * 2);
graphics.drawArc(randomInt(-radius, width - radius), randomInt(-radius, height - radius),
radius * 2, radius * 2, randomInt(0, 360), randomInt(40, 120));
}
}
/**
* 网格风格:整体旋转的斜向细网格
*/
private static void drawGridStyle(Graphics2D graphics, int width, int height, Color[] theme) {
Graphics2D scoped = (Graphics2D) graphics.create();
scoped.rotate(Math.toRadians(randomInt(12, 34)), width / 2.0, height / 2.0);
int spacing = randomInt(16, 26);
Color line = themeLine(theme);
scoped.setStroke(new BasicStroke(1F));
// 旋转后网格须超出画布覆盖四角
int reach = width + height;
for (int x = -reach; x <= reach; x += spacing) {
scoped.setColor(withAlpha(line, randomInt(22, 46)));
scoped.drawLine(x, -reach, x, reach);
}
for (int y = -reach; y <= reach; y += spacing) {
scoped.setColor(withAlpha(line, randomInt(22, 46)));
scoped.drawLine(-reach, y, reach, y);
}
scoped.dispose();
// 少量贯穿斜线增强方向线索
for (int i = 0; i < randomInt(2, 4); i++) {
graphics.setColor(withAlpha(themeLine(theme), randomInt(60, 100)));
graphics.setStroke(new BasicStroke(randomInt(2, 4), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
graphics.drawLine(randomInt(-30, width / 2), randomInt(-20, height + 20),
randomInt(width / 2, width + 30), randomInt(-20, height + 20));
}
}
/**
* 取主题流线色组中的随机一色
*
* @param theme 主题色组
* @return 流线色
*/
protected static Color themeLine(Color[] theme) {
return theme[4 + ThreadLocalRandom.current().nextInt(3)];
}
private static Color blend(Color from, Color to, double ratio) {
return new Color(
(int) (from.getRed() + (to.getRed() - from.getRed()) * ratio),
(int) (from.getGreen() + (to.getGreen() - from.getGreen()) * ratio),
(int) (from.getBlue() + (to.getBlue() - from.getBlue()) * ratio));
}
private static int clampChannel(int value) {
return Math.min(255, Math.max(0, value));
}
/**
* 取随机主题色组
*
* @return 主题色组
*/
protected static Color[] randomTheme() {
return THEMES[ThreadLocalRandom.current().nextInt(THEMES.length)];
}
/**
* 在底图右下角绘制品牌水印,低透明度不干扰题面;文案取服务端常量,不接受外部入参以杜绝向图片注入任意文本
*/
private static void drawWatermark(Graphics2D graphics, int width, int height) {
Graphics2D scoped = (Graphics2D) graphics.create();
scoped.setFont(new Font(Font.SANS_SERIF, Font.BOLD | Font.ITALIC, 13));
FontMetrics metrics = scoped.getFontMetrics();
int textX = width - metrics.stringWidth(OAuth2TokenConstant.BEHAVIOR_WATERMARK) - 10;
int textY = height - 8;
scoped.setColor(new Color(0, 0, 0, 40));
scoped.drawString(OAuth2TokenConstant.BEHAVIOR_WATERMARK, textX + 1, textY + 1);
scoped.setColor(new Color(255, 255, 255, 52));
scoped.drawString(OAuth2TokenConstant.BEHAVIOR_WATERMARK, textX, textY);
scoped.dispose();
}
/**
* 创建已应用渲染倍率与质量参数的画笔,调用方按逻辑坐标绘制
*
* @param image 超采样图片
* @return 画笔
*/
protected static Graphics2D scaledGraphics(BufferedImage image) {
Graphics2D graphics = image.createGraphics();
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
graphics.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
graphics.scale(RENDER_SCALE, RENDER_SCALE);
return graphics;
}
/**
* 编码为 JPEG 格式 DataURI,用于无透明通道的底图以控制体积
*
* @param image 图片
* @return DataURI 字符串
*/
protected static String toJpegBase64(BufferedImage image) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageWriter writer = ImageIO.getImageWritersByFormatName("jpg").next();
ImageWriteParam param = writer.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(JPEG_QUALITY);
try (MemoryCacheImageOutputStream imageOutput = new MemoryCacheImageOutputStream(outputStream)) {
writer.setOutput(imageOutput);
writer.write(null, new IIOImage(image, null, null), param);
} finally {
writer.dispose();
}
return "data:image/jpeg;base64," + Base64.getEncoder().encodeToString(outputStream.toByteArray());
} catch (IOException exception) {
throw new UncheckedIOException("行为验证码图片编码失败", exception);
}
}
/**
* 编码为 PNG 格式 DataURI,用于携带透明通道的抠块与圆图
*
* @param image 图片
* @return DataURI 字符串
*/
protected static String toPngBase64(BufferedImage image) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, "png", outputStream);
return "data:image/png;base64," + Base64.getEncoder().encodeToString(outputStream.toByteArray());
} catch (IOException exception) {
throw new UncheckedIOException("行为验证码图片编码失败", exception);
}
}
/**
* 调整颜色透明度
*
* @param color 原色
* @param alpha 透明度
* @return 调整后的颜色
*/
protected static Color withAlpha(Color color, int alpha) {
return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha);
}
/**
* 取区间随机数
*
* @param origin 起始值(含)
* @param bound 结束值(不含)
* @return 随机数
*/
protected static int randomInt(int origin, int bound) {
return ThreadLocalRandom.current().nextInt(origin, bound);
}
}
@@ -0,0 +1,61 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.behavior;
import lombok.Data;
import org.springblade.core.tool.support.Kv;
import java.io.Serial;
import java.io.Serializable;
/**
* 行为验证码出题结果
*
* <p>data 为下发客户端的渲染数据,answer 为服务端留存的校验答案,两者严格分离,答案不得出现在响应体中。</p>
*
* @author Chill
*/
@Data
public class Behavior implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 验证码模式编码
*/
private String type;
/**
* 客户端渲染数据
*/
private Kv data;
/**
* 服务端校验答案
*/
private String answer;
}
@@ -0,0 +1,78 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.behavior;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 行为验证码工厂
*
* <p>生成器均为无状态实现,随类加载注册一次即可全局复用;出题时随机选取模式,校验时按出题留存的模式精确分发。</p>
*
* @author Chill
*/
public class BehaviorFactory {
/**
* 生成器注册表
*/
private static final List<BehaviorGenerator> GENERATORS = List.of(
new WordBehaviorGenerator(), new IdiomBehaviorGenerator(), new PuzzleBehaviorGenerator(),
new ConcatBehaviorGenerator(), new RotateBehaviorGenerator()
);
/**
* 模式编码索引
*/
private static final Map<String, BehaviorGenerator> GENERATOR_POOL = GENERATORS.stream()
.collect(Collectors.toUnmodifiableMap(BehaviorGenerator::type, Function.identity()));
private BehaviorFactory() {
}
/**
* 随机选取一种模式的生成器
*
* @return 生成器
*/
public static BehaviorGenerator random() {
return GENERATORS.get(ThreadLocalRandom.current().nextInt(GENERATORS.size()));
}
/**
* 按模式编码获取生成器
*
* @param type 模式编码
* @return 生成器,未注册的模式返回 null
*/
public static BehaviorGenerator of(String type) {
return GENERATOR_POOL.get(type);
}
}
@@ -0,0 +1,59 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.behavior;
/**
* 行为验证码生成器
*
* <p>实现类须为无状态设计,可被多线程并发调用;答案采用紧凑字符串编码,由各模式自行定义格式并负责解析校验。</p>
*
* @author Chill
*/
public interface BehaviorGenerator {
/**
* 验证码模式编码
*
* @return 模式编码
*/
String type();
/**
* 生成验证码
*
* @return 出题结果
*/
Behavior generate();
/**
* 校验用户作答
*
* @param answer 服务端留存答案
* @param userAnswer 用户作答内容
* @return 是否通过
*/
boolean verify(String answer, String userAnswer);
}
@@ -0,0 +1,126 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.behavior;
import org.springblade.core.tool.support.Kv;
import java.awt.*;
import java.awt.image.BufferedImage;
/**
* 拼接还原验证码生成器
*
* <p>底图沿随机高度水平切分,下条内容整体循环左移随机偏移量后下发,
* 用户拖动滑块使下条右移复位,上下纹理衔接连贯即通过。
* 答案为还原所需的右移量,作答为用户拖动的位移;位移在画布宽度上循环,校验按环绕就近取差。</p>
*
* @author Chill
*/
public class ConcatBehaviorGenerator extends AbstractBehaviorGenerator {
/**
* 模式编码
*/
public static final String TYPE = "concat";
private static final int WIDTH = 340;
private static final int HEIGHT = 220;
/**
* 切缝与画布上下缘的最小距离,保证上下条都有足够的纹理参与对齐
*/
private static final int SLICE_MARGIN = 70;
/**
* 错位量与零点的最小距离,避免初始状态即接近还原
*/
private static final int OFFSET_MARGIN = 60;
/**
* 命中容差(像素)
*/
private static final int TOLERANCE = 8;
@Override
public String type() {
return TYPE;
}
@Override
public Behavior generate() {
// 拼接对齐依赖跨切缝的纹理连续性,排除无方向感的光点风格
BufferedImage background = createArtBackground(WIDTH, HEIGHT, randomTheme(),
ArtStyle.FLOW, ArtStyle.WAVE, ArtStyle.GEOMETRIC, ArtStyle.GRID);
int sliceY = randomInt(SLICE_MARGIN, HEIGHT - SLICE_MARGIN + 1);
int offset = randomInt(OFFSET_MARGIN, WIDTH - OFFSET_MARGIN + 1);
BufferedImage dislocated = dislocate(background, sliceY, offset);
Behavior captcha = new Behavior();
captcha.setType(TYPE);
captcha.setAnswer(String.valueOf(offset));
captcha.setData(Kv.create()
.set("image", toJpegBase64(dislocated))
.set("sliceY", sliceY)
.set("width", WIDTH)
.set("height", HEIGHT));
return captcha;
}
@Override
public boolean verify(String answer, String userAnswer) {
try {
int target = Integer.parseInt(answer);
int moved = (int) Double.parseDouble(userAnswer.trim());
int delta = Math.abs(moved - target) % WIDTH;
return Math.min(delta, WIDTH - delta) <= TOLERANCE;
} catch (NumberFormatException exception) {
return false;
}
}
/**
* 输出下条循环左移后的错位图。像素块搬移直接在物理像素域进行,
* 避免经过缩放变换的画笔中逻辑目标区与物理源区两套坐标混用出错
*/
private BufferedImage dislocate(BufferedImage background, int sliceY, int offset) {
int physicalWidth = WIDTH * RENDER_SCALE;
int physicalHeight = HEIGHT * RENDER_SCALE;
int physicalSlice = sliceY * RENDER_SCALE;
int physicalOffset = offset * RENDER_SCALE;
BufferedImage result = new BufferedImage(physicalWidth, physicalHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics = result.createGraphics();
// 上条原样保留
graphics.drawImage(background,
0, 0, physicalWidth, physicalSlice,
0, 0, physicalWidth, physicalSlice, null);
// 下条循环左移:源图右段移至目标左段,源图左段补至目标右段
graphics.drawImage(background,
0, physicalSlice, physicalWidth - physicalOffset, physicalHeight,
physicalOffset, physicalSlice, physicalWidth, physicalHeight, null);
graphics.drawImage(background,
physicalWidth - physicalOffset, physicalSlice, physicalWidth, physicalHeight,
0, physicalSlice, physicalOffset, physicalHeight, null);
graphics.dispose();
return result;
}
}
@@ -0,0 +1,103 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.behavior;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
/**
* 成语语序点选验证码生成器
*
* <p>底图乱序布置一条成语的四个字与若干干扰字,不下发任何目标提示,
* 用户须凭语言知识辨识出成语并按语序点击。机器即使识别出全部字符,
* 仍需具备成语知识才能确定目标与顺序,对抗强度高于明文提示的文字点选。
* 布局、绘制与校验逻辑复用文字点选实现。</p>
*
* @author Chill
*/
public class IdiomBehaviorGenerator extends WordBehaviorGenerator {
/**
* 模式编码
*/
public static final String TYPE = "idiom";
/**
* 干扰字符数
*/
private static final int DISTURB_COUNT = 2;
/**
* 常用四字成语词表,收录时保证条目内四字互异,避免同图出现相同字导致作答歧义
*/
private static final String[] IDIOMS = {
"风和日丽", "山清水秀", "鸟语花香", "春暖花开", "秋高气爽", "冰天雪地", "万紫千红", "五彩缤纷", "花好月圆", "湖光山色",
"青山绿水", "世外桃源", "名山大川", "奇花异草", "古木参天", "电闪雷鸣", "风起云涌", "海阔天空", "波澜壮阔",
"一帆风顺", "万事如意", "心想事成", "吉祥如意", "五谷丰登", "六畜兴旺", "国泰民安", "安居乐业", "丰衣足食",
"张灯结彩", "欢天喜地", "普天同庆", "皆大欢喜", "兴高采烈", "眉开眼笑", "手舞足蹈", "心花怒放", "神采飞扬",
"聚精会神", "全神贯注", "专心致志", "一丝不苟", "持之以恒", "坚持不懈", "自强不息", "发愤图强", "勤学苦练",
"博览群书", "学富五车", "才高八斗", "出口成章", "妙笔生花", "画龙点睛", "入木三分", "行云流水", "龙飞凤舞", "笔走龙蛇",
"高瞻远瞩", "深谋远虑", "运筹帷幄", "足智多谋", "神机妙算", "料事如神", "明察秋毫", "洞若观火", "心明眼亮", "耳聪目明",
"雷厉风行", "当机立断", "斩钉截铁", "大刀阔斧", "干净利落", "快马加鞭", "争分夺秒", "只争朝夕", "闻鸡起舞", "夜以继日",
"同舟共济", "齐心协力", "众志成城", "万众一心", "同甘共苦", "患难与共", "肝胆相照", "荣辱与共", "风雨同舟",
"光明磊落", "表里如一", "言行一致", "脚踏实地", "实事求是", "名副其实", "货真价实", "童叟无欺",
"见义勇为", "助人为乐", "雪中送炭", "拔刀相助", "舍己为人", "大公无私", "克己奉公", "两袖清风", "一尘不染",
"虚怀若谷", "不耻下问", "集思广益", "从善如流", "闻过则喜", "开诚布公", "推心置腹", "设身处地",
"温故知新", "举一反三", "融会贯通", "触类旁通", "深入浅出", "通俗易懂", "言简意赅", "短小精悍", "画蛇添足", "杯弓蛇影",
"守株待兔", "刻舟求剑", "亡羊补牢", "掩耳盗铃", "自相矛盾", "滥竽充数", "买椟还珠", "邯郸学步", "东施效颦", "叶公好龙",
"狐假虎威", "鹬蚌相争", "螳臂当车", "井底之蛙", "对牛弹琴", "指鹿为马", "塞翁失马", "愚公移山", "精卫填海",
"八仙过海", "三头六臂", "七嘴八舌", "五湖四海", "四面八方", "千军万马", "万水千山", "千辛万苦", "千变万化",
"日新月异", "突飞猛进", "一日千里", "与时俱进", "继往开来", "承前启后", "推陈出新", "标新立异", "独辟蹊径", "别具匠心",
"栉风沐雨", "披星戴月", "风餐露宿", "跋山涉水", "翻山越岭", "长途跋涉", "不远万里", "走南闯北", "五体投地", "心悦诚服",
"金碧辉煌", "富丽堂皇", "雕梁画栋", "巧夺天工", "鬼斧神工", "匠心独运", "精雕细琢", "玲珑剔透",
"车水马龙", "川流不息", "熙来攘往", "门庭若市", "高朋满座", "宾至如归", "络绎不绝", "人声鼎沸", "座无虚席", "万人空巷",
};
@Override
public String type() {
return TYPE;
}
@Override
public Behavior generate() {
// 语序辨识依赖汉字语义,服务器缺少中文字体时降级为明文提示的文字点选
if (!CJK_AVAILABLE) {
return super.generate();
}
String idiom = IDIOMS[ThreadLocalRandom.current().nextInt(IDIOMS.length)];
List<String> drawChars = new ArrayList<>(idiom.length() + DISTURB_COUNT);
for (int i = 0; i < idiom.length(); i++) {
drawChars.add(String.valueOf(idiom.charAt(i)));
}
// 干扰字不得与成语字重复,否则用户无法唯一定位目标
Set<String> excludes = new HashSet<>(drawChars);
drawChars.addAll(pickDistinctChars(DISTURB_COUNT, excludes));
return renderWordPuzzle(drawChars, idiom.length(), false);
}
}
@@ -0,0 +1,146 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.behavior;
import org.springblade.core.tool.support.Kv;
import java.awt.*;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
/**
* 滑块拼图验证码生成器
*
* <p>底图随机位置抠出拼图块,拼图块纵向位置固定下发,用户仅横向拖动滑轨使其对齐缺口。
* 答案为缺口左上角横坐标,作答为拼图块最终停靠横坐标,容差内视为命中。</p>
*
* @author Chill
*/
public class PuzzleBehaviorGenerator extends AbstractBehaviorGenerator {
/**
* 模式编码
*/
public static final String TYPE = "puzzle";
private static final int WIDTH = 340;
private static final int HEIGHT = 220;
/**
* 拼图块包围盒边长
*/
private static final int PIECE_SIZE = 60;
/**
* 命中容差(像素)
*/
private static final int TOLERANCE = 8;
@Override
public String type() {
return TYPE;
}
@Override
public Behavior generate() {
BufferedImage background = createArtBackground(WIDTH, HEIGHT);
// 缺口位于画布右半区,避免拼图块起始位置即命中
int gapX = randomInt(WIDTH / 2, WIDTH - PIECE_SIZE - 10);
int gapY = randomInt(10, HEIGHT - PIECE_SIZE - 10);
Shape pieceShape = createPieceShape();
// 先按缺口位置截取拼图块,再在底图上覆盖缺口暗色遮罩
BufferedImage piece = cutPiece(background, pieceShape, gapX, gapY);
maskGap(background, pieceShape, gapX, gapY);
Behavior captcha = new Behavior();
captcha.setType(TYPE);
captcha.setAnswer(String.valueOf(gapX));
captcha.setData(Kv.create()
.set("image", toJpegBase64(background))
.set("piece", toPngBase64(piece))
.set("pieceY", gapY)
.set("pieceSize", PIECE_SIZE)
.set("width", WIDTH)
.set("height", HEIGHT));
return captcha;
}
@Override
public boolean verify(String answer, String userAnswer) {
try {
int deltaX = Math.abs((int) Double.parseDouble(userAnswer.trim()) - Integer.parseInt(answer));
return deltaX <= TOLERANCE;
} catch (NumberFormatException exception) {
return false;
}
}
/**
* 构造拼图块形状:圆角方形主体 + 顶部凸起 + 右侧凹口,坐标基于包围盒原点
*/
private Shape createPieceShape() {
Area area = new Area(new RoundRectangle2D.Double(4, 16, 44, 40, 8, 8));
area.add(new Area(new Ellipse2D.Double(17, 7, 18, 18)));
area.subtract(new Area(new Ellipse2D.Double(40, 28, 16, 16)));
return area;
}
/**
* 按拼图形状截取底图内容并描边,输出带透明通道的拼图块
*/
private BufferedImage cutPiece(BufferedImage background, Shape pieceShape, int gapX, int gapY) {
BufferedImage piece = new BufferedImage(PIECE_SIZE * RENDER_SCALE, PIECE_SIZE * RENDER_SCALE, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = scaledGraphics(piece);
graphics.setClip(pieceShape);
graphics.drawImage(background, -gapX, -gapY, WIDTH, HEIGHT, null);
graphics.setClip(null);
graphics.setColor(new Color(255, 255, 255, 210));
graphics.setStroke(new BasicStroke(1.2F));
graphics.draw(pieceShape);
graphics.dispose();
return piece;
}
/**
* 在底图缺口位置叠加暗色遮罩与双层描边(外亮内暗),形成凹陷的拖动目标提示
*/
private void maskGap(BufferedImage background, Shape pieceShape, int gapX, int gapY) {
Graphics2D graphics = scaledGraphics(background);
graphics.translate(gapX, gapY);
graphics.setColor(new Color(10, 14, 22, 150));
graphics.fill(pieceShape);
graphics.setClip(pieceShape);
graphics.setColor(new Color(0, 0, 0, 110));
graphics.setStroke(new BasicStroke(4F));
graphics.draw(pieceShape);
graphics.setClip(null);
graphics.setColor(new Color(255, 255, 255, 130));
graphics.setStroke(new BasicStroke(1.2F));
graphics.draw(pieceShape);
graphics.dispose();
}
}
@@ -0,0 +1,133 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.behavior;
import org.springblade.core.tool.support.Kv;
import java.awt.*;
import java.awt.geom.Ellipse2D;
import java.awt.image.BufferedImage;
/**
* 旋转还原验证码生成器
*
* <p>底图与其他模式保持一致的横幅画布,从画布中心截取圆形区域并预旋转随机角度,外围图案保持原状,
* 用户转动内圆使其与外围纹理衔接连贯即通过。答案为还原所需角度,与预旋转角度互补为整圆;
* 校验时按圆周就近取差,容差内视为命中。</p>
*
* @author Chill
*/
public class RotateBehaviorGenerator extends AbstractBehaviorGenerator {
/**
* 模式编码
*/
public static final String TYPE = "rotate";
private static final int WIDTH = 340;
private static final int HEIGHT = 220;
/**
* 中心圆直径
*/
private static final int CIRCLE_SIZE = 150;
/**
* 命中容差(度)
*/
private static final int TOLERANCE = 8;
@Override
public String type() {
return TYPE;
}
@Override
public Behavior generate() {
// 旋转还原依赖内外纹理的方向连续性,限定具备贯穿性线条的风格
BufferedImage background = createArtBackground(WIDTH, HEIGHT, randomTheme(), ArtStyle.FLOW, ArtStyle.WAVE, ArtStyle.GRID);
// 预旋转角度避开整圆两端,防止初始状态即接近还原
int rotateAngle = randomInt(45, 316);
BufferedImage ring = cutRotatedCircle(background, rotateAngle);
maskCircle(background);
Behavior captcha = new Behavior();
captcha.setType(TYPE);
captcha.setAnswer(String.valueOf(360 - rotateAngle));
captcha.setData(Kv.create()
.set("image", toJpegBase64(background))
.set("ring", toPngBase64(ring))
.set("ringSize", CIRCLE_SIZE)
.set("width", WIDTH)
.set("height", HEIGHT));
return captcha;
}
@Override
public boolean verify(String answer, String userAnswer) {
try {
int target = Integer.parseInt(answer);
int rotated = (int) Double.parseDouble(userAnswer.trim());
int delta = Math.abs(rotated - target) % 360;
return Math.min(delta, 360 - delta) <= TOLERANCE;
} catch (NumberFormatException exception) {
return false;
}
}
/**
* 从画布中心截取圆形区域并预旋转,输出带透明通道的内圆图
*/
private BufferedImage cutRotatedCircle(BufferedImage background, int rotateAngle) {
BufferedImage circle = new BufferedImage(CIRCLE_SIZE * RENDER_SCALE, CIRCLE_SIZE * RENDER_SCALE, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = scaledGraphics(circle);
graphics.setClip(new Ellipse2D.Double(0, 0, CIRCLE_SIZE, CIRCLE_SIZE));
double radius = CIRCLE_SIZE / 2.0;
graphics.rotate(Math.toRadians(rotateAngle), radius, radius);
graphics.drawImage(background, -(WIDTH - CIRCLE_SIZE) / 2, -(HEIGHT - CIRCLE_SIZE) / 2, WIDTH, HEIGHT, null);
graphics.setClip(null);
// 内圆细描边柔化与外围遮罩的拼接边缘
graphics.setColor(new Color(255, 255, 255, 90));
graphics.setStroke(new BasicStroke(1.2F));
graphics.draw(new Ellipse2D.Double(0.6, 0.6, CIRCLE_SIZE - 1.2, CIRCLE_SIZE - 1.2));
graphics.dispose();
return circle;
}
/**
* 在底图中心叠加暗色圆形遮罩与描边,内圆区域由客户端组件覆盖渲染
*/
private void maskCircle(BufferedImage background) {
Graphics2D graphics = scaledGraphics(background);
int offsetX = (WIDTH - CIRCLE_SIZE) / 2;
int offsetY = (HEIGHT - CIRCLE_SIZE) / 2;
Ellipse2D hole = new Ellipse2D.Double(offsetX, offsetY, CIRCLE_SIZE, CIRCLE_SIZE);
graphics.setColor(new Color(10, 14, 22, 165));
graphics.fill(hole);
graphics.setColor(new Color(255, 255, 255, 110));
graphics.setStroke(new BasicStroke(1.2F));
graphics.draw(hole);
graphics.dispose();
}
}
@@ -0,0 +1,311 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.behavior;
import org.springblade.core.tool.support.Kv;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
/**
* 文字点选验证码生成器
*
* <p>底图随机布置若干字符,提示以字符文本数组下发,由客户端自行渲染提示样式,用户按提示顺序点击命中即通过。
* 答案格式为按序的目标字符中心坐标串「x,y;x,y;x,y」,作答格式与其一致。
* 画布布字与作答校验逻辑同时服务于语序点选等文字类子模式。</p>
*
* @author Chill
*/
public class WordBehaviorGenerator extends AbstractBehaviorGenerator {
/**
* 模式编码
*/
public static final String TYPE = "word";
private static final int WIDTH = 340;
private static final int HEIGHT = 220;
private static final int MIN_FONT_SIZE = 34;
private static final int MAX_FONT_SIZE = 44;
private static final int DRAW_COUNT = 6;
private static final int TARGET_COUNT = 3;
/**
* 命中容差(像素),覆盖最大字号包围盒并预留触点抖动余量
*/
private static final int TOLERANCE = 28;
/**
* 常用简体汉字池,选取笔画清晰、辨识度高的字符,池内字符严格互异,避免同图出现相同字导致作答歧义
*/
private static final String CJK_CHARSET =
"天地月星辰云雨雪风霜露雷电虹霞光影" +
"春夏秋冬晨昏昼夜今古年岁时刻早晚" +
"东南西北中上下左右前后内外高低远深浅" +
"大小多少长短宽窄厚薄轻重快慢冷暖湿新旧" +
"红黄蓝绿紫青黑白灰彩明暗浓淡" +
"花草树叶林竹梅兰菊荷松柏桃李杏枝根果种" +
"鸟鱼虫马牛羊狗猫鸭鹅龙凤虎豹狼鹿熊猴象鹰燕蜂蝶蛙龟鹤" +
"山水江河湖海溪泉波浪田原岭峰谷" +
"门窗屋房墙院园路街巷城乡村镇楼阁亭桥塔仓库店铺" +
"书画琴棋诗词歌赋笔墨纸砚文章句音律曲调" +
"茶酒饭菜米面油盐糖醋汤饼瓜豆蛋奶" +
"衣帽鞋袜布丝绸棉麻皮革绒线" +
"金银铜铁钢玉石珠宝砖瓦泥沙" +
"心手足口耳目眉发肩背腰腿骨肉脉" +
"仁义礼智信忠孝勇谦和善美真诚正直清" +
"福寿康宁安泰吉祥瑞庆贺" +
"走跑跳飞游泳行摇摆抛掷接送迎" +
"学问知识才技能功课业教写算";
/**
* 字体缺失时的降级字符池,仅保留无歧义的字母数字
*/
private static final String ASCII_CHARSET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
/**
* 可用字体池,绘制时每字随机取用,混排字体使字形特征碎片化;
* 服务器缺少中文字体时字符会渲染为占位方框,启动时探测一次并降级为字母数字字符池
*/
private static final List<Font> TEXT_FONTS = resolveTextFonts();
/**
* 中文字体是否可用,语序点选等依赖汉字语义的子模式据此决定是否降级
*/
protected static final boolean CJK_AVAILABLE = TEXT_FONTS.get(0).canDisplay(CJK_CHARSET.charAt(0));
private static final String CHARSET = CJK_AVAILABLE ? CJK_CHARSET : ASCII_CHARSET;
@Override
public String type() {
return TYPE;
}
@Override
public Behavior generate() {
return renderWordPuzzle(pickDistinctChars(DRAW_COUNT, Collections.emptySet()), TARGET_COUNT, true);
}
@Override
public boolean verify(String answer, String userAnswer) {
String[] targets = answer.split(";");
String[] clicks = userAnswer.split(";");
if (clicks.length != targets.length) {
return false;
}
try {
for (int i = 0; i < targets.length; i++) {
String[] target = targets[i].split(",");
String[] click = clicks[i].split(",");
int deltaX = Math.abs((int) Double.parseDouble(click[0].trim()) - Integer.parseInt(target[0]));
int deltaY = Math.abs((int) Double.parseDouble(click[1].trim()) - Integer.parseInt(target[1]));
if (deltaX > TOLERANCE || deltaY > TOLERANCE) {
return false;
}
}
return true;
} catch (NumberFormatException | ArrayIndexOutOfBoundsException exception) {
return false;
}
}
/**
* 布置字符画布并产出题面:前 targetCount 个字符为按序作答目标,其余为干扰字符
*
* @param drawChars 待布置字符,数量须与画布网格容量一致
* @param targetCount 作答目标数
* @param withPrompt 是否下发目标字符明文提示,语序类子模式不下发以要求用户凭语义辨识
* @return 出题结果
*/
protected Behavior renderWordPuzzle(List<String> drawChars, int targetCount, boolean withPrompt) {
// 字符伪装色与底图共用同一主题,保证同色系融合
Color[] theme = randomTheme();
BufferedImage image = createArtBackground(WIDTH, HEIGHT, theme);
Graphics2D graphics = scaledGraphics(image);
// 画布按 3x2 网格布置字符,网格内随机抖动,保证字符互不重叠
List<Point> centers = new ArrayList<>(drawChars.size());
List<Integer> cells = shuffledCells(drawChars.size());
int cellWidth = WIDTH / 3;
int cellHeight = HEIGHT / 2;
int margin = MAX_FONT_SIZE / 2 + 4;
for (int i = 0; i < drawChars.size(); i++) {
int cell = cells.get(i);
int cellX = (cell % 3) * cellWidth;
int cellY = (cell / 3) * cellHeight;
int centerX = randomInt(cellX + margin, cellX + cellWidth - margin + 1);
int centerY = randomInt(cellY + margin, cellY + cellHeight - margin + 1);
drawCamouflagedChar(graphics, drawChars.get(i), centerX, centerY, theme);
centers.add(new Point(centerX, centerY));
}
drawVeilGlows(graphics, theme);
drawInterferenceLines(graphics, theme);
graphics.dispose();
StringBuilder answer = new StringBuilder();
for (int i = 0; i < targetCount; i++) {
if (i > 0) {
answer.append(';');
}
answer.append(centers.get(i).x).append(',').append(centers.get(i).y);
}
Behavior captcha = new Behavior();
captcha.setType(type());
captcha.setAnswer(answer.toString());
Kv data = Kv.create()
.set("image", toJpegBase64(image))
.set("targetCount", targetCount)
.set("width", WIDTH)
.set("height", HEIGHT);
if (withPrompt) {
data.set("prompt", drawChars.subList(0, targetCount));
}
captcha.setData(data);
return captcha;
}
/**
* 从字符池随机抽取互不重复且不在排除集内的字符
*
* @param count 抽取数量
* @param excludes 排除字符集
* @return 字符列表
*/
protected List<String> pickDistinctChars(int count, Set<String> excludes) {
List<Integer> indexes = new ArrayList<>(CHARSET.length());
for (int i = 0; i < CHARSET.length(); i++) {
indexes.add(i);
}
Collections.shuffle(indexes, ThreadLocalRandom.current());
List<String> chars = new ArrayList<>(count);
for (Integer index : indexes) {
String candidate = String.valueOf(CHARSET.charAt(index));
if (!excludes.contains(candidate)) {
chars.add(candidate);
if (chars.size() == count) {
break;
}
}
}
return chars;
}
/**
* 以指定中心点绘制伪装字符:取主题流线同系色并随机降低不透明度,使字符与底图色调融合而非高对比浮出,
* 随机字体、字号、旋转与错切形变使字形特征碎片化;伪轮廓用主题暗色低透明度勾勒,仅保留细看可辨的边缘
*/
private void drawCamouflagedChar(Graphics2D graphics, String text, int centerX, int centerY, Color[] theme) {
Graphics2D scoped = (Graphics2D) graphics.create();
Font font = TEXT_FONTS.get(ThreadLocalRandom.current().nextInt(TEXT_FONTS.size()))
.deriveFont(Font.BOLD, (float) randomInt(MIN_FONT_SIZE, MAX_FONT_SIZE + 1));
scoped.setFont(font);
scoped.rotate(Math.toRadians(randomInt(-35, 36)), centerX, centerY);
scoped.translate(centerX, centerY);
scoped.shear(randomInt(-18, 19) / 100.0, 0);
scoped.translate(-centerX, -centerY);
FontMetrics metrics = scoped.getFontMetrics();
int textWidth = metrics.stringWidth(text);
int baselineY = centerY + (metrics.getAscent() - metrics.getDescent()) / 2;
scoped.setColor(withAlpha(theme[1], 60));
scoped.drawString(text, centerX - textWidth / 2 + 1, baselineY + 1);
scoped.setColor(withAlpha(themeLine(theme), randomInt(90, 150)));
scoped.drawString(text, centerX - textWidth / 2, baselineY);
scoped.dispose();
}
/**
* 在字符层之上回叠主题柔光斑,压低字符与背景的整体对比度,形成若隐若现的观感
*/
private void drawVeilGlows(Graphics2D graphics, Color[] theme) {
for (int i = 0; i < 2; i++) {
Color glow = theme[2 + randomInt(0, 2)];
int radius = randomInt(HEIGHT / 2, HEIGHT);
java.awt.geom.Point2D center = new java.awt.geom.Point2D.Double(randomInt(0, WIDTH), randomInt(0, HEIGHT));
graphics.setPaint(new RadialGradientPaint(center, radius, new float[]{0F, 1F},
new Color[]{withAlpha(glow, randomInt(26, 46)), withAlpha(glow, 0)}));
graphics.fillOval((int) center.getX() - radius, (int) center.getY() - radius, radius * 2, radius * 2);
}
}
/**
* 在字符层之上叠加主题同系色弧线,切断字符轮廓的连续边缘
*/
private void drawInterferenceLines(Graphics2D graphics, Color[] theme) {
for (int i = 0; i < 3; i++) {
graphics.setColor(withAlpha(themeLine(theme), randomInt(60, 110)));
graphics.setStroke(new BasicStroke(randomInt(1, 3) + 0.5F, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
graphics.draw(new java.awt.geom.CubicCurve2D.Double(
randomInt(-30, 10), randomInt(0, HEIGHT),
randomInt(0, WIDTH), randomInt(-30, HEIGHT + 30),
randomInt(0, WIDTH), randomInt(-30, HEIGHT + 30),
randomInt(WIDTH - 10, WIDTH + 30), randomInt(0, HEIGHT)));
}
}
/**
* 打乱网格顺序,使目标字符位置分布随机
*/
private List<Integer> shuffledCells(int count) {
List<Integer> cells = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
cells.add(i);
}
Collections.shuffle(cells, ThreadLocalRandom.current());
return cells;
}
/**
* 探测可用的中文字体池:优先收集常见字体族的全部可用项,为空时扫描系统字体,仍为空时回退系统无衬线字体
*/
private static List<Font> resolveTextFonts() {
List<Font> fonts = new ArrayList<>();
String[] preferredFamilies = {"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
"Noto Sans CJK SC", "Source Han Sans SC", "WenQuanYi Micro Hei", "SimHei", "STHeiti", "KaiTi", "SimSun"};
for (String family : preferredFamilies) {
Font font = new Font(family, Font.BOLD, MAX_FONT_SIZE);
if (font.canDisplay('验')) {
fonts.add(font);
}
}
if (fonts.isEmpty()) {
for (Font candidate : GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts()) {
if (candidate.canDisplay('验')) {
fonts.add(candidate.deriveFont(Font.BOLD, (float) MAX_FONT_SIZE));
if (fonts.size() >= 3) {
break;
}
}
}
}
if (fonts.isEmpty()) {
fonts.add(new Font(Font.SANS_SERIF, Font.BOLD, MAX_FONT_SIZE));
}
return List.copyOf(fonts);
}
}
@@ -0,0 +1,94 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.config;
import lombok.AllArgsConstructor;
import org.springblade.core.jwt.props.JwtProperties;
import org.springblade.core.oauth2.granter.TokenGranter;
import org.springblade.core.oauth2.granter.TokenGranterEnhancer;
import org.springblade.core.oauth2.granter.TokenGranterFactory;
import org.springblade.core.oauth2.handler.*;
import org.springblade.core.oauth2.props.OAuth2Properties;
import org.springblade.core.oauth2.service.OAuth2ClientService;
import org.springblade.core.oauth2.service.OAuth2UserService;
import org.springblade.core.oauth2.service.impl.OAuth2ClientDetailService;
import org.springblade.core.oauth2.service.impl.OAuth2UserDetailService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.List;
/**
* OAuth2Configuration
*
* @author BladeX
*/
@AllArgsConstructor
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(OAuth2Properties.class)
@ConditionalOnProperty(value = OAuth2Properties.PREFIX + ".enabled", havingValue = "true", matchIfMissing = true)
public class OAuth2AutoConfiguration {
@Bean
@ConditionalOnMissingBean
public AuthorizationHandler authorizationHandler() {
return new OAuth2AuthorizationHandler();
}
@Bean
@ConditionalOnMissingBean
public PasswordHandler passwordHandler(OAuth2Properties properties) {
return new OAuth2PasswordHandler(properties);
}
@Bean
@ConditionalOnMissingBean
public TokenHandler tokenHandler(JwtProperties properties) {
return new OAuth2TokenHandler(properties);
}
@Bean
@ConditionalOnMissingBean
public OAuth2ClientService oAuth2ClientService(JdbcTemplate jdbcTemplate) {
return new OAuth2ClientDetailService(jdbcTemplate);
}
@Bean
@ConditionalOnMissingBean
public OAuth2UserService oAuth2UserService(JdbcTemplate jdbcTemplate) {
return new OAuth2UserDetailService(jdbcTemplate);
}
@Bean
public TokenGranterFactory tokenGranterFactory(List<TokenGranter> tokenGranters, List<TokenGranterEnhancer> tokenGranterEnhancers, OAuth2Properties properties) {
return new TokenGranterFactory(tokenGranters, tokenGranterEnhancers, properties);
}
}
@@ -0,0 +1,46 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.config;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* OAuth资源配置
*
* @author BladeX
*/
@AutoConfiguration
public class OAuth2WebConfiguration implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
}
}
@@ -0,0 +1,84 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.constant;
/**
* OAuth2AuthorizationConstant
*
* @author BladeX
*/
public interface OAuth2AuthorizationConstant {
/**
* 用户session key
*/
String AUTHORIZATION_SESSION_KEY = "user";
/**
* 授权请求key
*/
String AUTHORIZATION_REQUEST_KEY = "authorizationRequest";
/**
* 公钥
*/
String PUBLIC_KEY = "publicKey";
/**
* 跳转参数
*/
String REDIRECT_URL = "redirect:";
/**
* 授权地址
*/
String AUTHORIZE_URL = "/oauth/authorize";
/**
* 登录地址
*/
String LOGIN_URL = "/oauth/login";
/**
* 错误地址
*/
String ERROR_URL = "/oauth/error";
/**
* 授权视图
*/
String AUTHORIZE_MODEL = "authorize";
/**
* 登录视图
*/
String LOGIN_MODEL = "login";
/**
* 错误视图
*/
String ERROR_MODEL = "error";
}
@@ -0,0 +1,54 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.constant;
/**
* OAuth2ClientConstant
*
* @author BladeX
*/
public interface OAuth2ClientConstant {
/**
* blade_client表字段
*/
String CLIENT_FIELDS = "id, client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove";
/**
* blade_client查询语句
*/
String BASE_STATEMENT = "select " + CLIENT_FIELDS + " from blade_client";
/**
* blade_client查询排序
*/
String DEFAULT_FIND_STATEMENT = BASE_STATEMENT + " order by client_id";
/**
* 查询client_id
*/
String DEFAULT_SELECT_STATEMENT = BASE_STATEMENT + " where client_id = ?";
}
@@ -0,0 +1,80 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.constant;
/**
* GranterTypeConstant
*
* @author BladeX
*/
public interface OAuth2GranterConstant {
/**
* 授权码模式
*/
String AUTHORIZATION_CODE = "authorization_code";
/**
* 密码模式
*/
String PASSWORD = "password";
/**
* 刷新token模式
*/
String REFRESH_TOKEN = "refresh_token";
/**
* 客户端模式
*/
String CLIENT_CREDENTIALS = "client_credentials";
/**
* 简化模式
*/
String IMPLICIT = "implicit";
/**
* 验证码模式
*/
String CAPTCHA = "captcha";
/**
* 行为验证码模式
*/
String BEHAVIOR = "behavior";
/**
* 手机验证码模式
*/
String SMS_CODE = "sms_code";
/**
* 微信小程序模式
*/
String WECHAT_APPLET = "wechat_applet";
/**
* 开放平台模式
*/
String SOCIAL = "social";
/**
* 注册模式
*/
String REGISTER = "register";
}
@@ -0,0 +1,107 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.constant;
/**
* OAuth2常量.
*
* @author BladeX
*/
public interface OAuth2ParameterConstant {
/**
* 客户端id
*/
String CLIENT_ID = "client_id";
/**
* 客户端密钥
*/
String CLIENT_SECRET = "client_secret";
/**
* 令牌
*/
String ACCESS_TOKEN = "access_token";
/**
* 刷新令牌
*/
String REFRESH_TOKEN = "refresh_token";
/**
* 租户编号
*/
String TENANT_ID = "tenant_id";
/**
* 用户名字
*/
String NAME = "name";
/**
* 用户名
*/
String USERNAME = "username";
/**
* 密码
*/
String PASSWORD = "password";
/**
* 手机号
*/
String PHONE = "phone";
/**
* 电子游戏
*/
String EMAIL = "email";
/**
* 授权类型
*/
String GRANT_TYPE = "grant_type";
/**
* 响应类型
*/
String SCOPE = "scope";
/**
* 重定向地址
*/
String REDIRECT_URI = "redirect_uri";
/**
* 返回类型
*/
String RESPONSE_TYPE = "response_type";
/**
* 状态
*/
String STATE = "state";
/**
* 验证
*/
String CODE = "code";
/**
* 来源
*/
String SOURCE = "source";
/**
* 自动授权
*/
String AUTO_APPROVE = "auto_approve";
}
@@ -0,0 +1,40 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.constant;
/**
* OAuth2ResponseConstant
*
* @author BladeX
*/
public interface OAuth2ResponseConstant {
Integer DEFAULT_SUCCESS_CODE = 200;
String SUCCESS = "success";
String SUCCESS_CODE = "code";
String SUCCESS_DESCRIPTION = "msg";
String ERROR_CODE = "error_code";
String ERROR_DESCRIPTION = "error_description";
}
@@ -0,0 +1,64 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.constant;
/**
* TokenConstant
*
* @author BladeX
*/
public interface OAuth2TokenConstant {
String HEADER_AUTHORIZATION = "Authorization";
String HEADER_AUTHORIZATION_PREFIX = "Basic ";
String TOKEN_HEADER = "Blade-Auth";
String TENANT_HEADER = "Tenant-Id";
String DEFAULT_TENANT_ID = "000000";
String USER_HEADER = "User-Id";
String DEPT_HEADER = "Dept-Id";
String ROLE_HEADER = "Role-Id";
String USER_TYPE_HEADER = "User-Type";
String DEFAULT_USER_TYPE = "web";
String USER_FAIL_KEY = "blade:user::blade:fail:";
String CAPTCHA_CACHE_KEY = "blade:auth::blade:captcha:";
String CAPTCHA_HEADER_KEY = "Captcha-Key";
String CAPTCHA_HEADER_CODE = "Captcha-Code";
String CAPTCHA_NOT_CORRECT = "验证码不正确";
String BEHAVIOR_CACHE_KEY = "blade:auth::blade:behavior:";
String BEHAVIOR_TICKET_KEY = "blade:auth::blade:behavior-ticket:";
String BEHAVIOR_WATERMARK = "BladeX";
String BEHAVIOR_INVALID = "行为验证码已失效,请重新验证";
String BEHAVIOR_NOT_CORRECT = "行为验证未通过,请重新验证";
String TOKEN_NOT_CORRECT = "令牌授权不正确";
String TOKEN_NOT_PERMISSION = "令牌授权已过期";
String USER_NOT_FOUND = "用户名或密码错误";
String USER_PHONE_NOT_FOUND = "用户手机未注册";
String USER_HAS_NO_ROLE = "未获得用户的角色信息";
String USER_HAS_NO_TENANT = "未获得用户的租户信息";
String USER_HAS_NO_TENANT_PERMISSION = "租户授权已过期,请联系管理员";
String USER_HAS_TOO_MANY_FAILS = "登录错误次数过多,请稍后再试";
String DEFAULT_AVATAR = "https://bladex.cn/images/logo-small.png";
}
@@ -0,0 +1,50 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.constant;
/**
* OAuth2UserConstant
*
* @author BladeX
*/
public interface OAuth2UserConstant {
/**
* blade_user查询
*/
String DEFAULT_USERID_SELECT_STATEMENT = "select id as user_id, tenant_id , account, password from blade_user where id = ?";
/**
* blade_user查询
*/
String DEFAULT_USERNAME_SELECT_STATEMENT = "select id as user_id, tenant_id , account, password from blade_user where account = ?";
/**
* blade_user查询
*/
String DEFAULT_PHONE_SELECT_STATEMENT = "select id as user_id, tenant_id , account, password from blade_user where phone = ?";
}
@@ -0,0 +1,282 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.endpoint;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springblade.core.oauth2.constant.OAuth2AuthorizationConstant;
import org.springblade.core.oauth2.handler.PasswordHandler;
import org.springblade.core.oauth2.props.OAuth2Properties;
import org.springblade.core.oauth2.provider.OAuth2AuthorizationRequest;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.service.OAuth2Client;
import org.springblade.core.oauth2.service.OAuth2ClientService;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.service.OAuth2UserService;
import org.springblade.core.oauth2.utils.OAuth2CodeUtil;
import org.springblade.core.redis.cache.BladeRedis;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttribute;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.time.Duration;
import java.util.Optional;
import static org.springblade.core.oauth2.constant.OAuth2ParameterConstant.*;
/**
* AuthorizationEndpoint
*
* @author BladeX
*/
@Controller
@RequiredArgsConstructor
@Tag(name = "用户授权码模式认证", description = "2 - OAuth2授权码模式端点")
public class OAuth2AuthorizationEndpoint implements OAuth2AuthorizationConstant {
private final BladeRedis bladeRedis;
private final OAuth2ClientService clientService;
private final OAuth2UserService userService;
private final PasswordHandler passwordHandler;
private final OAuth2Properties oAuth2Properties;
@GetMapping("/oauth/login")
public String loginPage(HttpSession session, Model model) {
// 从session中获取授权请求参数
Optional.ofNullable((OAuth2AuthorizationRequest) session.getAttribute(AUTHORIZATION_REQUEST_KEY))
.ifPresent(authorizationRequest -> {
model.addAttribute(PUBLIC_KEY, oAuth2Properties.getPublicKey());
model.addAttribute(AUTHORIZATION_REQUEST_KEY, authorizationRequest);
});
// 返回登录页面视图
return LOGIN_MODEL;
}
@PostMapping("/oauth/login/perform")
public String performLogin(@SessionAttribute(AUTHORIZATION_REQUEST_KEY) OAuth2AuthorizationRequest authorizationRequest,
RedirectAttributes redirectAttributes, HttpSession session) {
// 根据用户名和密码验证用户
return Optional.ofNullable(authenticateUser(session, authorizationRequest))
.map(user -> {
// 用户验证成功,处理授权请求参数和重定向
authorizationRequest.setTenantId(user.getTenantId());
session.setAttribute(AUTHORIZATION_REQUEST_KEY, authorizationRequest);
redirectAttributes.addAllAttributes(authorizationRequest.getParameters());
return REDIRECT_URL + AUTHORIZE_URL; // 重定向回授权视图
})
.orElse(REDIRECT_URL + ERROR_URL); // 用户验证失败,重定向回失败视图
}
@GetMapping("/oauth/authorize")
public String authorize(@SessionAttribute(value = AUTHORIZATION_REQUEST_KEY, required = false) OAuth2AuthorizationRequest authorizationRequest,
HttpSession session, Model model) {
// 获取授权请求参数
OAuth2AuthorizationRequest request = OAuth2AuthorizationRequest.create().buildParameters();
// 设置请求参数
Optional.ofNullable(authorizationRequest).ifPresentOrElse(authReq -> {
if (request.getState() == null) {
request.setState(authReq.getState());
}
if (request.getClientId() != null) {
session.setAttribute(AUTHORIZATION_REQUEST_KEY, request);
}
}, () -> session.setAttribute(AUTHORIZATION_REQUEST_KEY, request));
// 获取用户信息并跳转
return Optional.ofNullable(session.getAttribute(AUTHORIZATION_SESSION_KEY))
.map(obj -> (OAuth2User) obj)
.map(user -> {
String clientId = request.getParameters().get(CLIENT_ID);
OAuth2Client client = clientService.loadByClientId(clientId);
if (client == null) {
return ERROR_MODEL;
}
model.addAttribute(AUTO_APPROVE, client.getAutoapprove());
model.addAttribute(USERNAME, user.getAccount());
model.addAllAttributes(request.getParameters());
return AUTHORIZE_MODEL; // 用户已登录,显示授权页面
})
.orElse(REDIRECT_URL + LOGIN_URL); // 用户未登录,重定向到登录页面
}
@PostMapping("/oauth/authorize/perform")
public String performAuthorize(@RequestParam(required = false) String state,
@SessionAttribute(AUTHORIZATION_REQUEST_KEY) OAuth2AuthorizationRequest authorizationRequest,
RedirectAttributes redirectAttributes, HttpSession session) {
// 获取客户端信息
OAuth2Client client = clientService.loadByClientId(authorizationRequest.getClientId());
// 校验回调地址信息
if (!clientService.validateRedirectUri(client, authorizationRequest.getRedirectUri())) {
// 重定向URI参数不匹配,返回错误页面
return ERROR_MODEL;
}
// 生成授权码
String code = createCode();
// 设置用户信息
OAuth2User user = (OAuth2User) session.getAttribute(AUTHORIZATION_SESSION_KEY);
if (user == null) {
// 用户未登录,重定向到登录页面
return REDIRECT_URL + LOGIN_URL;
}
// 校验state参数
if (Func.equalsSafe(authorizationRequest.getState(), state)) {
// 保存授权码
saveCode(code, user);
} else {
// 重定向URI和state参数不匹配,返回错误页面
return ERROR_MODEL;
}
// 使用RedirectAttributes添加授权码和state参数
redirectAttributes.addAttribute(CODE, code);
// 添加tenantId参数为state参数
if (authorizationRequest.getTenantId() != null) {
redirectAttributes.addAttribute(STATE, authorizationRequest.getTenantId());
}
// 用户自定义state参数则覆盖
if (state != null) {
redirectAttributes.addAttribute(STATE, state);
}
// 重定向到客户端提供的重定向URI
return REDIRECT_URL + authorizationRequest.getRedirectUri();
}
@GetMapping("/oauth/authorize/logout")
public String logout(HttpSession session) {
// 创建 OAuth2 请求对象并构建参数
OAuth2Request request = OAuth2Request.create().buildParameterArgs();
// 退出登录,清除session中的用户信息
if (session != null) {
// 销毁当前会话
session.removeAttribute(AUTHORIZATION_SESSION_KEY);
}
if (StringUtil.isNotBlank(request.getRedirectUri())) {
// 重定向到客户端提供的重定向URI
return REDIRECT_URL + request.getRedirectUri();
}
// 重定向到默认登录页面
return REDIRECT_URL + LOGIN_URL;
}
@GetMapping("/oauth/error")
public String error() {
// 返回错误页面
return ERROR_MODEL;
}
private OAuth2User authenticateUser(HttpSession session, OAuth2AuthorizationRequest authorizationRequest) {
// 创建 OAuth2 请求对象并构建参数
OAuth2Request request = OAuth2Request.create().buildParameterArgs().buildHeaderArgs();
// 获取请求参数
String username = request.getUsername();
String password = request.getPassword();
String clientId = authorizationRequest.getClientId();
String redirectUri = authorizationRequest.getRedirectUri();
// 获取客户端信息
OAuth2Client client = clientService.loadByClientId(clientId);
// 校验回调地址信息
if (!clientService.validateRedirectUri(client, redirectUri)) {
return null;
}
// 获取用户信息
OAuth2User user = userService.loadByUsername(username, request);
// 校验用户信息
if (!userService.validateUser(user)) {
return null;
}
// 校验用户密码
if (!passwordHandler.matches(password, user.getPassword())) {
return null;
}
// 将用户信息存入session
session.setAttribute(AUTHORIZATION_SESSION_KEY, user);
// 返回用户信息
return user;
}
/**
* 授权码模式获取授权码
*
* @return 授权码
*/
private String createCode() {
// 生成6位随机数作为授权码
String code = StringUtil.random(6);
if (bladeRedis.exists(OAuth2CodeUtil.codeKey(code))) {
// 如果生成的授权码已存在,则递归调用重新生成
return createCode();
}
return code;
}
/**
* 保存code信息
*/
private void saveCode(String code, OAuth2User user) {
// 保存code信息到redis30分钟过期
bladeRedis.setEx(OAuth2CodeUtil.codeKey(code), user, Duration.ofMinutes(30));
}
/**
* 根据code获取用户信息
*
* @param code code
* @return 用户信息
*/
public OAuth2User getUserByCode(String code) {
// 根据code从redis中获取用户信息
return bladeRedis.get(OAuth2CodeUtil.codeKey(code));
}
}
@@ -0,0 +1,102 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.endpoint;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils;
import org.springblade.core.social.props.SocialProperties;
import org.springblade.core.social.utils.SocialUtil;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
/**
* 第三方登录端点
*
* @author BladeX
*/
@Slf4j
@RestController
@AllArgsConstructor
@ConditionalOnProperty(value = "social.enabled", havingValue = "true")
@Tag(name = "开放平台登录", description = "3 - 开放平台登录端点")
public class OAuth2SocialEndpoint {
private final SocialProperties socialProperties;
/**
* 授权完毕跳转
*/
@Operation(summary = "授权完毕跳转")
@RequestMapping("/oauth/render/{source}")
public void renderAuth(@PathVariable("source") String source, HttpServletResponse response) throws IOException {
AuthRequest authRequest = SocialUtil.getAuthRequest(source, socialProperties);
String authorizeUrl = authRequest.authorize(AuthStateUtils.createState());
response.sendRedirect(authorizeUrl);
}
/**
* 获取认证信息
*/
@Operation(summary = "获取认证信息")
@RequestMapping("/oauth/callback/{source}")
public Object login(@PathVariable("source") String source, AuthCallback callback) {
AuthRequest authRequest = SocialUtil.getAuthRequest(source, socialProperties);
return authRequest.login(callback);
}
/**
* 撤销授权
*/
@Operation(summary = "撤销授权")
@RequestMapping("/oauth/revoke/{source}/{token}")
public Object revokeAuth(@PathVariable("source") String source, @PathVariable("token") String token) {
AuthRequest authRequest = SocialUtil.getAuthRequest(source, socialProperties);
return authRequest.revoke(AuthToken.builder().accessToken(token).build());
}
/**
* 续期accessToken
*/
@Operation(summary = "续期令牌")
@RequestMapping("/oauth/refresh/{source}")
public Object refreshAuth(@PathVariable("source") String source, String token) {
AuthRequest authRequest = SocialUtil.getAuthRequest(source, socialProperties);
return authRequest.refresh(AuthToken.builder().refreshToken(token).build());
}
}
@@ -0,0 +1,244 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.endpoint;
import com.wf.captcha.SpecCaptcha;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springblade.core.cache.utils.CacheUtil;
import org.springblade.core.jwt.JwtUtil;
import org.springblade.core.jwt.props.JwtProperties;
import org.springblade.core.launch.props.BladeProperties;
import org.springblade.core.oauth2.behavior.Behavior;
import org.springblade.core.oauth2.behavior.BehaviorFactory;
import org.springblade.core.oauth2.behavior.BehaviorGenerator;
import org.springblade.core.oauth2.constant.OAuth2ParameterConstant;
import org.springblade.core.oauth2.exception.OAuth2Exception;
import org.springblade.core.oauth2.granter.TokenGranter;
import org.springblade.core.oauth2.granter.TokenGranterFactory;
import org.springblade.core.oauth2.handler.AuthorizationHandler;
import org.springblade.core.oauth2.handler.TokenHandler;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.provider.OAuth2Response;
import org.springblade.core.oauth2.provider.OAuth2Token;
import org.springblade.core.oauth2.provider.OAuth2Validation;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.utils.OAuth2ExceptionUtil;
import org.springblade.core.oauth2.utils.OAuth2LogUtil;
import org.springblade.core.redis.cache.BladeRedis;
import org.springblade.core.secure.BladeUser;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.support.Kv;
import org.springblade.core.tool.utils.CharPool;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
import java.util.UUID;
import static org.springblade.core.cache.constant.CacheConstant.*;
import static org.springblade.core.oauth2.constant.OAuth2TokenConstant.*;
/**
* OAuth2认证端点
*
* @author BladeX
*/
@RestController
@AllArgsConstructor
@Tag(name = "用户授权认证", description = "1 - OAuth2授权认证端点")
public class OAuth2TokenEndPoint {
private final BladeRedis bladeRedis;
private final JwtProperties jwtProperties;
private final BladeProperties bladeProperties;
private final TokenGranterFactory granterFactory;
private final AuthorizationHandler authorizationHandler;
private final TokenHandler tokenHandler;
@PostMapping("/oauth/token")
@Operation(
summary = "获取Token",
description = "OAuth2认证接口",
parameters = {
@Parameter(in = ParameterIn.QUERY, name = OAuth2ParameterConstant.USERNAME, description = "账号", schema = @Schema(type = "string")),
@Parameter(in = ParameterIn.QUERY, name = OAuth2ParameterConstant.PASSWORD, description = "密码", schema = @Schema(type = "string")),
@Parameter(in = ParameterIn.QUERY, name = OAuth2ParameterConstant.GRANT_TYPE, description = "授权类型", schema = @Schema(type = "string")),
@Parameter(in = ParameterIn.QUERY, name = OAuth2ParameterConstant.REFRESH_TOKEN, description = "刷新token", schema = @Schema(type = "string")),
@Parameter(in = ParameterIn.QUERY, name = OAuth2ParameterConstant.SCOPE, description = "权限范围", schema = @Schema(type = "string"))
}
)
public ResponseEntity<Kv> token() {
// 创建 OAuth2 请求对象并构建参数
OAuth2Request request = OAuth2Request.create().buildArgs();
// 使用授权处理器对登录前的用户进行验证
OAuth2Validation preValidation = authorizationHandler.preValidation(request);
if (!preValidation.isSuccess()) {
// 认证失败处理逻辑
authorizationHandler.preFailure(request, preValidation);
return ResponseEntity.ok(OAuth2Response.create().ofFailure(preValidation.getCode(), preValidation.getMessage()));
}
// 根据请求的授权类型创建对应的 TokenGranter
TokenGranter tokenGranter = granterFactory.create(request.getGrantType());
// 使用 TokenGranter 获取用户信息
OAuth2User user;
try {
// 使用 TokenGranter 获取用户信息
user = tokenGranter.user(request);
} catch (OAuth2Exception ex) {
// 认证失败日志记录
OAuth2LogUtil.logOAuth2Exception(ex, request, bladeProperties.isProd());
// 认证失败处理逻辑
authorizationHandler.preFailure(request, OAuth2Validation.create().setSuccess(false)
.setCode(ex.getExceptionCode().getCode()).setMessage(ex.getMessage()));
return ResponseEntity.ok(OAuth2Response.create().ofFailure(ex.getExceptionCode().getCode(), ex.getMessage()));
}
// 使用授权处理器对登录后的用户进行验证
OAuth2Validation authValidation = authorizationHandler.authValidation(user, request);
// 检查验证是否成功
if (!authValidation.isSuccess()) {
// 验证失败处理逻辑
authorizationHandler.authFailure(user, request, authValidation);
// 根据验证失败的错误代码抛出异常
OAuth2ExceptionUtil.throwFromCode(authValidation.getCode());
}
// 创建令牌
OAuth2Token token = tokenGranter.token(user, request);
// 对令牌进行增强处理
OAuth2Token enhanceToken = tokenHandler.enhance(user, token, request);
// 验证成功处理逻辑
authorizationHandler.authSuccessful(user, request);
// 返回增强后的令牌
return ResponseEntity.ok(enhanceToken.getArgs());
}
@GetMapping("/oauth/logout")
@Operation(summary = "退出登录")
public ResponseEntity<Kv> logout() {
BladeUser user = AuthUtil.getUser();
// 退出登录,清除有状态模式下缓存的token
if (user != null && jwtProperties.getState()) {
OAuth2Request request = OAuth2Request.create().buildHeaderArgs();
String token = JwtUtil.getToken(request.getToken());
JwtUtil.removeAccessToken(user.getTenantId(), user.getClientId(), String.valueOf(user.getUserId()), token);
JwtUtil.removeRefreshToken(user.getTenantId(), user.getClientId(), String.valueOf(user.getUserId()), token);
}
return ResponseEntity.ok(OAuth2Response.create().ofSuccessful("退出登录成功"));
}
@GetMapping("/oauth/captcha")
@Operation(summary = "获取验证码")
public ResponseEntity<Kv> captcha() {
SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 5);
String verCode = specCaptcha.text().toLowerCase();
String key = UUID.randomUUID().toString();
// 存入redis并设置过期时间为30分钟
bladeRedis.setEx(CAPTCHA_CACHE_KEY + key, verCode, Duration.ofMinutes(30));
// 将key和base64返回给前端
return ResponseEntity.ok(OAuth2Response.create().ofSuccessful("获取验证码成功").set("key", key).set("image", specCaptcha.toBase64()));
}
@GetMapping("/oauth/behavior")
@Operation(summary = "获取行为验证码")
public ResponseEntity<Kv> behavior() {
// 随机选取模式出题,模式与答案一并留存,校验时不信任客户端上送的类型
Behavior behavior = BehaviorFactory.random().generate();
String key = UUID.randomUUID().toString();
bladeRedis.setEx(BEHAVIOR_CACHE_KEY + key, behavior.getType() + StringPool.PIPE + behavior.getAnswer(), Duration.ofMinutes(5));
return ResponseEntity.ok(OAuth2Response.create().ofSuccessful("获取行为验证码成功")
.set("key", key).set("type", behavior.getType()).set("data", behavior.getData()));
}
@PostMapping("/oauth/behavior/check")
@Operation(
summary = "校验行为验证码",
parameters = {
@Parameter(in = ParameterIn.QUERY, name = "key", description = "验证码key", schema = @Schema(type = "string")),
@Parameter(in = ParameterIn.QUERY, name = "answer", description = "用户作答内容", schema = @Schema(type = "string"))
}
)
public ResponseEntity<Kv> checkBehavior(String key, String answer) {
// 答案一次性消费,校验失败须重新出题,防止对同一题目穷举作答
String cache = bladeRedis.getAndDel(BEHAVIOR_CACHE_KEY + key);
if (StringUtil.isBlank(cache) || StringUtil.isBlank(answer)) {
return ResponseEntity.ok(OAuth2Response.create().ofFailure(400, BEHAVIOR_INVALID));
}
int separator = cache.indexOf(CharPool.PIPE);
BehaviorGenerator generator = BehaviorFactory.of(cache.substring(0, separator));
if (generator == null || !generator.verify(cache.substring(separator + 1), answer)) {
return ResponseEntity.ok(OAuth2Response.create().ofFailure(400, BEHAVIOR_NOT_CORRECT));
}
// 校验通过发放一次性通行票据,登录时由行为验证码授权器核销
String ticket = UUID.randomUUID().toString();
bladeRedis.setEx(BEHAVIOR_TICKET_KEY + key, ticket, Duration.ofMinutes(3));
return ResponseEntity.ok(OAuth2Response.create().ofSuccessful("行为验证通过").set("ticket", ticket));
}
@GetMapping("/oauth/user-info")
@Operation(summary = "获取用户信息")
public ResponseEntity<BladeUser> userInfo(BladeUser user) {
return ResponseEntity.ok(user);
}
@GetMapping("/oauth/clear-cache")
@Operation(summary = "清除缓存")
public ResponseEntity<Kv> clearCache() {
CacheUtil.clear(BIZ_CACHE);
CacheUtil.clear(USER_CACHE);
CacheUtil.clear(DICT_CACHE);
CacheUtil.clear(FLOW_CACHE);
CacheUtil.clear(SYS_CACHE);
CacheUtil.clear(PARAM_CACHE);
CacheUtil.clear(RESOURCE_CACHE);
CacheUtil.clear(MENU_CACHE);
CacheUtil.clear(DICT_CACHE, Boolean.FALSE);
CacheUtil.clear(MENU_CACHE, Boolean.FALSE);
CacheUtil.clear(SYS_CACHE, Boolean.FALSE);
CacheUtil.clear(PARAM_CACHE, Boolean.FALSE);
return ResponseEntity.ok(OAuth2Response.create().ofSuccessful("清除缓存成功"));
}
}
@@ -0,0 +1,43 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.exception;
/**
* 客户端认证失败
*
* @author BladeX
*/
public class ClientInvalidException extends OAuth2Exception {
public ClientInvalidException(String msg) {
super(ExceptionCode.INVALID_CLIENT, msg);
}
public ClientInvalidException(String msg, Throwable cause) {
super(ExceptionCode.INVALID_CLIENT, msg, cause);
}
}
@@ -0,0 +1,43 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.exception;
/**
* 客户端未找到
*
* @author BladeX
*/
public class ClientNotFoundException extends OAuth2Exception {
public ClientNotFoundException(String msg) {
super(ExceptionCode.CLIENT_NOT_FOUND, msg);
}
public ClientNotFoundException(String msg, Throwable cause) {
super(ExceptionCode.CLIENT_NOT_FOUND, msg, cause);
}
}
@@ -0,0 +1,43 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.exception;
/**
* 客户端未授权
*
* @author BladeX
*/
public class ClientUnauthorizedException extends OAuth2Exception {
public ClientUnauthorizedException(String msg) {
super(ExceptionCode.UNAUTHORIZED_CLIENT, msg);
}
public ClientUnauthorizedException(String msg, Throwable cause) {
super(ExceptionCode.UNAUTHORIZED_CLIENT, msg, cause);
}
}
@@ -0,0 +1,155 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
import static org.springblade.core.oauth2.exception.OAuth2ErrorMessage.INVALID_ERROR_CODE;
/**
* OAuth2ExceptionCode
*
* @author BladeX
*/
@Getter
@AllArgsConstructor
public enum ExceptionCode implements Oauth2ExceptionCode {
/**
* 无效请求 - 请求缺少必要的参数或格式不正确。
*/
INVALID_REQUEST(OAuth2ErrorCode.INVALID_REQUEST, "无效请求"),
/**
* 用户不存在 - 指定的用户ID不存在或无效。
*/
USER_NOT_FOUND(OAuth2ErrorCode.USER_NOT_FOUND, "用户不存在"),
/**
* 用户租户不存在 - 指定的用户租户未授权。
*/
USER_TENANT_NOT_FOUND(OAuth2ErrorCode.USER_TENANT_NOT_FOUND, "用户租户不存在"),
/**
* 用户登录失败次数过多 - 用户登录失败次数过多。
*/
USER_TOO_MANY_FAILS(OAuth2ErrorCode.USER_TOO_MANY_FAILS, "用户登录失败次数过多"),
/**
* 用户认证失败 - 指定的用户认证信息错误或无效。
*/
INVALID_USER(OAuth2ErrorCode.INVALID_USER, "认证信息错误或无效"),
/**
* 用户密码不合法 - 指定的用户密码不合法。
*/
INVALID_USER_PASSWORD(OAuth2ErrorCode.INVALID_USER, "用户密码强度过低"),
/**
* 用户未授权 - 指定的用户未授权。
*/
UNAUTHORIZED_USER(OAuth2ErrorCode.UNAUTHORIZED_USER, "认证信息错误或无效"),
/**
* 用户租户未授权 - 指定的用户租户未授权。
*/
UNAUTHORIZED_USER_TENANT(OAuth2ErrorCode.UNAUTHORIZED_USER_TENANT, "用户租户未授权"),
/**
* 用户未授权 - 指定的用户未授权。
*/
INVALID_REFRESH_TOKEN(OAuth2ErrorCode.INVALID_REFRESH_TOKEN, "令牌刷新错误或无效"),
/**
* 客户端不存在 - 指定的客户端ID不存在或无效。
*/
CLIENT_NOT_FOUND(OAuth2ErrorCode.CLIENT_NOT_FOUND, "客户端不存在"),
/**
* 客户端认证失败 - 客户端提供的认证信息错误或无效。
*/
INVALID_CLIENT(OAuth2ErrorCode.INVALID_CLIENT, "客户端认证失败"),
/**
* 回调地址错误或无效 - 客户端回调地址错误或无效。
*/
INVALID_CLIENT_REDIRECT_URI(OAuth2ErrorCode.INVALID_CLIENT_REDIRECT_URI, "客户端未授权"),
/**
* 客户端未授权 - 客户端无权执行此操作。
*/
UNAUTHORIZED_CLIENT(OAuth2ErrorCode.UNAUTHORIZED_CLIENT, "客户端未授权"),
/**
* 不支持的授权类型 - 请求的授权类型不被服务器支持。
*/
UNSUPPORTED_GRANT_TYPE(OAuth2ErrorCode.UNSUPPORTED_GRANT_TYPE, "不支持的授权类型"),
/**
* 无效的授权类型 - 提供的授权令牌无效、过期或被撤销。
*/
INVALID_GRANTER(OAuth2ErrorCode.INVALID_GRANTER, "无效的授权类型"),
/**
* 无效的无效的授权范围 - 请求的无效的授权范围无效、未知或格式不正确。
*/
INVALID_SCOPE(OAuth2ErrorCode.INVALID_SCOPE, "授权范围"),
/**
* 服务器错误 - 服务器内部错误,无法完成请求。
*/
SERVER_ERROR(OAuth2ErrorCode.SERVER_ERROR, "服务器错误"),
/**
* 访问被拒绝 - 由于各种原因,服务器拒绝执行此操作。
*/
ACCESS_DENIED(OAuth2ErrorCode.ACCESS_DENIED, "访问被拒绝"),
/**
* 服务暂不可用 - 服务器暂时过载或维护,无法处理请求。
*/
TEMPORARILY_UNAVAILABLE(OAuth2ErrorCode.TEMPORARILY_UNAVAILABLE, "服务暂不可用");
final int code;
final String message;
/**
* 通过错误代码获取枚举
*
* @param code 错误代码
* @return ExceptionCodeEnum
*/
public static ExceptionCode of(int code) {
for (ExceptionCode value : ExceptionCode.values()) {
if (value.code == code) {
return value;
}
}
throw new IllegalArgumentException(String.format(INVALID_ERROR_CODE, code));
}
}
@@ -0,0 +1,43 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.exception;
/**
* 无效的授权
*
* @author BladeX
*/
public class GranterInvalidException extends OAuth2Exception {
public GranterInvalidException(String msg) {
super(ExceptionCode.INVALID_GRANTER, msg);
}
public GranterInvalidException(String msg, Throwable cause) {
super(ExceptionCode.INVALID_GRANTER, msg, cause);
}
}
@@ -0,0 +1,106 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.exception;
/**
* OAuth2ErrorCodes
*
* @author BladeX
*/
public interface OAuth2ErrorCode {
/**
* 无效请求 - 请求缺少必要的参数或格式不正确。
*/
int INVALID_REQUEST = 2000;
/**
* 用户不存在 - 指定的用户ID不存在或无效。
*/
int USER_NOT_FOUND = 2001;
/**
* 用户租户不存在 - 指定的用户租户未授权。
*/
int USER_TENANT_NOT_FOUND = 2002;
/**
* 用户登录失败次数过多 - 用户登录失败次数过多。
*/
int USER_TOO_MANY_FAILS = 2003;
/**
* 用户认证失败 - 指定的用户认证信息错误或无效。
*/
int INVALID_USER = 2004;
/**
* 用户未授权 - 指定的用户未授权。
*/
int UNAUTHORIZED_USER = 2005;
/**
* 用户租户未授权 - 指定的用户租户未授权。
*/
int UNAUTHORIZED_USER_TENANT = 2006;
/**
* 令牌刷新错误或无效 - 刷新令牌认证信息错误或无效。
*/
int INVALID_REFRESH_TOKEN = 2010;
/**
* 客户端不存在 - 指定的客户端ID不存在或无效。
*/
int CLIENT_NOT_FOUND = 3000;
/**
* 客户端认证失败 - 客户端提供的认证信息错误或无效。
*/
int INVALID_CLIENT = 3001;
/**
* 回调地址错误或无效 - 客户端回调地址错误或无效。
*/
int INVALID_CLIENT_REDIRECT_URI = 3002;
/**
* 客户端未授权 - 客户端无权执行此操作。
*/
int UNAUTHORIZED_CLIENT = 3003;
/**
* 不支持的授权类型 - 请求的授权类型不被服务器支持。
*/
int UNSUPPORTED_GRANT_TYPE = 4000;
/**
* 无效的授权类型 - 提供的授权类型无效、过期或被撤销。
*/
int INVALID_GRANTER = 4001;
/**
* 无效的授权范围 - 请求的授权范围无效、未知或格式不正确。
*/
int INVALID_SCOPE = 4002;
/**
* 服务器错误 - 服务器内部错误,无法完成请求。
*/
int SERVER_ERROR = 5000;
/**
* 访问被拒绝 - 由于各种原因,服务器拒绝执行此操作。
*/
int ACCESS_DENIED = 5001;
/**
* 服务暂不可用 - 服务器暂时过载或维护,无法处理请求。
*/
int TEMPORARILY_UNAVAILABLE = 5002;
}
@@ -0,0 +1,47 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.exception;
/**
* OAuth2ErrorMessage
*
* @author BladeX
*/
public interface OAuth2ErrorMessage {
String INVALID_GRANT_TYPE = "无效的授权类型: %s";
String CLIENT_AUTHORIZATION_FAILED = "客户端认证失败, 请检查请求头 [Authorization] 信息";
String CLIENT_TOKEN_PARSE_FAILED = "客户端令牌解析失败";
String INVALID_CLIENT_TOKEN = "客户端令牌不合法";
String AUTHORIZATION_NOT_FOUND = "请求头中未找到 [Authorization] 信息";
String INVALID_ERROR_CODE = "无效的错误代码: %s";
String USER_HAS_NO_TENANT = "未获得用户的租户信息";
String USER_HAS_NO_TENANT_PERMISSION = "租户授权已过期,请联系管理员";
String USER_HAS_TOO_MANY_FAILS = "登录错误次数过多,请稍后再试";
}
@@ -0,0 +1,75 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.exception;
import lombok.Getter;
import java.io.Serial;
/**
* OAuth2通用异常
*
* @author BladeX
*/
@Getter
public class OAuth2Exception extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
private final Oauth2ExceptionCode exceptionCode;
public OAuth2Exception(String message) {
super(message);
this.exceptionCode = ExceptionCode.ACCESS_DENIED;
}
public OAuth2Exception(Oauth2ExceptionCode exceptionCode) {
super(exceptionCode.getMessage());
this.exceptionCode = exceptionCode;
}
public OAuth2Exception(Oauth2ExceptionCode exceptionCode, String message) {
super(message);
this.exceptionCode = exceptionCode;
}
public OAuth2Exception(Oauth2ExceptionCode exceptionCode, Throwable cause) {
super(cause);
this.exceptionCode = exceptionCode;
}
public OAuth2Exception(Oauth2ExceptionCode exceptionCode, String message, Throwable cause) {
super(message, cause);
this.exceptionCode = exceptionCode;
}
@Override
public Throwable fillInStackTrace() {
return this;
}
}
@@ -0,0 +1,62 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.exception;
import io.jsonwebtoken.JwtException;
import org.springblade.core.oauth2.endpoint.OAuth2AuthorizationEndpoint;
import org.springblade.core.oauth2.endpoint.OAuth2SocialEndpoint;
import org.springblade.core.oauth2.endpoint.OAuth2TokenEndPoint;
import org.springblade.core.oauth2.provider.OAuth2Response;
import org.springblade.core.secure.exception.SecureException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
/**
* OAuth2ExceptionHandler
*
* @author BladeX
*/
@ControllerAdvice(basePackageClasses = {OAuth2AuthorizationEndpoint.class, OAuth2SocialEndpoint.class, OAuth2TokenEndPoint.class})
public class OAuth2ExceptionHandler {
@ExceptionHandler(OAuth2Exception.class)
public ResponseEntity<?> handleOAuth2Exception(OAuth2Exception ex) {
// 统一处理验证失败的情况
return ResponseEntity.ok(OAuth2Response.create().ofFailure(ex.getExceptionCode().getCode(), ex.getMessage()));
}
@ExceptionHandler(SecureException.class)
public ResponseEntity<?> handleSecureException(SecureException ex) {
// 统一处理验证失败的情况
return ResponseEntity.ok(OAuth2Response.create().ofFailure(OAuth2ErrorCode.INVALID_USER, ex.getMessage()));
}
@ExceptionHandler(JwtException.class)
public ResponseEntity<?> handleJwtException(JwtException ex) {
// 统一处理验证失败的情况
return ResponseEntity.ok(OAuth2Response.create().ofFailure(OAuth2ErrorCode.ACCESS_DENIED, ex.getMessage()));
}
}
@@ -0,0 +1,47 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.exception;
/**
* Oauth2ExceptionCode
*
* @author BladeX
*/
public interface Oauth2ExceptionCode {
/**
* 获取异常编码
*
* @return int
*/
int getCode();
/**
* 获取异常消息
*
* @return String
*/
String getMessage();
}
@@ -0,0 +1,43 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.exception;
/**
* 用户认证失败
*
* @author BladeX
*/
public class UserInvalidException extends OAuth2Exception {
public UserInvalidException(String msg) {
super(ExceptionCode.INVALID_USER, msg);
}
public UserInvalidException(String msg, Throwable cause) {
super(ExceptionCode.INVALID_USER, msg, cause);
}
}
@@ -0,0 +1,43 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.exception;
/**
* 用户未授权
*
* @author BladeX
*/
public class UserUnauthorizedException extends OAuth2Exception {
public UserUnauthorizedException(String msg) {
super(ExceptionCode.UNAUTHORIZED_USER, msg);
}
public UserUnauthorizedException(String msg, Throwable cause) {
super(ExceptionCode.UNAUTHORIZED_USER, msg, cause);
}
}
@@ -0,0 +1,43 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.exception;
/**
* 用户名未找到
*
* @author BladeX
*/
public class UsernameNotFoundException extends OAuth2Exception {
public UsernameNotFoundException(String msg) {
super(ExceptionCode.USER_NOT_FOUND, msg);
}
public UsernameNotFoundException(String msg, Throwable cause) {
super(ExceptionCode.USER_NOT_FOUND, msg, cause);
}
}
@@ -0,0 +1,189 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.granter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.launch.constant.TokenConstant;
import org.springblade.core.oauth2.exception.OAuth2ErrorCode;
import org.springblade.core.oauth2.handler.PasswordHandler;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.provider.OAuth2Token;
import org.springblade.core.oauth2.service.OAuth2Client;
import org.springblade.core.oauth2.service.OAuth2ClientService;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.service.OAuth2UserService;
import org.springblade.core.oauth2.utils.OAuth2ExceptionUtil;
import org.springblade.core.oauth2.utils.OAuth2Util;
import org.springblade.core.secure.TokenInfo;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import java.time.Instant;
import java.util.Objects;
/**
* AbstractTokenGranter
*
* @author BladeX
*/
@Slf4j
@RequiredArgsConstructor
public abstract class AbstractTokenGranter implements TokenGranter {
/**
* 认证失败原因:用户不存在
*/
private static final String REASON_USER_NOT_FOUND = "用户不存在";
/**
* 认证失败原因:用户状态异常
*/
private static final String REASON_USER_INVALID = "用户状态异常";
/**
* 认证失败原因:密码匹配失败
*/
private static final String REASON_PASSWORD_MISMATCH = "密码匹配失败";
private final OAuth2ClientService clientService;
private final OAuth2UserService userService;
private final PasswordHandler passwordHandler;
protected TokenGranterEnhancer enhancer;
public abstract String type();
@Override
public void enhancer(TokenGranterEnhancer enhancer) {
this.enhancer = enhancer;
}
@Override
public OAuth2Client client(OAuth2Request request) {
// 获取clientId
String clientId = request.getClientId();
// 获取clientSecret
String clientSecret = request.getClientSecret();
// 获取客户端信息
OAuth2Client client = clientService.loadByClientId(clientId);
// 校验客户端信息
if (!clientService.validateClient(client, clientId, clientSecret)) {
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_CLIENT);
}
// 校验授权类型
if (!clientService.validateGranter(client, type())) {
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_GRANTER);
}
// 返回客户端信息
return client;
}
@Override
public OAuth2User user(OAuth2Request request) {
// 获取用户信息
OAuth2User user = (StringUtil.isNotBlank(request.getUserId()) && request.isRefreshToken())
? userService.loadByUserId(request.getUserId(), request)
: userService.loadByUsername(request.getUsername(), request);
// 用户不存在
if (user == null) {
rejectUser(request, REASON_USER_NOT_FOUND);
}
// 校验用户信息
if (!userService.validateUser(Objects.requireNonNull(user))) {
rejectUser(request, REASON_USER_INVALID);
}
// 校验用户密码
if (request.isCaptchaCode() || request.isPassword()) {
// 验证码模式和密码模式需要校验密码
boolean matches = passwordHandler.matches(request.getPassword(), user.getPassword());
if (!matches) {
rejectUser(request, REASON_PASSWORD_MISMATCH);
}
}
// 设置客户端信息
user.setClient(client(request));
// 返回用户信息
return user;
}
/**
* 拒绝认证
* <p>
* 将真实失败原因记录到服务端日志便于排查,对外统一返回通用错误以防止账号枚举。
*
* @param request 认证请求
* @param reason 真实失败原因
*/
private void rejectUser(OAuth2Request request, String reason) {
log.warn("OAuth2 认证失败 → {} | tenantId={}, username={}, grantType={}",
reason, request.getTenantId(), request.getUsername(), request.getGrantType());
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_USER);
}
@Override
public OAuth2Token token(OAuth2User user, OAuth2Request request) {
// 首次认证时间作为绝对有效期基线:刷新授权沿用旧令牌透传的时间,其余授权以当前时间为起点
long authTime = (request.getAuthTime() != null) ? request.getAuthTime() : Instant.now().getEpochSecond();
TokenInfo accessToken = OAuth2Util.createAccessToken(user);
TokenInfo refreshToken = OAuth2Util.createRefreshToken(user, authTime);
OAuth2Token token = OAuth2Token.create();
token.getArgs().set(TokenConstant.TENANT_ID, user.getTenantId())
.set(TokenConstant.USER_ID, user.getUserId())
.set(TokenConstant.DEPT_ID, user.getDeptId())
.set(TokenConstant.POST_ID, user.getPostId())
.set(TokenConstant.ROLE_ID, user.getRoleId())
.set(TokenConstant.OAUTH_ID, user.getOauthId())
.set(TokenConstant.ACCOUNT, user.getAccount())
.set(TokenConstant.USER_NAME, user.getAccount())
.set(TokenConstant.NICK_NAME, user.getName())
.set(TokenConstant.REAL_NAME, user.getRealName())
.set(TokenConstant.ROLE_NAME, Func.join(user.getAuthorities()))
.set(TokenConstant.AVATAR, Func.toStr(user.getAvatar(), TokenConstant.DEFAULT_AVATAR))
.set(TokenConstant.ACCESS_TOKEN, accessToken.getToken())
.set(TokenConstant.REFRESH_TOKEN, refreshToken.getToken())
.set(TokenConstant.TOKEN_TYPE, TokenConstant.BEARER)
.set(TokenConstant.EXPIRES_IN, accessToken.getExpire())
.set(TokenConstant.DETAIL, user.getDetail())
.set(TokenConstant.LICENSE, TokenConstant.LICENSE_NAME);
return token.setAccessToken(accessToken.getToken())
.setAccessTokenExpire(accessToken.getExpire())
.setRefreshToken(refreshToken.getToken())
.setRefreshTokenExpire(refreshToken.getExpire());
}
}
@@ -0,0 +1,102 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.granter;
import org.springblade.core.oauth2.exception.ExceptionCode;
import org.springblade.core.oauth2.exception.OAuth2ErrorCode;
import org.springblade.core.oauth2.exception.UserInvalidException;
import org.springblade.core.oauth2.handler.PasswordHandler;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.service.OAuth2Client;
import org.springblade.core.oauth2.service.OAuth2ClientService;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.service.OAuth2UserService;
import org.springblade.core.oauth2.utils.OAuth2CodeUtil;
import org.springblade.core.oauth2.utils.OAuth2ExceptionUtil;
import org.springblade.core.redis.cache.BladeRedis;
import org.springblade.core.tool.utils.ObjectUtil;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.stereotype.Component;
import java.util.Optional;
/**
* AuthorizationCodeGranter
*
* @author BladeX
*/
@Component
public class AuthorizationCodeGranter extends AbstractTokenGranter {
private final OAuth2UserService userService;
private final PasswordHandler passwordHandler;
private final BladeRedis bladeRedis;
public AuthorizationCodeGranter(OAuth2ClientService clientService, OAuth2UserService userService, PasswordHandler passwordHandler, BladeRedis bladeRedis) {
super(clientService, userService, passwordHandler);
this.userService = userService;
this.passwordHandler = passwordHandler;
this.bladeRedis = bladeRedis;
}
@Override
public String type() {
return AUTHORIZATION_CODE;
}
@Override
public OAuth2User user(OAuth2Request request) {
// 获取客户端信息并校验
OAuth2Client client = client(request);
if (!StringUtil.equals(client.getWebServerRedirectUri(), request.getRedirectUri())) {
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_CLIENT_REDIRECT_URI);
}
// 根据code获取用户信息
String code = request.getCode();
OAuth2User user = bladeRedis.get(OAuth2CodeUtil.codeKey(code));
// 判断用户是否存在
if (ObjectUtil.isNotEmpty(user)) {
// 校验用户信息
if (!userService.validateUser(user)) {
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_USER);
}
// 校验用户密码
if ((request.isCaptchaCode() || request.isPassword()) && !passwordHandler.matches(request.getPassword(), user.getPassword())) {
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_USER);
}
// 设置客户端信息
user.setClient(client);
// 删除缓存的code信息防止二次认证
bladeRedis.del(OAuth2CodeUtil.codeKey(code));
// 返回user
return Optional.ofNullable(this.enhancer)
.map(enhancer -> enhancer.enhance(user, request))
.orElse(user);
}
throw new UserInvalidException(ExceptionCode.INVALID_USER.getMessage());
}
}
@@ -0,0 +1,92 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.granter;
import org.springblade.core.launch.constant.TokenConstant;
import org.springblade.core.oauth2.handler.PasswordHandler;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.provider.OAuth2Token;
import org.springblade.core.oauth2.service.OAuth2Client;
import org.springblade.core.oauth2.service.OAuth2ClientService;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.service.OAuth2UserService;
import org.springblade.core.oauth2.service.impl.OAuth2UserDetail;
import org.springblade.core.oauth2.utils.OAuth2Util;
import org.springblade.core.secure.TokenInfo;
import org.springblade.core.tool.utils.Func;
import org.springframework.stereotype.Component;
import java.util.Optional;
/**
* ClientCredentialsGranter
*
* @author BladeX
*/
@Component
public class ClientCredentialsGranter extends AbstractTokenGranter {
public ClientCredentialsGranter(OAuth2ClientService clientService, OAuth2UserService userService, PasswordHandler passwordHandler) {
super(clientService, userService, passwordHandler);
}
@Override
public String type() {
return CLIENT_CREDENTIALS;
}
@Override
public OAuth2User user(OAuth2Request request) {
OAuth2UserDetail user = new OAuth2UserDetail();
OAuth2Client client = client(request);
user.setTenantId(request.getTenantId());
user.setClient(client);
user.setAccount(client.getClientId());
user.setName(client.getClientId());
return Optional.ofNullable(this.enhancer)
.map(enhancer -> enhancer.enhance(user, request))
.orElse(user);
}
@Override
public OAuth2Token token(OAuth2User user, OAuth2Request request) {
TokenInfo accessToken = OAuth2Util.createClientAccessToken(user);
OAuth2Token token = OAuth2Token.create();
token.getArgs().set(TokenConstant.TENANT_ID, user.getTenantId())
.set(TokenConstant.CLIENT_ID, user.getClient().getClientId())
.set(TokenConstant.AVATAR, Func.toStr(user.getAvatar(), TokenConstant.DEFAULT_AVATAR))
.set(TokenConstant.ACCESS_TOKEN, accessToken.getToken())
.set(TokenConstant.TOKEN_TYPE, TokenConstant.BEARER)
.set(TokenConstant.EXPIRES_IN, accessToken.getExpire())
.set(TokenConstant.DETAIL, user.getDetail())
.set(TokenConstant.LICENSE, TokenConstant.LICENSE_NAME);
return token.setAccessToken(accessToken.getToken()).setAccessTokenExpire(accessToken.getExpire());
}
}
@@ -0,0 +1,96 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.granter;
import org.springblade.core.launch.constant.TokenConstant;
import org.springblade.core.oauth2.handler.PasswordHandler;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.provider.OAuth2Token;
import org.springblade.core.oauth2.service.OAuth2Client;
import org.springblade.core.oauth2.service.OAuth2ClientService;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.service.OAuth2UserService;
import org.springblade.core.oauth2.service.impl.OAuth2ClientDetail;
import org.springblade.core.oauth2.service.impl.OAuth2UserDetail;
import org.springblade.core.oauth2.utils.OAuth2Util;
import org.springblade.core.secure.TokenInfo;
import org.springblade.core.tool.utils.Func;
import org.springframework.stereotype.Component;
import java.util.Optional;
/**
* ImplicitGranter
*
* @author BladeX
*/
@Component
public class ImplicitGranter extends AbstractTokenGranter {
public ImplicitGranter(OAuth2ClientService clientService, OAuth2UserService userService, PasswordHandler passwordHandler) {
super(clientService, userService, passwordHandler);
}
@Override
public String type() {
return IMPLICIT;
}
@Override
public OAuth2Client client(OAuth2Request request) {
return new OAuth2ClientDetail();
}
@Override
public OAuth2User user(OAuth2Request request) {
OAuth2UserDetail user = new OAuth2UserDetail();
user.setTenantId(request.getTenantId());
user.setAccount(request.getUsername());
user.setName(request.getUsername());
return Optional.ofNullable(this.enhancer)
.map(enhancer -> enhancer.enhance(user, request))
.orElse(user);
}
@Override
public OAuth2Token token(OAuth2User user, OAuth2Request request) {
TokenInfo accessToken = OAuth2Util.createImplicitAccessToken(user);
OAuth2Token token = OAuth2Token.create();
token.getArgs().set(TokenConstant.TENANT_ID, user.getTenantId())
.set(TokenConstant.USER_NAME, user.getAccount())
.set(TokenConstant.AVATAR, Func.toStr(user.getAvatar(), TokenConstant.DEFAULT_AVATAR))
.set(TokenConstant.ACCESS_TOKEN, accessToken.getToken())
.set(TokenConstant.TOKEN_TYPE, TokenConstant.BEARER)
.set(TokenConstant.EXPIRES_IN, accessToken.getExpire())
.set(TokenConstant.DETAIL, user.getDetail())
.set(TokenConstant.LICENSE, TokenConstant.LICENSE_NAME);
return token.setAccessToken(accessToken.getToken()).setAccessTokenExpire(accessToken.getExpire());
}
}
@@ -0,0 +1,62 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.granter;
import org.springblade.core.oauth2.handler.PasswordHandler;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.service.OAuth2ClientService;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.service.OAuth2UserService;
import org.springframework.stereotype.Component;
import java.util.Optional;
/**
* PasswordTokenGranter
*
* @author BladeX
*/
@Component
public class PasswordTokenGranter extends AbstractTokenGranter {
public PasswordTokenGranter(OAuth2ClientService clientService, OAuth2UserService userService, PasswordHandler passwordHandler) {
super(clientService, userService, passwordHandler);
}
@Override
public String type() {
return PASSWORD;
}
@Override
public OAuth2User user(OAuth2Request request) {
OAuth2User user = super.user(request);
return Optional.ofNullable(this.enhancer)
.map(enhancer -> enhancer.enhance(user, request))
.orElse(user);
}
}
@@ -0,0 +1,162 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.granter;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.jwt.JwtUtil;
import org.springblade.core.jwt.props.JwtProperties;
import org.springblade.core.launch.constant.TokenConstant;
import org.springblade.core.oauth2.exception.OAuth2ErrorCode;
import org.springblade.core.oauth2.handler.PasswordHandler;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.service.OAuth2ClientService;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.service.OAuth2UserService;
import org.springblade.core.oauth2.utils.OAuth2ExceptionUtil;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
import java.util.Optional;
/**
* PasswordTokenGranter
*
* @author BladeX
*/
@Slf4j
@Component
public class RefreshTokenGranter extends PasswordTokenGranter {
private final JwtProperties jwtProperties;
public RefreshTokenGranter(OAuth2ClientService clientService, OAuth2UserService userService, PasswordHandler passwordHandler, JwtProperties jwtProperties) {
super(clientService, userService, passwordHandler);
this.jwtProperties = jwtProperties;
}
@Override
public String type() {
return REFRESH_TOKEN;
}
@Override
public OAuth2User user(OAuth2Request request) {
String refreshToken = request.getRefreshToken();
// 刷新令牌缺失属于无效请求,提前拦截并返回明确的错误码,避免空令牌进入解析链路
if (StringUtil.isBlank(refreshToken)) {
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_REFRESH_TOKEN);
}
// 解析refreshToken,获取其中的声明信息
Claims refreshClaims = JwtUtil.parseJWT(refreshToken);
if (refreshClaims == null) {
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_USER);
}
// 校验refreshToken的合法性
if (!judgeRefreshToken(refreshClaims, refreshToken, request)) {
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_REFRESH_TOKEN);
}
// 校验refreshToken的格式
String tokenType = String.valueOf(Objects.requireNonNull(refreshClaims).get(TokenConstant.TOKEN_TYPE));
if (!StringUtil.equals(tokenType, TokenConstant.REFRESH_TOKEN)) {
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_REFRESH_TOKEN);
}
// 校验refreshToken是否可获取username
String userId = String.valueOf(refreshClaims.get(TokenConstant.USER_ID));
if (StringUtil.isBlank(userId)) {
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_USER);
}
// 设置username
request.setUserId(userId);
// 认证绝对有效期校验并透传首次认证时间
enforceAuthMaxAge(refreshClaims, request);
return super.user(request);
}
/**
* 校验认证是否超过最长有效期上限,并将首次认证时间透传给后续令牌构建。
* <p>
* 首次认证时间存放于刷新令牌的签名声明中,滑动续期时原样沿用而不重置,使认证总寿命存在硬性上限。
* 该校验不依赖服务端令牌状态,无状态模式下同样生效,用于收敛刷新令牌失窃后的最长可用时长。
* 存量令牌不含该声明时视为本次刷新起始,平滑兼容历史令牌,避免升级即触发全员重新登录。
*
* @param refreshClaims 刷新令牌声明
* @param request 授权请求
*/
private void enforceAuthMaxAge(Claims refreshClaims, OAuth2Request request) {
Long authMaxAge = jwtProperties.getAuthMaxAge();
if (authMaxAge == null || authMaxAge <= 0) {
return;
}
Object authTimeClaim = refreshClaims.get(TokenConstant.AUTH_TIME);
if (authTimeClaim == null) {
return;
}
long authTime = Long.parseLong(String.valueOf(authTimeClaim));
long elapsedSeconds = Instant.now().getEpochSecond() - authTime;
long maxAgeSeconds = Duration.ofDays(authMaxAge).getSeconds();
if (elapsedSeconds > maxAgeSeconds) {
log.warn("刷新令牌已超过认证最长有效期,拒绝续期 → userId={}, clientId={}, elapsedSeconds={}, maxAgeDays={}",
refreshClaims.get(TokenConstant.USER_ID), request.getClientId(), elapsedSeconds, authMaxAge);
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_REFRESH_TOKEN);
}
request.setAuthTime(authTime);
}
/**
* 校验refreshToken的合法性
*
* @param refreshToken 待校验的refreshToken
* @return refreshToken是否合法
*/
private boolean judgeRefreshToken(Claims refreshClaims, String refreshToken, OAuth2Request request) {
// 首先检查JWT是否启用单人登录模式,如果不是则直接返回true
if (!jwtProperties.getState() || !jwtProperties.getSingle()) {
return true;
}
// 解析JWT,如果无法解析则认为不合法
return Optional.ofNullable(refreshClaims)
.map(claims -> {
// 提取 tenantId clientId 和 userId
String tenantId = request.getTenantId();
String clientId = request.getClientId();
String userId = String.valueOf(claims.get(TokenConstant.USER_ID));
// 根据提取的信息和refreshToken生成新的token
String token = JwtUtil.getRefreshToken(tenantId, clientId, userId, refreshToken);
// 比较新生成的token与传入的refreshToken是否一致,如果一致则认为合法
return StringUtil.equalsIgnoreCase(token, refreshToken);
})
.orElse(false); // 如果claims为空,则返回false
}
}
@@ -0,0 +1,80 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.granter;
import org.springblade.core.oauth2.constant.OAuth2GranterConstant;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.provider.OAuth2Token;
import org.springblade.core.oauth2.service.OAuth2Client;
import org.springblade.core.oauth2.service.OAuth2User;
/**
* 授权认证统一接口.
*
* @author BladeX
*/
public interface TokenGranter extends OAuth2GranterConstant {
/**
* 获取授权模式
*
* @return String
*/
String type();
/**
* 获取客户端信息
*
* @param request 授权参数
* @return OAuth2Client
*/
OAuth2Client client(OAuth2Request request);
/**
* 获取用户信息
*
* @param request 授权参数
* @return OAuth2User
*/
OAuth2User user(OAuth2Request request);
/**
* 创建令牌
*
* @param user 用户信息
* @param request 授权参数
* @return OAuth2Token
*/
OAuth2Token token(OAuth2User user, OAuth2Request request);
/**
* 自定义增强
*
* @param enhancer enhancer
*/
void enhancer(TokenGranterEnhancer enhancer);
}
@@ -0,0 +1,55 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.granter;
import org.springblade.core.oauth2.constant.OAuth2GranterConstant;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.service.OAuth2User;
/**
* TokenGranterEnhancer
*
* @author BladeX
*/
public interface TokenGranterEnhancer extends OAuth2GranterConstant {
/**
* 获取授权模式
*
* @return String
*/
String type();
/**
* 增强用户令牌
*
* @param user 用户信息
* @param request 授权参数
* @return OAuth2User
*/
OAuth2User enhance(OAuth2User user, OAuth2Request request);
}
@@ -0,0 +1,121 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.granter;
import lombok.AllArgsConstructor;
import org.springblade.core.oauth2.exception.GranterInvalidException;
import org.springblade.core.oauth2.props.OAuth2Properties;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static org.springblade.core.oauth2.constant.OAuth2GranterConstant.*;
import static org.springblade.core.oauth2.exception.OAuth2ErrorMessage.INVALID_GRANT_TYPE;
/**
* TokenGranterFactory
*
* @author BladeX
*/
@AllArgsConstructor
public class TokenGranterFactory {
/**
* TokenGranter 集合
*/
private final List<TokenGranter> tokenGranters;
/**
* TokenGranterEnhancer 集合
*/
private final List<TokenGranterEnhancer> tokenGranterEnhancers;
/**
* OAuth2 属性配置
*/
private final OAuth2Properties properties;
/**
* TokenGranter 缓存池,使用 ConcurrentHashMap 保证线程安全。
*/
private static final Map<String, TokenGranter> GRANTER_POOL = new ConcurrentHashMap<>();
/**
* 根据授权模式获取 TokenGranter,如果缓存中不存在,则尝试创建。
*
* @param grantType 授权模式
* @return TokenGranter 实例
* @throws IllegalArgumentException 如果请求的授权类型不支持
*/
public TokenGranter create(String grantType) {
// 使用 computeIfAbsent 实现延迟加载
return GRANTER_POOL.computeIfAbsent(grantType, this::initializeTokenGranter);
}
/**
* 尝试根据授权类型初始化 TokenGranter。
*
* @param grantType 授权模式
* @return 初始化的 TokenGranter
* @throws GranterInvalidException 如果无法识别授权类型
*/
private TokenGranter initializeTokenGranter(String grantType) {
// 根据授权类型查找对应的 TokenGranter 并应用第一个找到的增强类
return tokenGranters.stream()
.filter(granter -> granter.type().equals(grantType) && isGranterEnabled(granter))
.peek(granter -> tokenGranterEnhancers.stream()
.filter(enhancer -> enhancer.type().equals(grantType))
.findFirst()
.ifPresent(granter::enhancer))
.findFirst()
.orElseThrow(() -> new GranterInvalidException(String.format(INVALID_GRANT_TYPE, grantType)));
}
/**
* 判断 TokenGranter 是否启用。
*
* @param granter TokenGranter 实例
* @return 是否启用
*/
private boolean isGranterEnabled(TokenGranter granter) {
return switch (granter.type()) {
case AUTHORIZATION_CODE -> properties.getGranter().getAuthorizationCode();
case PASSWORD -> properties.getGranter().getPassword();
case REFRESH_TOKEN -> properties.getGranter().getRefreshToken();
case CLIENT_CREDENTIALS -> properties.getGranter().getClientCredentials();
case IMPLICIT -> properties.getGranter().getImplicit();
case CAPTCHA -> properties.getGranter().getCaptcha();
case BEHAVIOR -> properties.getGranter().getBehavior();
case SMS_CODE -> properties.getGranter().getSmsCode();
case WECHAT_APPLET -> properties.getGranter().getWechatApplet();
case SOCIAL -> properties.getGranter().getSocial();
case REGISTER -> properties.getGranter().getRegister();
default -> true;
};
}
}
@@ -0,0 +1,120 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.handler;
import org.springblade.core.oauth2.exception.ExceptionCode;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.provider.OAuth2Validation;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.tool.utils.Func;
/**
* AbstractAuthorizationHandler
*
* @author BladeX
*/
public abstract class AbstractAuthorizationHandler implements AuthorizationHandler {
/**
* 认证前校验
*
* @param request 请求信息
* @return boolean
*/
@Override
public OAuth2Validation preValidation(OAuth2Request request) {
if (request.isClientCredentials() || request.isImplicit() || request.isSocial()) {
return new OAuth2Validation();
}
if (request.isPassword() || request.isCaptchaCode()) {
if (Func.hasEmpty(request.getUsername())) {
return buildValidationFailure(ExceptionCode.INVALID_USER);
}
}
return new OAuth2Validation();
}
/**
* 认证前失败回调
*
* @param validation 失败信息
*/
@Override
public abstract void preFailure(OAuth2Request request, OAuth2Validation validation);
/**
* 认证校验
*
* @param user 用户信息
* @param request 请求信息
* @return boolean
*/
@Override
public OAuth2Validation authValidation(OAuth2User user, OAuth2Request request) {
if (request.isClientCredentials() || request.isImplicit() || request.isSocial()) {
return new OAuth2Validation();
}
if (Func.hasEmpty(user, user.getUserId())) {
return buildValidationFailure(ExceptionCode.INVALID_USER);
}
if (Func.isEmpty(user.getAuthorities())) {
return buildValidationFailure(ExceptionCode.UNAUTHORIZED_USER);
}
return new OAuth2Validation();
}
/**
* 认证成功回调
*
* @param user 用户信息
*/
@Override
public abstract void authSuccessful(OAuth2User user, OAuth2Request request);
/**
* 认证失败回调
*
* @param user 用户信息
* @param validation 失败信息
*/
@Override
public abstract void authFailure(OAuth2User user, OAuth2Request request, OAuth2Validation validation);
/**
* 构建认证失败返回
*
* @param errorCode 错误码
* @return 认证结果
*/
public OAuth2Validation buildValidationFailure(ExceptionCode errorCode) {
return new OAuth2Validation().setSuccess(false)
.setCode(errorCode.getCode())
.setMessage(errorCode.getMessage());
}
}
@@ -0,0 +1,77 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.handler;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.provider.OAuth2Validation;
import org.springblade.core.oauth2.service.OAuth2User;
/**
* OAuth2AuthorizationHandler
*
* @author BladeX
*/
public interface AuthorizationHandler {
/**
* 认证前校验
*
* @param request 请求信息
* @return boolean
*/
OAuth2Validation preValidation(OAuth2Request request);
/**
* 认证前失败回调
*
* @param validation 失败信息
*/
void preFailure(OAuth2Request request, OAuth2Validation validation);
/**
* 认证校验
*
* @param user 用户信息
* @param request 请求信息
* @return boolean
*/
OAuth2Validation authValidation(OAuth2User user, OAuth2Request request);
/**
* 认证成功回调
*
* @param user 用户信息
*/
void authSuccessful(OAuth2User user, OAuth2Request request);
/**
* 认证失败回调
*
* @param user 用户信息
* @param validation 失败信息
*/
void authFailure(OAuth2User user, OAuth2Request request, OAuth2Validation validation);
}
@@ -0,0 +1,67 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.handler;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.provider.OAuth2Validation;
import org.springblade.core.oauth2.service.OAuth2User;
/**
* AbstractAuthorizationHandler
*
* @author BladeX
*/
@Slf4j
public class OAuth2AuthorizationHandler extends AbstractAuthorizationHandler {
@Override
public void preFailure(OAuth2Request request, OAuth2Validation validation) {
}
/**
* 认证成功回调
*
* @param user 用户信息
*/
@Override
public void authSuccessful(OAuth2User user, OAuth2Request request) {
}
/**
* 认证失败回调
*
* @param user 用户信息
* @param validation 失败信息
*/
@Override
public void authFailure(OAuth2User user, OAuth2Request request, OAuth2Validation validation) {
}
}
@@ -0,0 +1,72 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.handler;
import lombok.RequiredArgsConstructor;
import org.springblade.core.oauth2.props.OAuth2Properties;
import org.springblade.core.tool.utils.DigestUtil;
import org.springblade.core.tool.utils.SM2Util;
/**
* BladePasswordHandler
*
* @author BladeX
*/
@RequiredArgsConstructor
public class OAuth2PasswordHandler implements PasswordHandler {
private final OAuth2Properties properties;
/**
* 判断密码是否匹配
*
* @param rawPassword 请求时提交的原密码
* @param encodedPassword 数据库加密后的密码
* @return boolean
*/
@Override
public boolean matches(String rawPassword, String encodedPassword) {
// 获取公钥
String publicKey = properties.getPublicKey();
// 获取私钥
String privateKey = properties.getPrivateKey();
// 解密密码
String decryptPassword = SM2Util.decrypt(rawPassword, publicKey, privateKey);
// 二次加密后与数据库加密密码匹配
return encodedPassword.equals(encode(decryptPassword));
}
/**
* 加密密码规则
*
* @param rawPassword 密码
* @return 加密后的密码
*/
@Override
public String encode(String rawPassword) {
return DigestUtil.encrypt(rawPassword);
}
}
@@ -0,0 +1,80 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.handler;
import lombok.RequiredArgsConstructor;
import org.springblade.core.jwt.JwtUtil;
import org.springblade.core.jwt.props.JwtProperties;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.provider.OAuth2Token;
import org.springblade.core.oauth2.service.OAuth2User;
/**
* BladeTokenHandler
*
* @author BladeX
*/
@RequiredArgsConstructor
public class OAuth2TokenHandler implements TokenHandler {
private final JwtProperties properties;
/**
* 令牌增强
*
* @param user 用户信息
* @param token 令牌信息
* @param request 授权参数
* @return OAuth2Token
*/
@Override
public OAuth2Token enhance(OAuth2User user, OAuth2Token token, OAuth2Request request) {
//令牌状态配置, 仅在生成AccessToken时候执行
if (properties.getState() && token.hasAccessToken()) {
JwtUtil.addAccessToken(
user.getTenantId(),
user.getClient().getClientId(),
user.getUserId(),
token.getAccessToken(),
token.getAccessTokenExpire()
);
}
//令牌状态配置, 仅在生成RefreshToken时候执行
if (properties.getState() && properties.getSingle() && token.hasRefreshToken()) {
JwtUtil.addRefreshToken(
user.getTenantId(),
user.getClient().getClientId(),
user.getUserId(),
token.getRefreshToken(),
token.getRefreshTokenExpire()
);
}
// 返回令牌
return token;
}
}
@@ -0,0 +1,51 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.handler;
/**
* PasswordHandler
*
* @author BladeX
*/
public interface PasswordHandler {
/**
* 判断密码是否匹配
*
* @param rawPassword 请求时提交的原密码
* @param encodedPassword 数据库加密后的密码
* @return boolean
*/
boolean matches(String rawPassword, String encodedPassword);
/**
* 加密密码规则
*
* @param rawPassword 密码
* @return 加密后的密码
*/
String encode(String rawPassword);
}
@@ -0,0 +1,48 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.handler;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.provider.OAuth2Token;
import org.springblade.core.oauth2.service.OAuth2User;
/**
* TokenHandler
*
* @author BladeX
*/
public interface TokenHandler {
/**
* 令牌增强
*
* @param user 用户信息
* @param token 令牌信息
* @param request 授权参数
* @return OAuth2Token
*/
OAuth2Token enhance(OAuth2User user, OAuth2Token token, OAuth2Request request);
}
@@ -0,0 +1,122 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.props;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* OAuth2Property
*
* @author BladeX
*/
@Getter
@Setter
@ConfigurationProperties(OAuth2Properties.PREFIX)
public class OAuth2Properties {
/**
* 配置前缀
*/
public static final String PREFIX = "blade.oauth2";
/**
* 是否开启OAuth2
*/
private Boolean enabled = true;
/**
* code缓存时间
*/
private long codeTimeout = 10 * 60L;
/**
* sm2公钥
*/
private String publicKey;
/**
* sm2私钥
*/
private String privateKey;
/**
* 授权模式
*/
private Granter granter = new Granter();
@Data
@NoArgsConstructor
public static class Granter {
/**
* 是否开启授权码模式
*/
private Boolean authorizationCode = true;
/**
* 是否开启验证码模式
*/
private Boolean captcha = true;
/**
* 是否开启行为验证码模式
*/
private Boolean behavior = true;
/**
* 是否开启密码模式
*/
private Boolean password = true;
/**
* 是否开启刷新token模式
*/
private Boolean refreshToken = true;
/**
* 是否开启客户端模式
*/
private Boolean clientCredentials = true;
/**
* 是否开启简化模式
*/
private Boolean implicit = true;
/**
* 是否手机验证码模式
*/
private Boolean smsCode = true;
/**
* 是否开启微信小程序模式
*/
private Boolean wechatApplet = true;
/**
* 是否开启开放平台模式
*/
private Boolean social = true;
/**
* 是否开启注册模式
*/
private Boolean register = true;
}
}
@@ -0,0 +1,107 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.provider;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Data;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.core.tool.utils.WebUtil;
import java.io.Serial;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import static org.springblade.core.oauth2.constant.OAuth2ParameterConstant.*;
/**
* OAuth2AuthorizationRequest
*
* @author BladeX
*/
@Data
public class OAuth2AuthorizationRequest implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String responseType;
private String tenantId;
private String clientId;
private String redirectUri;
private String scope;
private String state;
/**
* 实例化
*/
public static OAuth2AuthorizationRequest create() {
return new OAuth2AuthorizationRequest();
}
/**
* 构建参数
*
* @return OAuth2AuthorizationRequest
*/
public OAuth2AuthorizationRequest buildParameters() {
HttpServletRequest request = Objects.requireNonNull(WebUtil.getRequest());
this.responseType = request.getParameter(RESPONSE_TYPE);
this.tenantId = request.getParameter(TENANT_ID);
this.clientId = request.getParameter(CLIENT_ID);
this.redirectUri = request.getParameter(REDIRECT_URI);
this.scope = request.getParameter(SCOPE);
this.state = request.getParameter(STATE);
return this;
}
/**
* 获取参数
*
* @return Map
*/
public Map<String, String> getParameters() {
Map<String, String> parameters = new HashMap<>();
parameters.put(RESPONSE_TYPE, this.responseType);
parameters.put(CLIENT_ID, this.clientId);
parameters.put(REDIRECT_URI, this.redirectUri);
if (scope != null) {
parameters.put(SCOPE, this.scope);
}
if (this.tenantId != null) {
parameters.put(STATE, this.tenantId);
}
if (state != null) {
parameters.put(STATE, this.state);
}
return parameters;
}
public String getState() {
return StringUtil.isBlank(this.state) ? this.tenantId : this.state;
}
}
@@ -0,0 +1,440 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.provider;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Data;
import org.springblade.core.oauth2.utils.OAuth2Util;
import org.springblade.core.tool.support.Kv;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.core.tool.utils.WebUtil;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import static org.springblade.core.oauth2.constant.OAuth2GranterConstant.PASSWORD;
import static org.springblade.core.oauth2.constant.OAuth2GranterConstant.REFRESH_TOKEN;
import static org.springblade.core.oauth2.constant.OAuth2GranterConstant.*;
import static org.springblade.core.oauth2.constant.OAuth2ParameterConstant.*;
import static org.springblade.core.oauth2.constant.OAuth2TokenConstant.*;
/**
* OAuth2参数类
*
* @author BladeX
*/
@Data
public class OAuth2Request {
/**
* 实例化
*/
public static OAuth2Request create() {
return new OAuth2Request();
}
/**
* 客户端参数
*/
private Kv clientArgs = Kv.create();
/**
* 请求参数
*/
private Kv parameterArgs = Kv.create();
/**
* 头部参数
*/
private Kv headerArgs = Kv.create();
/**
* 当前HTTP请求对象
*/
private HttpServletRequest httpRequest;
/**
* 首次认证时间(epoch 秒)
*/
private Long authTime;
/**
* 自动构建参数
*
* @return OAuth2Request
*/
public OAuth2Request buildArgs() {
return this.buildHttpRequest().buildClientArgs().buildParameterArgs().buildHeaderArgs();
}
/**
* 自动构建HTTP请求对象
*
* @return OAuth2Request
*/
public OAuth2Request buildHttpRequest() {
if (this.httpRequest == null) {
this.httpRequest = WebUtil.getRequest();
}
return this;
}
/**
* 自动构建客户端参数
*
* @return OAuth2Request
*/
public OAuth2Request buildClientArgs() {
String[] tokens = getClientFromAuthorization();
assert tokens.length == 2;
clientArgs.set(CLIENT_ID, tokens[0]);
clientArgs.set(CLIENT_SECRET, tokens[1]);
return this;
}
/**
* 自动构建请求头参数
*
* @return OAuth2Request
*/
public OAuth2Request buildParameterArgs() {
HttpServletRequest request = Objects.requireNonNull(buildHttpRequest().httpRequest);
Arrays.stream(new String[]{
CLIENT_ID, CLIENT_SECRET, ACCESS_TOKEN, REFRESH_TOKEN, TENANT_ID, USERNAME, PASSWORD, NAME, PHONE, EMAIL, GRANT_TYPE, SCOPE, REDIRECT_URI, RESPONSE_TYPE, CODE, STATE, SOURCE
}).forEach(param -> Optional.ofNullable(request.getParameter(param)).ifPresent(value -> parameterArgs.set(param, value)));
return this;
}
/**
* 自动构建头部参数
*
* @return OAuth2Request
*/
public OAuth2Request buildHeaderArgs() {
HttpServletRequest request = Objects.requireNonNull(buildHttpRequest().httpRequest);
Arrays.stream(new String[]{
HEADER_AUTHORIZATION, TOKEN_HEADER, TENANT_HEADER, USER_HEADER, ROLE_HEADER, DEPT_HEADER, USER_TYPE_HEADER, CAPTCHA_HEADER_KEY, CAPTCHA_HEADER_CODE
}).forEach(param -> Optional.ofNullable(request.getHeader(param)).ifPresent(value -> headerArgs.set(param, value)));
return this;
}
/**
* 获取请求头中的客户端id
*/
public String getClientId() {
return clientArgs.getStr(CLIENT_ID);
}
/**
* 获取请求头中的客户端密钥
*/
public String getClientSecret() {
return clientArgs.getStr(CLIENT_SECRET);
}
/**
* 获取客户端ID和密钥
*
* @return String[]
*/
public String[] getClientFromAuthorization() {
return OAuth2Util.extractAndDecodeAuthorization();
}
/**
* 获取客户端ID
*
* @return String
*/
public String getClientIdFromParameter() {
return parameterArgs.getStr(CLIENT_ID);
}
/**
* 获取客户端密钥
*
* @return String
*/
public String getClientSecretFromParameter() {
return parameterArgs.getStr(CLIENT_SECRET);
}
/**
* 获取令牌
*
* @return String
*/
public String getToken() {
return headerArgs.getStr(TOKEN_HEADER);
}
/**
* 获取租户编号
*
* @return String
*/
public String getTenantId() {
if (StringUtil.isBlank(headerArgs.getStr(TENANT_HEADER))) {
return parameterArgs.getStr(TENANT_ID);
}
return headerArgs.getStr(TENANT_HEADER);
}
/**
* 获取用户ID
*
* @return String
*/
public String getUserId() {
return headerArgs.getStr(USER_HEADER);
}
/**
* 获取用户名
*
* @return String
*/
public String getUsername() {
return parameterArgs.getStr(USERNAME);
}
/**
* 获取密码
*
* @return String
*/
public String getPassword() {
return parameterArgs.getStr(PASSWORD);
}
/**
* 获取用户名字
*
* @return String
*/
public String getName() {
return parameterArgs.getStr(NAME);
}
/**
* 获取手机号
*
* @return String
*/
public String getPhone() {
return parameterArgs.getStr(PHONE);
}
/**
* 获取电子邮箱
*
* @return String
*/
public String getEmail() {
return parameterArgs.getStr(EMAIL);
}
/**
* 获取用户类型
*
* @return String
*/
public String getUserType() {
return headerArgs.getStr(USER_TYPE_HEADER);
}
/**
* 获取用户部门
*
* @return String
*/
public String getUserDept() {
return headerArgs.getStr(DEPT_HEADER);
}
/**
* 获取用户角色
*
* @return String
*/
public String getUserRole() {
return headerArgs.getStr(ROLE_HEADER);
}
/**
* 获取验证码key
*/
public String getCaptchaKey() {
return headerArgs.getStr(CAPTCHA_HEADER_KEY);
}
/**
* 获取验证码code
*/
public String getCaptchaCode() {
return headerArgs.getStr(CAPTCHA_HEADER_CODE);
}
/**
* 获取授权类型
*
* @return String
*/
public String getGrantType() {
return parameterArgs.getStr(GRANT_TYPE);
}
/**
* 获取刷新令牌
*
* @return String
*/
public String getRefreshToken() {
return parameterArgs.getStr(REFRESH_TOKEN);
}
/**
* 获取验证code
*
* @return String
*/
public String getCode() {
return parameterArgs.getStr(CODE);
}
/**
* 获取状态
*
* @return String
*/
public String getState() {
return parameterArgs.getStr(STATE);
}
/**
* 获取来源
*
* @return String
*/
public String getSource() {
return parameterArgs.getStr(SOURCE);
}
/**
* 获取回调地址
*
* @return String
*/
public String getRedirectUri() {
return parameterArgs.getStr(REDIRECT_URI);
}
/**
* 是否密码模式
*
* @return Boolean
*/
public Boolean isPassword() {
return PASSWORD.equals(getGrantType());
}
/**
* 是否刷新模式
*
* @return Boolean
*/
public Boolean isRefreshToken() {
return REFRESH_TOKEN.equals(getGrantType());
}
/**
* 是否验证码模式
*
* @return Boolean
*/
public Boolean isCaptchaCode() {
return CAPTCHA.equals(getGrantType());
}
/**
* 是否密码模式
*
* @return Boolean
*/
public Boolean isClientCredentials() {
return CLIENT_CREDENTIALS.equals(getGrantType());
}
/**
* 是否简化模式
*
* @return Boolean
*/
public Boolean isImplicit() {
return IMPLICIT.equals(getGrantType());
}
/**
* 是否开放平台模式
*
* @return Boolean
*/
public Boolean isSocial() {
return SOCIAL.equals(getGrantType());
}
/**
* 设置租户ID
*
* @param tenantId 户ID
*/
public void setTenantId(String tenantId) {
this.headerArgs.set(TENANT_HEADER, tenantId);
}
/**
* 设置用户ID
*
* @param userId 用户ID
*/
public void setUserId(String userId) {
this.headerArgs.set(USER_HEADER, userId);
}
/**
* 设置用户名
*
* @param username 用户名
*/
public void setUsername(String username) {
this.parameterArgs.set(USERNAME, username);
}
}
@@ -0,0 +1,94 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.provider;
import lombok.Data;
import org.springblade.core.tool.support.Kv;
import java.io.Serial;
import java.io.Serializable;
import static org.springblade.core.oauth2.constant.OAuth2ResponseConstant.*;
/**
* OAuth2Response
*
* @author BladeX
*/
@Data
public class OAuth2Response implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 实例化
*/
public static OAuth2Response create() {
return new OAuth2Response();
}
/**
* 响应参数
*/
private Kv args = Kv.create();
public Kv ofFailure(int errorCode, String errorDescription) {
args.set(SUCCESS, Boolean.FALSE);
args.set(ERROR_CODE, errorCode);
args.set(ERROR_DESCRIPTION, errorDescription);
return args;
}
public Kv ofSuccessful(String successDescription) {
args.set(SUCCESS, Boolean.TRUE);
args.set(SUCCESS_CODE, DEFAULT_SUCCESS_CODE);
args.set(SUCCESS_DESCRIPTION, successDescription);
return args;
}
public Kv ofSuccessful(int successCode, String successDescription) {
args.set(SUCCESS, Boolean.TRUE);
args.set(SUCCESS_CODE, successCode);
args.set(SUCCESS_DESCRIPTION, successDescription);
return args;
}
public Kv ofSuccessful(int successCode, String successDescription, Kv successData) {
args.set(SUCCESS, Boolean.TRUE);
args.set(SUCCESS_CODE, successCode);
args.set(SUCCESS_DESCRIPTION, successDescription);
args.setAll(successData);
return args;
}
public Kv ofValidation(OAuth2Validation validation) {
args.set(SUCCESS, validation.isSuccess());
args.set(ERROR_CODE, validation.getCode());
args.set(ERROR_DESCRIPTION, validation.getMessage());
return args;
}
}
@@ -0,0 +1,99 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.provider;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springblade.core.tool.support.Kv;
import org.springblade.core.tool.utils.StringUtil;
import java.io.Serial;
import java.io.Serializable;
/**
* OAuth2Token
*
* @author BladeX
*/
@Data
@Accessors(chain = true)
public class OAuth2Token implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 实例化
*/
public static OAuth2Token create() {
return new OAuth2Token();
}
/**
* 令牌值
*/
private String accessToken;
/**
* 刷新令牌值
*/
private String refreshToken;
/**
* 令牌过期秒数
*/
private int accessTokenExpire;
/**
* 刷新令牌过期秒数
*/
private int refreshTokenExpire;
/**
* 令牌参数
*/
private Kv args = Kv.create();
/**
* 是否包含令牌
*
* @return Boolean
*/
public Boolean hasAccessToken() {
return StringUtil.isNotBlank(accessToken);
}
/**
* 是否包含刷新令牌
*
* @return Boolean
*/
public Boolean hasRefreshToken() {
return StringUtil.isNotBlank(refreshToken);
}
}
@@ -0,0 +1,62 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.provider;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* OAuth2Validation
*
* @author BladeX
*/
@Data
@Accessors(chain = true)
public class OAuth2Validation {
/**
* 实例化
*/
public static OAuth2Validation create() {
return new OAuth2Validation();
}
/**
* 是否成功
*/
boolean success = true;
/**
* 状态码
*/
int code = 1000;
/**
* 验证信息
*/
String message = "认证通过";
}
@@ -0,0 +1,113 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.service;
import java.io.Serializable;
/**
* 多终端详情接口
*
* @author BladeX
*/
public interface OAuth2Client extends Serializable {
/**
* 获取客户端ID.
*
* @return String
*/
String getClientId();
/**
* 获取客户端密钥.
*
* @return String
*/
String getClientSecret();
/**
* 获取资源集合.
*
* @return String
*/
String getResourceIds();
/**
* 获取授权范围.
*
* @return String
*/
String getScope();
/**
* 获取授权类型.
*
* @return String
*/
String getAuthorizedGrantTypes();
/**
* 获取回调地址.
*
* @return String
*/
String getWebServerRedirectUri();
/**
* 获取权限.
*
* @return String
*/
String getAuthorities();
/**
* 获取访问令牌有效期.
*
* @return Integer
*/
Integer getAccessTokenValidity();
/**
* 获取刷新令牌有效期.
*
* @return Integer
*/
Integer getRefreshTokenValidity();
/**
* 获取附加信息.
*
* @return String
*/
String getAdditionalInformation();
/**
* 获取自动授权.
*
* @return String
*/
String getAutoapprove();
}
@@ -0,0 +1,82 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.service;
import org.springblade.core.oauth2.provider.OAuth2Request;
/**
* 多终端注册接口
*
* @author BladeX
*/
public interface OAuth2ClientService {
/**
* 根据clientId获取Client详情
*
* @param clientId 客户端id
* @return 客户端信息
*/
OAuth2Client loadByClientId(String clientId);
/**
* 根据clientId获取Client详情
*
* @param clientId 客户端id
* @param request 授权参数
* @return 客户端信息
*/
OAuth2Client loadByClientId(String clientId, OAuth2Request request);
/**
* 验证Client信息
*
* @param client client信息
* @param clientId 客户端id
* @param clientSecret 客户端密钥
* @return boolean
*/
boolean validateClient(OAuth2Client client, String clientId, String clientSecret);
/**
* 验证Client信息
*
* @param client client信息
* @param redirectUri 回调地址
* @return boolean
*/
boolean validateRedirectUri(OAuth2Client client, String redirectUri);
/**
* 验证授权类型
*
* @param client client信息
* @param grantType 授权类型
* @return boolean
*/
boolean validateGranter(OAuth2Client client, String grantType);
}
@@ -0,0 +1,170 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.service;
import org.springblade.core.tool.support.Kv;
import java.io.Serializable;
import java.util.List;
/**
* 用户基础信息
*
* @author BladeX
*/
public interface OAuth2User extends Serializable {
/**
* 获取用户ID.
*
* @return Long
*/
String getUserId();
/**
* 获取租户ID.
*
* @return String
*/
String getTenantId();
/**
* 获取第三方认证ID.
*
* @return String
*/
String getOauthId();
/**
* 获取昵称.
*
* @return String
*/
String getName();
/**
* 获取真名.
*
* @return String
*/
String getRealName();
/**
* 获取账号.
*
* @return String
*/
String getAccount();
/**
* 获取密码.
*
* @return String
*/
String getPassword();
/**
* 获取手机.
*
* @return String
*/
String getPhone();
/**
* 获取邮箱.
*
* @return String
*/
String getEmail();
/**
* 获取部门ID.
*
* @return String
*/
String getDeptId();
/**
* 获取岗位ID.
*
* @return String
*/
String getPostId();
/**
* 获取角色ID.
*
* @return String
*/
String getRoleId();
/**
* 获取角色名.
*
* @return String
*/
String getRoleName();
/**
* 获取头像.
*
* @return String
*/
String getAvatar();
/**
* 获取权限集合.
*
* @return List<String>
*/
List<String> getPermissions();
/**
* 获取角色集合.
*
* @return List<String>
*/
List<String> getAuthorities();
/**
* 获取客户端信息.
*
* @return OAuth2Client
*/
OAuth2Client getClient();
/**
* 设置客户端信息.
*/
void setClient(OAuth2Client client);
/**
* 获取用户详情.
*
* @return Kv
*/
Kv getDetail();
}
@@ -0,0 +1,72 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.service;
import org.springblade.core.oauth2.provider.OAuth2Request;
/**
* OAuth2UserService
*
* @author BladeX
*/
public interface OAuth2UserService {
/**
* 根据用户名获取用户信息
*
* @param userId 用户ID
* @param request 授权参数
* @return 用户信息
*/
OAuth2User loadByUserId(String userId, OAuth2Request request);
/**
* 根据用户名获取用户信息
*
* @param username 用户名
* @param request 授权参数
* @return 用户信息
*/
OAuth2User loadByUsername(String username, OAuth2Request request);
/**
* 根据手机号获取用户信息
*
* @param phone 手机号
* @param request 授权参数
* @return 用户信息
*/
OAuth2User loadByPhone(String phone, OAuth2Request request);
/**
* 校验用户信息
*
* @param user 用户信息
* @return 是否通过
*/
boolean validateUser(OAuth2User user);
}
@@ -0,0 +1,102 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.service.impl;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springblade.core.oauth2.service.OAuth2Client;
import java.io.Serial;
/**
* 客户端详情
*
* @author BladeX
*/
@Data
@Schema(description = "oauth2客户端实体类")
public class OAuth2ClientDetail implements OAuth2Client {
@Serial
private static final long serialVersionUID = 1L;
/**
* 客户端id
*/
@Schema(description = "客户端id")
private String clientId;
/**
* 客户端密钥
*/
@Schema(description = "客户端密钥")
private String clientSecret;
/**
* 资源集合
*/
@Schema(description = "资源集合")
private String resourceIds;
/**
* 授权范围
*/
@Schema(description = "授权范围")
private String scope;
/**
* 授权类型
*/
@Schema(description = "授权类型")
private String authorizedGrantTypes;
/**
* 回调地址
*/
@Schema(description = "回调地址")
private String webServerRedirectUri;
/**
* 权限
*/
@Schema(description = "权限")
private String authorities;
/**
* 令牌过期秒数
*/
@Schema(description = "令牌过期秒数")
private Integer accessTokenValidity;
/**
* 刷新令牌过期秒数
*/
@Schema(description = "刷新令牌过期秒数")
private Integer refreshTokenValidity;
/**
* 附加说明
*/
@Schema(description = "附加说明")
private String additionalInformation;
/**
* 自动授权
*/
@Schema(description = "自动授权")
private String autoapprove;
}
@@ -0,0 +1,88 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.service.impl;
import lombok.AllArgsConstructor;
import org.springblade.core.oauth2.constant.OAuth2ClientConstant;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.service.OAuth2Client;
import org.springblade.core.oauth2.service.OAuth2ClientService;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.Arrays;
import java.util.Optional;
/**
* 获取客户端详情
*
* @author BladeX
*/
@AllArgsConstructor
public class OAuth2ClientDetailService implements OAuth2ClientService {
private final JdbcTemplate jdbcTemplate;
@Override
public OAuth2Client loadByClientId(String clientId) {
return loadByClientId(clientId, null);
}
@Override
public OAuth2Client loadByClientId(String clientId, OAuth2Request request) {
try {
return jdbcTemplate.queryForObject(OAuth2ClientConstant.DEFAULT_SELECT_STATEMENT, new BeanPropertyRowMapper<>(OAuth2ClientDetail.class), clientId);
} catch (Exception ex) {
return null;
}
}
@Override
public boolean validateClient(OAuth2Client client, String clientId, String clientSecret) {
return Optional.ofNullable(client)
.map(c -> StringUtil.equals(clientId, c.getClientId()) && StringUtil.equals(clientSecret, c.getClientSecret()))
.orElse(false);
}
@Override
public boolean validateRedirectUri(OAuth2Client client, String redirectUri) {
return Optional.ofNullable(client)
.map(c -> StringUtil.equals(redirectUri, c.getWebServerRedirectUri()))
.orElse(false);
}
@Override
public boolean validateGranter(OAuth2Client client, String grantType) {
return Optional.ofNullable(client)
.map(c -> Arrays.stream(Func.split(c.getAuthorizedGrantTypes(), StringPool.COMMA))
.anyMatch(s -> s.trim().equals(grantType)))
.orElse(false);
}
}
@@ -0,0 +1,145 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.service.impl;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springblade.core.oauth2.service.OAuth2Client;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.tool.support.Kv;
import java.io.Serial;
import java.util.List;
/**
* 用户详情
*
* @author BladeX
*/
@Data
@Schema(description = "oauth2用户实体类")
public class OAuth2UserDetail implements OAuth2User {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户id
*/
@Schema(description = "用户id")
private String userId;
/**
* 租户ID
*/
@Schema(description = "租户ID")
private String tenantId;
/**
* 第三方认证ID
*/
@Schema(description = "第三方认证ID")
private String oauthId;
/**
* 昵称
*/
@Schema(description = "昵称")
private String name;
/**
* 真名
*/
@Schema(description = "真名")
private String realName;
/**
* 账号
*/
@Schema(description = "账号")
private String account;
/**
* 密码
*/
@JsonIgnore
@Schema(description = "密码")
private String password;
/**
* 手机
*/
@Schema(description = "手机")
private String phone;
/**
* 邮箱
*/
@Schema(description = "邮箱")
private String email;
/**
* 部门id
*/
@Schema(description = "部门id")
private String deptId;
/**
* 岗位id
*/
@Schema(description = "岗位id")
private String postId;
/**
* 角色id
*/
@Schema(description = "角色id")
private String roleId;
/**
* 角色名
*/
@Schema(description = "角色名")
private String roleName;
/**
* 头像
*/
@Schema(description = "头像")
private String avatar;
/**
* 权限标识集合
*/
@Schema(description = "权限集合")
private List<String> permissions;
/**
* 角色集合
*/
@Schema(description = "角色集合")
private List<String> authorities;
/**
* 客户端
*/
@Schema(description = "客户端")
private OAuth2Client client;
/**
* 用户详情
*/
@Schema(description = "用户详情")
private Kv detail;
}
@@ -0,0 +1,82 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.service.impl;
import lombok.AllArgsConstructor;
import org.springblade.core.oauth2.constant.OAuth2UserConstant;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.service.OAuth2UserService;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.Optional;
/**
* 获取用户详情
*
* @author BladeX
*/
@AllArgsConstructor
public class OAuth2UserDetailService implements OAuth2UserService {
private final JdbcTemplate jdbcTemplate;
@Override
public OAuth2User loadByUserId(String userId, OAuth2Request request) {
try {
return jdbcTemplate.queryForObject(OAuth2UserConstant.DEFAULT_USERID_SELECT_STATEMENT, new BeanPropertyRowMapper<>(OAuth2UserDetail.class), userId);
} catch (Exception ex) {
return null;
}
}
@Override
public OAuth2User loadByUsername(String username, OAuth2Request request) {
try {
return jdbcTemplate.queryForObject(OAuth2UserConstant.DEFAULT_USERNAME_SELECT_STATEMENT, new BeanPropertyRowMapper<>(OAuth2UserDetail.class), username);
} catch (Exception ex) {
return null;
}
}
@Override
public OAuth2User loadByPhone(String phone, OAuth2Request request) {
try {
return jdbcTemplate.queryForObject(OAuth2UserConstant.DEFAULT_PHONE_SELECT_STATEMENT, new BeanPropertyRowMapper<>(OAuth2UserDetail.class), phone);
} catch (Exception ex) {
return null;
}
}
@Override
public boolean validateUser(OAuth2User user) {
return Optional.ofNullable(user)
.filter(u -> u.getUserId() != null && !u.getUserId().isEmpty()) // 检查userId不为空
.filter(u -> u.getAuthorities() != null && !u.getAuthorities().isEmpty()) // 检查authorities不为空
.isPresent(); // 如果上述条件都满足,则返回true,否则返回false
}
}
@@ -0,0 +1,49 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.utils;
/**
* OAuth2CodeUtil
*
* @author Chill
*/
public class OAuth2CodeUtil {
/**
* 授权码缓存key
*/
public static final String AUTHORIZATION_CODE_KEY = "blade:auth::code:";
/**
* code key格式
*
* @param code code
* @return key
*/
public static String codeKey(String code) {
return AUTHORIZATION_CODE_KEY + code;
}
}
@@ -0,0 +1,114 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.utils;
import org.springblade.core.oauth2.exception.*;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
/**
* OAuth2ExceptionUtil
*
* @author BladeX
*/
public class OAuth2ExceptionUtil {
private static final Map<ExceptionCode, Supplier<OAuth2Exception>> OAUTH2_EXCEPTION = new ConcurrentHashMap<>(16);
static {
// 初始化异常映射
OAUTH2_EXCEPTION.put(
ExceptionCode.INVALID_REQUEST, () -> new OAuth2Exception(ExceptionCode.INVALID_REQUEST, ExceptionCode.INVALID_REQUEST.getMessage(), new Throwable())
);
OAUTH2_EXCEPTION.put(
ExceptionCode.USER_NOT_FOUND, () -> new UsernameNotFoundException(ExceptionCode.USER_NOT_FOUND.getMessage(), new Throwable())
);
OAUTH2_EXCEPTION.put(
ExceptionCode.USER_TENANT_NOT_FOUND, () -> new UserInvalidException(ExceptionCode.USER_TENANT_NOT_FOUND.getMessage(), new Throwable())
);
OAUTH2_EXCEPTION.put(
ExceptionCode.USER_TOO_MANY_FAILS, () -> new UserInvalidException(ExceptionCode.USER_TOO_MANY_FAILS.getMessage(), new Throwable())
);
OAUTH2_EXCEPTION.put(
ExceptionCode.INVALID_USER, () -> new UserInvalidException(ExceptionCode.INVALID_USER.getMessage(), new Throwable())
);
OAUTH2_EXCEPTION.put(
ExceptionCode.UNAUTHORIZED_USER, () -> new UserUnauthorizedException(ExceptionCode.UNAUTHORIZED_USER.getMessage(), new Throwable())
);
OAUTH2_EXCEPTION.put(
ExceptionCode.UNAUTHORIZED_USER_TENANT, () -> new UserUnauthorizedException(ExceptionCode.UNAUTHORIZED_USER_TENANT.getMessage(), new Throwable())
);
OAUTH2_EXCEPTION.put(
ExceptionCode.INVALID_REFRESH_TOKEN, () -> new GranterInvalidException(ExceptionCode.INVALID_REFRESH_TOKEN.getMessage(), new Throwable())
);
OAUTH2_EXCEPTION.put(
ExceptionCode.CLIENT_NOT_FOUND, () -> new ClientNotFoundException(ExceptionCode.CLIENT_NOT_FOUND.getMessage(), new Throwable())
);
OAUTH2_EXCEPTION.put(
ExceptionCode.INVALID_CLIENT, () -> new ClientInvalidException(ExceptionCode.INVALID_CLIENT.getMessage(), new Throwable())
);
OAUTH2_EXCEPTION.put(
ExceptionCode.INVALID_CLIENT_REDIRECT_URI, () -> new ClientInvalidException(ExceptionCode.INVALID_CLIENT_REDIRECT_URI.getMessage(), new Throwable())
);
OAUTH2_EXCEPTION.put(
ExceptionCode.UNAUTHORIZED_CLIENT, () -> new ClientUnauthorizedException(ExceptionCode.UNAUTHORIZED_CLIENT.getMessage(), new Throwable())
);
OAUTH2_EXCEPTION.put(
ExceptionCode.UNSUPPORTED_GRANT_TYPE, () -> new OAuth2Exception(ExceptionCode.UNSUPPORTED_GRANT_TYPE, ExceptionCode.UNSUPPORTED_GRANT_TYPE.getMessage(), new Throwable())
);
OAUTH2_EXCEPTION.put(
ExceptionCode.INVALID_GRANTER, () -> new GranterInvalidException(ExceptionCode.INVALID_GRANTER.getMessage(), new Throwable())
);
OAUTH2_EXCEPTION.put(
ExceptionCode.INVALID_SCOPE, () -> new OAuth2Exception(ExceptionCode.INVALID_SCOPE, ExceptionCode.INVALID_SCOPE.getMessage(), new Throwable())
);
OAUTH2_EXCEPTION.put(
ExceptionCode.SERVER_ERROR, () -> new OAuth2Exception(ExceptionCode.SERVER_ERROR, ExceptionCode.SERVER_ERROR.getMessage(), new Throwable())
);
OAUTH2_EXCEPTION.put(
ExceptionCode.ACCESS_DENIED, () -> new OAuth2Exception(ExceptionCode.ACCESS_DENIED, ExceptionCode.ACCESS_DENIED.getMessage(), new Throwable())
);
OAUTH2_EXCEPTION.put(
ExceptionCode.TEMPORARILY_UNAVAILABLE, () -> new OAuth2Exception(ExceptionCode.TEMPORARILY_UNAVAILABLE, ExceptionCode.TEMPORARILY_UNAVAILABLE.getMessage(), new Throwable())
);
}
/**
* 根据错误代码抛出异常
*
* @param code 错误代码
*/
public static void throwFromCode(int code) {
Supplier<OAuth2Exception> exceptionSupplier = OAUTH2_EXCEPTION.get(ExceptionCode.of(code));
if (exceptionSupplier != null) {
throw exceptionSupplier.get();
} else {
throw new UserInvalidException(ExceptionCode.INVALID_USER.getMessage(), new Throwable());
}
}
}
@@ -0,0 +1,49 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.utils;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.oauth2.exception.OAuth2Exception;
import org.springblade.core.oauth2.provider.OAuth2Request;
/**
* OAuth2LogUtil
*
* @author Chill
*/
@Slf4j
public class OAuth2LogUtil {
public static void logOAuth2Exception(OAuth2Exception ex, OAuth2Request request, boolean isProd) {
if (!isProd) {
log.warn("----------OAuth2认证异常,具体信息如下----------");
log.warn("OAuth2Request: {}", request);
log.warn("OAuth2Exception: ", ex);
log.warn("--------------------------------------------");
}
}
}
@@ -0,0 +1,124 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.oauth2.utils;
import org.springblade.core.oauth2.service.OAuth2Client;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.secure.TokenInfo;
import org.springblade.core.secure.utils.SecureUtil;
import org.springblade.core.tool.support.Kv;
import java.time.Instant;
import static org.springblade.core.launch.constant.TokenConstant.*;
/**
* OAuth2Util
*
* @author BladeX
*/
public class OAuth2Util extends SecureUtil {
/**
* 创建accessToken
*
* @param user 用户信息
* @return accessToken
*/
public static TokenInfo createAccessToken(OAuth2User user) {
Kv kv = Kv.create().set(TOKEN_TYPE, ACCESS_TOKEN)
.set(CLIENT_ID, user.getClient().getClientId())
.set(TENANT_ID, user.getTenantId())
.set(USER_ID, user.getUserId())
.set(DEPT_ID, user.getDeptId())
.set(POST_ID, user.getPostId())
.set(ROLE_ID, user.getRoleId())
.set(OAUTH_ID, user.getOauthId())
.set(ACCOUNT, user.getAccount())
.set(USER_NAME, user.getAccount())
.set(NICK_NAME, user.getName())
.set(REAL_NAME, user.getRealName())
.set(ROLE_NAME, user.getRoleName())
.set(DETAIL, user.getDetail());
return createToken(kv, user.getClient().getAccessTokenValidity());
}
/**
* 创建refreshToken
*
* @param user 用户信息
* @return refreshToken
*/
public static TokenInfo createRefreshToken(OAuth2User user) {
return createRefreshToken(user, Instant.now().getEpochSecond());
}
/**
* 创建refreshToken
*
* @param user 用户信息
* @param authTime 首次认证时间(epoch 秒),刷新续期时沿用旧令牌的值,作为绝对有效期基线
* @return refreshToken
*/
public static TokenInfo createRefreshToken(OAuth2User user, long authTime) {
Kv kv = Kv.create().set(TOKEN_TYPE, REFRESH_TOKEN)
.set(USER_ID, user.getUserId())
.set(DEPT_ID, user.getDeptId())
.set(ROLE_ID, user.getRoleId())
.set(AUTH_TIME, authTime);
return createToken(kv, user.getClient().getRefreshTokenValidity());
}
/**
* 创建clientAccessToken
*
* @param user 用户信息
* @return clientToken
*/
public static TokenInfo createClientAccessToken(OAuth2User user) {
OAuth2Client client = user.getClient();
Kv kv = Kv.create().set(TOKEN_TYPE, CLIENT_ACCESS_TOKEN)
.set(TENANT_ID, user.getTenantId())
.set(CLIENT_ID, client.getClientId());
return createToken(kv, client.getAccessTokenValidity());
}
/**
* 创建implicitAccessToken
*
* @param user 用户信息
* @return implicitAccessToken
*/
public static TokenInfo createImplicitAccessToken(OAuth2User user) {
Kv kv = Kv.create().set(TOKEN_TYPE, IMPLICIT_ACCESS_TOKEN)
.set(TENANT_ID, user.getTenantId())
.set(ACCOUNT, user.getAccount());
return createToken(kv);
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,571 @@
/*------------------------------------------------------------------
* Theme Name: iofrm - form templates
* Theme URI: http://www.brandio.io/envato/iofrm
* Author: Brandio
* Author URI: http://www.brandio.io/
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
-------------------------------------------------------------------*/
body {
background-color: #152733;
}
.form-body {
background-color: #152733;
}
.website-logo {
display: none;
top: 50px;
left: 50px;
right: initial;
bottom: initial;
}
.website-logo img {
width: 100px;
}
.website-logo .logo {
background-image: url("../images/logo-light.svg");
}
.website-logo .logo img {
width: 100px;
}
.website-logo-inside img {
width: 100px;
}
.website-logo-inside .logo {
background-image: url("../images/logo-light.svg");
}
.website-logo-inside .logo img {
width: 100px;
}
.img-holder {
width: 0;
background-color: #5CBAFF;
}
.img-holder .info-holder h3 {
color: #fff;
text-align: left;
}
.img-holder .info-holder h3 span {
color: #fff;
}
.img-holder .info-holder h2 {
color: #fff;
text-align: left;
}
.img-holder .info-holder h2 span {
color: #fff;
}
.img-holder .info-holder p {
color: #fff;
text-align: left;
}
.img-holder .bg {
opacity: 0.23;
background-image: none;
}
.form-holder {
margin-left: 0;
}
.form-holder .form-content ::-webkit-input-placeholder {
color: #8D8D8D !important;
}
.form-holder .form-content :-moz-placeholder {
color: #8D8D8D !important;
}
.form-holder .form-content ::-moz-placeholder {
color: #8D8D8D !important;
}
.form-holder .form-content :-ms-input-placeholder {
color: #8D8D8D !important;
}
.form-content {
background-color: #152733;
}
.form-content .form-group {
color: #fff;
}
.form-content .form-items {
max-width: 380px;
text-align: center;
}
.form-content .form-icon {
margin-top: calc(-42px - 35px);
}
.form-content .form-icon .icon-holder {
background-color: #4A77F7;
}
.form-content h1 {
color: #fff;
text-align: center;
}
.form-content h2 {
color: #fff;
text-align: center;
}
.form-content h3 {
color: #fff;
text-align: center;
}
.form-content p {
color: #fff;
text-align: center;
}
.form-content label {
color: #fff;
text-align: center;
}
.form-content .page-links a {
color: #fff;
}
.form-content .page-links a:after {
background-color: rgba(255, 255, 255, 0.5);
}
.form-content .page-links a.active:after {
background-color: #fff;
}
.form-content .page-links a:hover:after, .form-content .page-links a:focus:after {
background-color: #fff;
}
.form-content input, .form-content .dropdown-toggle.btn-default {
border: 0;
background-color: #fff;
color: #8D8D8D;
}
.form-content input:hover, .form-content input:focus, .form-content .dropdown-toggle.btn-default:hover, .form-content .dropdown-toggle.btn-default:focus {
border: 0;
background-color: #ebeff8;
color: #8D8D8D;
}
.form-content textarea {
border: 0;
background-color: #fff;
color: #8D8D8D;
}
.form-content textarea:hover, .form-content textarea:focus {
border: 0;
background-color: #ebeff8;
color: #8D8D8D;
}
.form-content .custom-file-label {
border: 0;
background-color: #fff;
color: #8D8D8D;
}
.form-content .custom-file-label:after {
color: #0093FF;
}
.form-content .custom-file:hover .custom-file-label, .form-content .custom-file:focus .custom-file-label {
border: 0;
background-color: #ebeff8;
color: #8D8D8D;
}
.form-content input[type=checkbox]:not(:checked) + label, .form-content input[type=checkbox]:checked + label, .form-content input[type=radio]:not(:checked) + label, .form-content input[type=radio]:checked + label {
color: #fff;
font-weight: 700;
}
.form-content input[type=checkbox]:checked + label, .form-content input[type=radio]:checked + label {
color: #fff;
}
.form-content input[type=checkbox]:checked + label:before, .form-content input[type=radio]:checked + label:before {
background: #fff;
border: 0px solid #fff;
}
.form-content input[type=checkbox]:not(:checked) + label:before, .form-content input[type=radio]:not(:checked) + label:before {
background: transparent;
border: 2px solid #fff;
}
.form-content input[type=checkbox]:not(:checked) + label:after, .form-content input[type=checkbox]:checked + label:after {
color: #152733;
}
.form-content input[type=radio]:not(:checked) + label:after, .form-content input[type=radio]:checked + label:after {
background-color: #152733;
}
.form-content .custom-options input[type=checkbox]:not(:checked) + label, .form-content .custom-options input[type=checkbox]:checked + label, .form-content .custom-options input[type=radio]:not(:checked) + label, .form-content .custom-options input[type=radio]:checked + label {
color: #606060;
background-color: #F7F7F7;
}
.form-content .custom-options input[type=checkbox]:checked + label, .form-content .custom-options input[type=radio]:checked + label {
color: #fff;
background-color: #1592E6;
-webkit-box-shadow: 0 3px 8px rgba(0, 0, 0, 0.16);
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.16);
}
.form-content .form-button .lbtn {
margin-top: 10px;
background-color: #cb3444;
color: #fff;
-webkit-box-shadow: 0 0 0 rgba(0, 0, 0, 0.16);
box-shadow: 0 0 0 rgba(0, 0, 0, 0.16);
}
.form-content .form-button .lbtn:hover, .form-content .form-button .lbtn:focus {
background-color: #cb3444;
color: #fff;
-webkit-box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16);
box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16);
}
.form-content .form-button .ibtn {
background-color: #1592E6;
color: #fff;
-webkit-box-shadow: 0 0 0 rgba(0, 0, 0, 0.16);
box-shadow: 0 0 0 rgba(0, 0, 0, 0.16);
}
.form-content .form-button .ibtn:hover, .form-content .form-button .ibtn:focus {
background-color: #1592E6;
color: #fff;
-webkit-box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16);
box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16);
}
.form-content .form-button a {
color: #fff;
}
.form-content .other-links span {
color: #fff;
}
.form-content .other-links a {
color: #fff;
}
.form-content .form-sent .tick-holder .tick-icon {
background-color: rgba(14, 30, 41, 0);
}
.form-content .form-sent .tick-holder .tick-icon:before {
background-color: #8CCB57;
}
.form-content .form-sent .tick-holder .tick-icon:after {
background-color: #8CCB57;
}
.form-content .form-sent h3 {
color: #fff;
}
.form-content .form-sent p {
color: #fff;
}
.form-content .form-sent .info-holder {
color: #fff;
border-top: 1px solid rgba(255, 255, 255, 0.5);
}
.form-content .form-sent .info-holder span {
color: #fff;
}
.form-content .form-sent .info-holder a {
color: #fff;
}
@keyframes tick-anime3 {
0% {
background-color: rgba(14, 30, 41, 0);
-webkit-transform: rotate(35deg) scale(2);
-moz-transform: rotate(35deg) scale(2);
-ms-transform: rotate(35deg) scale(2);
transform: rotate(35deg) scale(2);
}
100% {
background-color: #0E1E29;
-webkit-transform: rotate(45deg) scale(1);
-moz-transform: rotate(45deg) scale(1);
-ms-transform: rotate(45deg) scale(1);
transform: rotate(45deg) scale(1);
}
}
.alert {
color: #fff;
}
.alert.alert-primary {
background-color: rgba(226, 240, 255, 0);
border-color: #3a86d6;
}
.alert.alert-primary hr {
border-top-color: #3a86d6;
}
.alert.alert-secondary {
background-color: rgba(240, 240, 240, 0);
border-color: #8e9396;
}
.alert.alert-secondary hr {
border-top-color: #8e9396;
}
.alert.alert-success {
background-color: rgba(247, 255, 240, 0);
border-color: #8CCB57;
}
.alert.alert-success hr {
border-top-color: #8CCB57;
}
.alert.alert-danger {
background-color: rgba(255, 250, 250, 0);
border-color: #F55050;
}
.alert.alert-danger hr {
border-top-color: #F55050;
}
.alert.alert-warning {
background-color: rgba(255, 248, 225, 0);
border-color: #f1cb4b;
}
.alert.alert-warning hr {
border-top-color: #f1cb4b;
}
.alert.alert-info {
background-color: rgba(220, 237, 241, 0);
border-color: #42bfdb;
}
.alert.alert-info hr {
border-top-color: #42bfdb;
}
.alert.alert-light {
background-color: rgba(254, 254, 254, 0);
border-color: #a7a4a4;
}
.alert.alert-light hr {
border-top-color: #a7a4a4;
}
.alert.alert-dark {
background-color: rgba(214, 216, 217, 0);
border-color: #525557;
}
.alert.alert-dark hr {
border-top-color: #525557;
}
.alert.with-icon.alert-primary:before {
color: #3a86d6;
}
.alert.with-icon.alert-secondary:before {
color: #8e9396;
}
.alert.with-icon.alert-success:before {
color: #8CCB57;
}
.alert.with-icon.alert-danger:before {
color: #F55050;
}
.alert.with-icon.alert-warning:before {
color: #f1cb4b;
}
.alert.with-icon.alert-info:before {
color: #42bfdb;
}
.alert.with-icon.alert-light:before {
color: #a7a4a4;
}
.alert.with-icon.alert-dark:before {
color: #525557;
}
.alert a, .alert a.alert-link {
color: #fff;
}
.alert .close {
color: #727272;
}
.alert .close span {
color: #727272;
}
.form-subtitle {
color: #fff;
}
.rad-with-details .more-info {
color: #fff;
}
.form-body.without-side h3 {
color: #000;
}
.form-body.without-side p {
color: #000;
}
.form-body.without-side label {
color: #000;
}
.form-body.without-side .img-holder .info-holder img {
display: inline-block;
}
.form-body.without-side .form-content .form-items {
padding: 35px 30px;
background-color: #fff;
}
.form-body.without-side .form-content .form-items .other-links .text {
color: #000;
}
.form-body.without-side .form-content .form-items .other-links a {
color: #000;
background-color: #F7F7F7;
}
.form-body.without-side .form-content .page-links a {
color: #000;
}
.form-body.without-side .form-content .page-links a:after {
background-color: rgba(255, 255, 255, 0.5);
}
.form-body.without-side .form-content .page-links a.active:after {
background-color: #fff;
}
.form-body.without-side .form-content .page-links a:hover:after, .form-body.without-side .form-content .page-links a:focus:after {
background-color: #fff;
}
.form-body.without-side .form-content input, .form-body.without-side .form-content .dropdown-toggle.btn-default {
border: 0;
background-color: #fff;
color: #8D8D8D;
}
.form-body.without-side .form-content input:hover, .form-body.without-side .form-content input:focus, .form-body.without-side .form-content .dropdown-toggle.btn-default:hover, .form-body.without-side .form-content .dropdown-toggle.btn-default:focus {
border: 0;
background-color: #fff;
color: #8D8D8D;
}
.form-body.without-side .form-content .form-button .lbtn {
background-color: #cb3444;
color: #fff;
-webkit-box-shadow: 0 0 0 rgba(0, 0, 0, 0.16);
box-shadow: 0 0 0 rgba(0, 0, 0, 0.16);
}
.form-body.without-side .form-content .form-button .lbtn:hover, .form-body.without-side .form-content .form-button .lbtn:focus {
-webkit-box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16);
box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16);
}
.form-body.without-side .form-content .form-button .ibtn {
background-color: #1592E6;
color: #fff;
-webkit-box-shadow: 0 0 0 rgba(0, 0, 0, 0.16);
box-shadow: 0 0 0 rgba(0, 0, 0, 0.16);
}
.form-body.without-side .form-content .form-button .ibtn:hover, .form-body.without-side .form-content .form-button .ibtn:focus {
-webkit-box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16);
box-shadow: 0 5px 6px rgba(0, 0, 0, 0.16);
}
.form-body.without-side .form-content .form-button a {
color: #fff;
}
/* -----------------------------------
2 - Responsive Styles
------------------------------------*/
@media (max-width: 992px) {
.form-holder {
margin-left: 0;
}
.website-logo {
top: 50px;
left: 50px;
right: initial;
bottom: initial;
}
.website-logo .logo {
background-image: url("../images/logo-light.svg");
}
.form-body.without-side .website-logo .logo {
background-image: url("../images/logo-light.svg");
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BladeX 统一认证系统</title>
<link rel="stylesheet" type="text/css" href="/static/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/static/css/iofrm-style.css">
<link rel="stylesheet" type="text/css" href="/static/css/iofrm-theme.css">
</head>
<body>
<div class="form-body">
<div class="row">
<div class="form-holder">
<div class="form-content">
<div class="form-items">
<div class="website-logo-inside">
<img class="logo-size" src="/static/images/bladex-logo.png">
<h1>BladeX 统一认证系统</h1>
</div>
<p th:text="'应用 [' + ${client_id} + '] 请求授权'">应用 [未获取到应用信息] 请求授权</p>
<p>授权后该应用将取得系统操作权限</p>
<div class="page-links">
<a th:text="'授权账号 [' + ${username} + '] 请求授权'">授权账号 [未获取到账号信息]</a>
</div>
<form action="/oauth/authorize/perform" method="post">
<input id='approval' name='approval' th:value="${auto_approve}" type='hidden'/>
<input id='state' name='state' th:value="${state}" type='hidden'/>
<div class="form-button">
<button id="submit" type="submit" class="ibtn">同意/授权</button>
<button id="logout" class="lbtn">退出登录</button>
</div>
</form>
</div>
<div class="other-links">
<span>Copyrights © <span id="copyright-notice"></span> <a href="https://bladex.cn" target="_blank">BladeX</a> All Rights Reserved.</span>
</div>
</div>
</div>
</div>
</div>
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/popper.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
<script>
$(window).on("load", function () {
// 加载版权信息
const currentYear = new Date().getFullYear();
$("#copyright-notice").html(currentYear);
// 初始化页面增加租户id传递
const state = getUrlParam("state");
if (state !== null && state !== "") {
$("#state").val(state);
}
// 获取approve的值,如果为true则自动提交表单
const approve = $("#approval").val();
if (approve === "true") {
$("#submit").click();
}
});
$("#logout").click(function(event){
event.preventDefault();
window.location.href = "/oauth/authorize/logout";
});
// 获取url中参数的值
function getUrlParam(name) {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(name);
}
</script>
</body>
</html>
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BladeX 统一认证系统</title>
<link rel="stylesheet" type="text/css" href="/static/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/static/css/iofrm-style.css">
<link rel="stylesheet" type="text/css" href="/static/css/iofrm-theme.css">
</head>
<body>
<div class="form-body">
<div class="row">
<div class="form-holder">
<div class="form-content">
<div class="form-items">
<div class="website-logo-inside">
<img class="logo-size" src="/static/images/bladex-logo.png">
<h1>BladeX 统一认证系统</h1>
</div>
<p>应用授权失败</p>
<div class="page-links">
<a>请返回 [认证页面] 重新认证</a>
</div>
<div class="form-button">
<button id="logout" class="lbtn">前往认证</button>
</div>
</div>
<div class="other-links">
<span>Copyrights © <span id="copyright-notice"></span> <a href="https://bladex.cn" target="_blank">BladeX</a> All Rights Reserved.</span>
</div>
</div>
</div>
</div>
</div>
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/popper.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
<script>
$(window).on("load", function () {
// 加载版权信息
const currentYear = new Date().getFullYear();
$("#copyright-notice").html(currentYear);
});
$("#logout").click(function(){
window.location.href = "/oauth/authorize/logout";
});
</script>
</body>
</html>
@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BladeX 统一认证系统</title>
<link rel="stylesheet" type="text/css" href="/static/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/static/css/iofrm-style.css">
<link rel="stylesheet" type="text/css" href="/static/css/iofrm-theme.css">
</head>
<body>
<div class="form-body">
<div class="row">
<div class="form-holder">
<div class="form-content">
<div class="form-items">
<div class="website-logo-inside">
<img class="logo-size" src="/static/images/bladex-logo.png">
<h1>BladeX 统一认证系统</h1>
</div>
<p>欢迎使用统一认证,提交后请对应用进行授权</p>
<div class="page-links">
<a>请输入认证信息</a>
</div>
<div>
<input id='pk' name='pk' th:value="${publicKey}" type='hidden'/>
</div>
<form id="form" action="/oauth/login/perform" method="post">
<input class="form-control" type="text" name="tenant_id" placeholder="请输入租户ID" required>
<input class="form-control" type="text" name="username" placeholder="请输入用户名" required>
<input class="form-control" id="ipt" type="password" name="password" placeholder="请输入密码" required>
<div class="form-button">
<button id="btn" type="button" class="ibtn">登 录</button>
</div>
</form>
</div>
<div class="other-links">
<span>Copyrights © <span id="copyright-notice"></span> <a href="https://bladex.cn" target="_blank">BladeX</a> All Rights Reserved.</span>
</div>
</div>
</div>
</div>
</div>
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/popper.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
<script src="/static/js/sm2.min.js"></script>
<script>
$(window).on("load", function () {
// 加载版权信息
const currentYear = new Date().getFullYear();
$("#copyright-notice").html(currentYear);
});
$("#btn").click(function(){
const val = $("#ipt").val();
$("#ipt").val(sm2.doEncrypt(val, $("#pk").val(), 0));
$("#btn").attr("disabled", "disabled");
$("#btn").text("登 录 中");
$("#form").submit();
});
</script>
</body>
</html>
+50
View File
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>
<parent>
<artifactId>blade-core-auth</artifactId>
<groupId>org.springblade</groupId>
<version>${revision}</version>
</parent>
<artifactId>blade-core-secure</artifactId>
<name>${project.artifactId}</name>
<version>${project.parent.version}</version>
<packaging>jar</packaging>
<properties>
<module.name>org.springblade.blade.core.secure</module.name>
</properties>
<dependencies>
<!--Blade-->
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-auth</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-cache</artifactId>
</dependency>
<!--Jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<exclusions>
<exclusion>
<artifactId>tomcat-jdbc</artifactId>
<groupId>org.apache.tomcat</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- Auto -->
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-core-auto</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,53 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.annotation;
import org.springblade.core.tool.constant.RoleConstant;
import java.lang.annotation.*;
/**
* Admin角色权限注解默认校验是否具有 Admin 角色
* <p>
* 使用示例
* <pre>{@code
* // 仅管理员可访问
* @IsAdmin
* public R<Boolean> adminOperation() {
* return R.data(true);
* }
* }</pre>
*
* @author BladeX
* @see PreAuth
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@PreAuth(RoleConstant.HAS_ROLE_ADMIN)
public @interface IsAdmin {
}
@@ -0,0 +1,53 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.annotation;
import org.springblade.core.tool.constant.RoleConstant;
import java.lang.annotation.*;
/**
* Administrator角色权限注解默认校验是否具有 Administrator 角色
* <p>
* 使用示例
* <pre>{@code
* // 仅超管可访问
* @IsAdministrator
* public R<Boolean> administratorOperation() {
* return R.data(true);
* }
* }</pre>
*
* @author BladeX
* @see PreAuth
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@PreAuth(RoleConstant.HAS_ROLE_ADMINISTRATOR)
public @interface IsAdministrator {
}
@@ -0,0 +1,87 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.annotation;
import org.springblade.core.tool.utils.StringPool;
import java.lang.annotation.*;
/**
* 权限注解用于检查权限规定访问权限
* 支持以下几种使用方式
* <p>
* 1. 单个属性模式
* <pre>{@code
* @PreAuth(permission = "user:add")
* @PreAuth(role = "admin")
* @PreAuth(menu = "user")
* }</pre>
*
* 2. 组合属性模式
* <pre>{@code
* @PreAuth(role = "admin", permission = "user:add")
* @PreAuth(menu = "user", permission = "user:list")
* }</pre>
*
* 3. SpEL表达式模式
* <pre>{@code
* @PreAuth("#userVO.id<10")
* @PreAuth("hasMenu('user')")
* @PreAuth("hasRole('admin')")
* @PreAuth("hasPermission('user:add')")
* @PreAuth("hasPermission(#test) and hasRole('admin')")
* }</pre>
*
* @author BladeX
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuth {
/**
* Spring el表达式
*/
String value() default StringPool.EMPTY;
/**
* 接口权限
*/
String permission() default StringPool.EMPTY;
/**
* 角色权限
*/
String role() default StringPool.EMPTY;
/**
* 菜单权限
*/
String menu() default StringPool.EMPTY;
}
@@ -0,0 +1,104 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.aspect;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import jakarta.annotation.security.PermitAll;
import org.springblade.core.secure.exception.SecureException;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.ResultCode;
import org.springblade.core.tool.utils.ClassUtil;
import org.springblade.core.tool.utils.WebUtil;
import java.util.ArrayList;
import java.util.List;
import static org.springblade.core.launch.constant.TokenConstant.AUTH_HEADER;
/**
* AOP 鉴权
*
* @author Chill
*/
@Slf4j
@Aspect
public class AdminAspect {
/**
* 方法 类上的 @IsAdmin 注解
*
* @param point 切点
* @return Object
* @throws Throwable 没有权限的异常
*/
@Around(
"@annotation(org.springblade.core.secure.annotation.IsAdmin) || " +
"@within(org.springblade.core.secure.annotation.IsAdmin)"
)
public Object preAuth(ProceedingJoinPoint point) throws Throwable {
// 检查 @PermitAll 注解有则跳过角色校验
if (ClassUtil.getAnnotation(point, PermitAll.class) != null) {
return point.proceed();
}
if (AuthUtil.isAdmin() || AuthUtil.isAdministrator()) {
return point.proceed();
}
this.handleLogger();
throw new SecureException(ResultCode.UN_AUTHORIZED);
}
/**
* 记录日志
*/
private void handleLogger() {
HttpServletRequest request = WebUtil.getRequest();
if (request != null && AuthUtil.hasAuth()) {
// 记录日志
List<Object> authArgs = new ArrayList<>();
String authLogger = "\n\n================ IsAdmin Start ================" +
"\nPreAuth : IsAdmin" +
"\nClientId : {}" +
"\nBladeAuth : {}" +
"\nBladeUser : {}" +
"\nRequestURI : {}" +
"\nRequestIP: {}" +
"\nRequestParam: {}" +
"\n================ IsAdmin End ================\n";
authArgs.add(AuthUtil.getClientId(request));
authArgs.add(WebUtil.getHeader(AUTH_HEADER));
authArgs.add(AuthUtil.getUser());
authArgs.add(WebUtil.getRequestURI(request));
authArgs.add(WebUtil.getIP(request));
authArgs.add(WebUtil.getRequestContent(request));
log.warn(authLogger, authArgs.toArray());
}
}
}
@@ -0,0 +1,105 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.aspect;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import jakarta.annotation.security.PermitAll;
import org.springblade.core.secure.exception.SecureException;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.ResultCode;
import org.springblade.core.tool.utils.ClassUtil;
import org.springblade.core.tool.utils.WebUtil;
import java.util.ArrayList;
import java.util.List;
import static org.springblade.core.launch.constant.TokenConstant.AUTH_HEADER;
/**
* AOP 鉴权
*
* @author Chill
*/
@Slf4j
@Aspect
public class AdministratorAspect {
/**
* 方法 类上的 @IsAdministrator 注解
*
* @param point 切点
* @return Object
* @throws Throwable 没有权限的异常
*/
@Around(
"@annotation(org.springblade.core.secure.annotation.IsAdministrator) || " +
"@within(org.springblade.core.secure.annotation.IsAdministrator)"
)
public Object preAuth(ProceedingJoinPoint point) throws Throwable {
// 检查 @PermitAll 注解有则跳过角色校验
if (ClassUtil.getAnnotation(point, PermitAll.class) != null) {
return point.proceed();
}
if (AuthUtil.isAdministrator()) {
return point.proceed();
}
this.handleLogger();
throw new SecureException(ResultCode.UN_AUTHORIZED);
}
/**
* 记录日志
*/
private void handleLogger() {
HttpServletRequest request = WebUtil.getRequest();
if (request != null && AuthUtil.hasAuth()) {
// 记录日志
List<Object> authArgs = new ArrayList<>();
String authLogger = "\n\n============ IsAdministrator Start ============" +
"\nPreAuth : IsAdministrator" +
"\nClientId : {}" +
"\nBladeAuth : {}" +
"\nBladeUser : {}" +
"\nRequestURI : {}" +
"\nRequestIP: {}" +
"\nRequestParam: {}" +
"\n============ IsAdministrator End ============\n";
authArgs.add(AuthUtil.getClientId(request));
authArgs.add(WebUtil.getHeader(AUTH_HEADER));
authArgs.add(AuthUtil.getUser());
authArgs.add(WebUtil.getRequestURI(request));
authArgs.add(WebUtil.getIP(request));
authArgs.add(WebUtil.getRequestContent(request));
log.warn(authLogger, authArgs.toArray());
}
}
}
@@ -0,0 +1,218 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.aspect;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import jakarta.annotation.security.PermitAll;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.auth.AuthFun;
import org.springblade.core.secure.exception.SecureException;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.ResultCode;
import org.springblade.core.tool.utils.ClassUtil;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.core.tool.utils.WebUtil;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.core.MethodParameter;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.NonNull;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import static org.springblade.core.launch.constant.TokenConstant.AUTH_HEADER;
/**
* AOP 鉴权
*
* @author Chill
*/
@Slf4j
@Aspect
@RequiredArgsConstructor
public class AuthAspect implements ApplicationContextAware {
/**
* 权限处理函数
*/
private final AuthFun authFun;
/**
* 表达式处理
*/
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
/**
* 方法 类上的 @PreAuth 注解
*
* @param point 切点
* @return Object
* @throws Throwable 没有权限的异常
*/
@Around(
"@annotation(org.springblade.core.secure.annotation.PreAuth) || " +
"@within(org.springblade.core.secure.annotation.PreAuth)"
)
public Object preAuth(ProceedingJoinPoint point) throws Throwable {
// 检查 @PermitAll 注解有则跳过权限校验
if (ClassUtil.getAnnotation(point, PermitAll.class) != null) {
return point.proceed();
}
if (handleAuth(point)) {
return point.proceed();
}
this.handleLogger(point);
throw new SecureException(ResultCode.UN_AUTHORIZED);
}
/**
* 处理权限
*
* @param point 切点
*/
private boolean handleAuth(ProceedingJoinPoint point) {
// 读取权限注解优先方法上没有则读取类
MethodSignature ms = (MethodSignature) point.getSignature();
Method method = ms.getMethod();
PreAuth preAuth = ClassUtil.getAnnotation(method, PreAuth.class);
// 处理接口权限属性验证
if (StringUtil.isNotBlank(preAuth.permission()) && !authFun.hasPermission(preAuth.permission())) {
return false;
}
// 处理角色权限属性验证
if (StringUtil.isNotBlank(preAuth.role()) && !authFun.hasRole(preAuth.role())) {
return false;
}
// 处理菜单权限属性验证
if (StringUtil.isNotBlank(preAuth.menu()) && !authFun.hasMenu(preAuth.menu())) {
return false;
}
// 判断表达式
String condition = preAuth.value();
if (StringUtil.isNotBlank(condition)) {
Expression expression = EXPRESSION_PARSER.parseExpression(condition);
// 方法参数值
Object[] args = point.getArgs();
StandardEvaluationContext context = getEvaluationContext(method, args);
return Boolean.TRUE.equals(expression.getValue(context, Boolean.class));
} else {
// 判断用户是否通过鉴权
return AuthUtil.hasAuth();
}
}
/**
* 获取方法上的参数
*
* @param method 方法
* @param args 变量
* @return {SimpleEvaluationContext}
*/
private StandardEvaluationContext getEvaluationContext(Method method, Object[] args) {
// 初始化Sp el表达式上下文并设置 AuthFun
StandardEvaluationContext context = new StandardEvaluationContext(authFun);
// 设置表达式支持spring bean
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
for (int i = 0; i < args.length; i++) {
// 读取方法参数
MethodParameter methodParam = ClassUtil.getMethodParameter(method, i);
// 设置方法 参数名和值 为sp el变量
context.setVariable(methodParam.getParameterName(), args[i]);
}
return context;
}
/**
* 记录日志
*/
private void handleLogger(ProceedingJoinPoint point) {
HttpServletRequest request = WebUtil.getRequest();
if (request != null && AuthUtil.hasAuth()) {
// 读取权限注解优先方法上没有则读取类
MethodSignature ms = (MethodSignature) point.getSignature();
Method method = ms.getMethod();
PreAuth preAuth = ClassUtil.getAnnotation(method, PreAuth.class);
String menu = preAuth.menu();
String role = preAuth.role();
String permission = preAuth.permission();
String value = preAuth.value();
// 获取preAuth注解有值的定义
StringBuilder authMessage = new StringBuilder();
if (StringUtil.isNotBlank(menu)) {
authMessage.append("menu[").append(menu).append("] ");
}
if (StringUtil.isNotBlank(role)) {
authMessage.append("role[").append(role).append("] ");
}
if (StringUtil.isNotBlank(permission)) {
authMessage.append("permission[").append(permission).append("] ");
}
if (StringUtil.isNotBlank(value)) {
authMessage.append("SpEL[").append(value).append("] ");
}
// 记录日志
List<Object> authArgs = new ArrayList<>();
String authLogger = "\n\n================ PreAuth Start ================" +
"\nPreAuth : {}" +
"\nClientId : {}" +
"\nBladeAuth : {}" +
"\nBladeUser : {}" +
"\nRequestURI : {}" +
"\nRequestIP: {}" +
"\nRequestParam: {}" +
"\n================ PreAuth End ================\n";
authArgs.add(authMessage.toString());
authArgs.add(AuthUtil.getClientId(request));
authArgs.add(WebUtil.getHeader(AUTH_HEADER));
authArgs.add(AuthUtil.getUser());
authArgs.add(WebUtil.getRequestURI(request));
authArgs.add(WebUtil.getIP(request));
authArgs.add(WebUtil.getRequestContent(request));
log.warn(authLogger, authArgs.toArray());
}
}
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
@@ -0,0 +1,233 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.auth;
import org.springblade.core.jwt.JwtUtil;
import org.springblade.core.launch.constant.TokenConstant;
import org.springblade.core.secure.BladeUser;
import org.springblade.core.secure.handler.IPermissionHandler;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.constant.RoleConstant;
import org.springblade.core.tool.utils.*;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Objects;
/**
* 权限判断
*
* @author Chill
*/
public class AuthFun {
/**
* 权限校验处理器
*/
private static IPermissionHandler permissionHandler;
private static IPermissionHandler getPermissionHandler() {
if (permissionHandler == null) {
permissionHandler = SpringUtil.getBean(IPermissionHandler.class);
}
return permissionHandler;
}
/**
* 判断角色是否具有接口权限
*
* @return {boolean}
*/
public boolean permissionAll() {
return getPermissionHandler().permissionAll();
}
/**
* 判断角色是否具有接口权限
*
* @param permission 权限编号
* @return {boolean}
*/
public boolean hasPermission(String permission) {
return getPermissionHandler().hasPermission(permission);
}
/**
* 判断角色是否具有菜单权限
*
* @param permission 菜单编号
* @return {boolean}
*/
public boolean hasMenu(String permission) {
return getPermissionHandler().hasMenu(permission);
}
/**
* 放行所有请求
*
* @return {boolean}
*/
public boolean permitAll() {
return true;
}
/**
* 只有超管角色才可访问
*
* @return {boolean}
*/
public boolean denyAll() {
return hasRole(RoleConstant.ADMIN);
}
/**
* 是否已授权
*
* @return {boolean}
*/
public boolean hasAuth() {
return AuthUtil.hasAuth();
}
/**
* 是否有时间授权
*
* @param start 开始时间
* @param end 结束时间
* @return {boolean}
*/
public boolean hasTimeAuth(Integer start, Integer end) {
Integer hour = DateUtil.hour();
return hour >= start && hour <= end;
}
/**
* 判断是否有该角色权限
*
* @param role 单角色
* @return {boolean}
*/
public boolean hasRole(String role) {
return hasAnyRole(role);
}
/**
* 判断是否具有所有角色权限
*
* @param role 角色集合
* @return {boolean}
*/
public boolean hasAllRole(String... role) {
for (String r : role) {
if (!hasRole(r)) {
return false;
}
}
return true;
}
/**
* 判断是否有该角色权限
*
* @param role 角色集合
* @return {boolean}
*/
public boolean hasAnyRole(String... role) {
BladeUser user = AuthUtil.getUser();
if (user == null) {
return false;
}
String userRole = user.getRoleName();
if (StringUtil.isBlank(userRole)) {
return false;
}
String[] roles = Func.toStrArray(userRole);
for (String r : role) {
if (CollectionUtil.contains(roles, r)) {
return true;
}
}
return false;
}
/**
* 判断请求是否为加密token
*
* @return {boolean}
*/
public boolean hasCrypto() {
HttpServletRequest request = WebUtil.getRequest();
String auth = Objects.requireNonNull(request).getHeader(TokenConstant.AUTH_HEADER);
return JwtUtil.isCrypto(
StringUtil.isNotBlank(auth) ? auth : request.getParameter(TokenConstant.AUTH_HEADER)
);
}
/**
* 判断令牌是否符合严格模式
*
* @return {boolean}
*/
public boolean hasStrictToken() {
BladeUser currentUser = AuthUtil.getUser();
return AuthUtil.userIncomplete(currentUser);
}
/**
* 判断是否包含安全请求头
*
* @return {boolean}
*/
public boolean hasStrictHeader() {
return !AuthUtil.secureHeaderIncomplete();
}
/**
* 判断是否有该请求头
*
* @param header 请求头
* @return {boolean}
*/
public boolean hasHeader(String header) {
HttpServletRequest request = WebUtil.getRequest();
String value = Objects.requireNonNull(request).getHeader(header);
return StringUtil.isNotBlank(value);
}
/**
* 判断是否有该请求头
*
* @param header 请求头
* @param key 请求值
* @return {boolean}
*/
public boolean hasHeader(String header, String key) {
HttpServletRequest request = WebUtil.getRequest();
String value = Objects.requireNonNull(request).getHeader(header);
return StringUtil.equals(value, key);
}
}
@@ -0,0 +1,56 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.config;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.jackson.BladeRoleSupplier;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
/**
* Jackson Views 角色提供者自动装配
* <p>
* 默认通过 {@link AuthUtil#getUserRole()} 获取当前用户角色名
* 用户可自定义 {@link BladeRoleSupplier} Bean 来覆盖
* </p>
*
* @author Chill
*/
@AutoConfiguration
public class BladeViewRoleConfiguration {
/**
* 默认角色名称提供者
* <p>使用 {@link ConditionalOnMissingBean} 允许用户自定义覆盖</p>
*/
@Bean
@ConditionalOnMissingBean
public BladeRoleSupplier roleNameSupplier() {
return AuthUtil::getUserRole;
}
}
@@ -0,0 +1,63 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.config;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.secure.nonce.LocalNonceStore;
import org.springblade.core.secure.nonce.NonceStore;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
/**
* NonceStore自动配置
* <p>
* 用于防重放攻击检测的Nonce存储配置
* 默认使用本地缓存实现外部可自行实现NonceStore接口并注册Bean覆盖
*
* @author Chill
*/
@Slf4j
@Order
@AutoConfiguration(before = RegistryConfiguration.class)
public class NonceStoreConfiguration {
/**
* 默认的NonceStore实现本地缓存
* <p>
* 使用ConcurrentHashMap存储nonce适用于单机环境
* 分布式环境可自行实现NonceStore接口如Redis实现并注册Bean覆盖
*
* @return LocalNonceStore
*/
@Bean
@ConditionalOnMissingBean(NonceStore.class)
public NonceStore nonceStore() {
return new LocalNonceStore();
}
}
@@ -0,0 +1,37 @@
package org.springblade.core.secure.config;
import lombok.AllArgsConstructor;
import org.springblade.core.secure.aspect.AdminAspect;
import org.springblade.core.secure.aspect.AdministratorAspect;
import org.springblade.core.secure.aspect.AuthAspect;
import org.springblade.core.secure.auth.AuthFun;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* 权限注解配置类
*
* @author BladeX
*/
@EnableAspectJAutoProxy(proxyTargetClass = true)
@AutoConfiguration
@AllArgsConstructor
public class PreAuthConfiguration {
@Bean
public AuthAspect authAspect() {
return new AuthAspect(new AuthFun());
}
@Bean
public AdminAspect adminAspect() {
return new AdminAspect();
}
@Bean
public AdministratorAspect administratorAspect() {
return new AdministratorAspect();
}
}
@@ -0,0 +1,89 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.config;
import lombok.AllArgsConstructor;
import org.springblade.core.launch.props.BladeProperties;
import org.springblade.core.launch.server.ServerInfo;
import org.springblade.core.secure.handler.*;
import org.springblade.core.secure.props.KeyProperties;
import org.springblade.core.secure.registry.SecureRegistry;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
/**
* secure注册默认配置
*
* @author Chill
*/
@Order
@AutoConfiguration(before = SecureConfiguration.class)
@EnableConfigurationProperties({KeyProperties.class})
@AllArgsConstructor
public class RegistryConfiguration {
@Bean
@ConditionalOnMissingBean(SecureRegistry.class)
public SecureRegistry secureRegistry() {
return new SecureRegistry();
}
@Bean
@ConditionalOnMissingBean(ISecureHandler.class)
public ISecureHandler secureHandler() {
return new BladeSecureHandler();
}
@Bean
@ConditionalOnMissingBean(IPermissionHandler.class)
public IPermissionHandler permissionHandler(JdbcTemplate jdbcTemplate) {
return new BladePermissionHandler(jdbcTemplate);
}
@Bean
@ConditionalOnMissingBean(IApiKeyLogHandler.class)
public IApiKeyLogHandler apiKeyLogHandler(JdbcTemplate jdbcTemplate, BladeProperties bladeProperties, ServerInfo serverInfo) {
return new BladeApiKeyLogHandler(jdbcTemplate, bladeProperties, serverInfo);
}
@Bean
@ConditionalOnMissingBean(IApiKeyHandler.class)
public IApiKeyHandler apiKeyHandler(JdbcTemplate jdbcTemplate, KeyProperties keyProperties, IApiKeyLogHandler apiKeyLogHandler) {
return new BladeApiKeyHandler(jdbcTemplate, keyProperties, apiKeyLogHandler);
}
@Bean
@ConditionalOnMissingBean(UserTokenHandler.class)
public UserTokenHandler userTokenHandler() {
return new BladeUserTokenHandler();
}
}
@@ -0,0 +1,143 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.config;
import lombok.AllArgsConstructor;
import org.springblade.core.launch.props.BladeProperties;
import org.springblade.core.secure.handler.ISecureHandler;
import org.springblade.core.secure.nonce.NonceStore;
import org.springblade.core.secure.props.AuthSecure;
import org.springblade.core.secure.props.BasicSecure;
import org.springblade.core.secure.props.BladeSecureProperties;
import org.springblade.core.secure.props.SignSecure;
import org.springblade.core.secure.registry.SecureRegistry;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.core.annotation.Order;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
import java.util.stream.Collectors;
/**
* 安全配置类
*
* @author Chill
*/
@Order
@AutoConfiguration
@AllArgsConstructor
@EnableConfigurationProperties({BladeSecureProperties.class})
public class SecureConfiguration implements WebMvcConfigurer {
private final SecureRegistry secureRegistry;
private final BladeProperties bladeProperties;
private final BladeSecureProperties secureProperties;
private final ISecureHandler secureHandler;
private final NonceStore nonceStore;
@Override
public void addInterceptors(@NonNull InterceptorRegistry registry) {
// 设置请求授权
if (secureRegistry.isAuthEnabled() || secureProperties.getAuthEnabled()) {
List<AuthSecure> authSecures = this.secureRegistry.addAuthPatterns(secureProperties.getAuth()).getAuthSecures();
if (!authSecures.isEmpty()) {
registry.addInterceptor(secureHandler.authInterceptor(secureProperties, authSecures));
// 设置路径放行
secureRegistry.excludePathPatterns(authSecures.stream().map(AuthSecure::getPattern).collect(Collectors.toList()));
}
}
// 设置基础认证授权
if (secureRegistry.isBasicEnabled() || secureProperties.getBasicEnabled()) {
List<BasicSecure> basicSecures = this.secureRegistry.addBasicPatterns(secureProperties.getBasic()).getBasicSecures();
if (!basicSecures.isEmpty()) {
registry.addInterceptor(secureHandler.basicInterceptor(basicSecures));
// 设置路径放行
secureRegistry.excludePathPatterns(basicSecures.stream().map(BasicSecure::getPattern).collect(Collectors.toList()));
}
}
// 设置签名认证授权
if (secureRegistry.isSignEnabled() || secureProperties.getSignEnabled()) {
List<SignSecure> signSecures = this.secureRegistry.addSignPatterns(secureProperties.getSign()).getSignSecures();
if (!signSecures.isEmpty()) {
registry.addInterceptor(secureHandler.signInterceptor(signSecures, nonceStore));
// 设置路径放行
secureRegistry.excludePathPatterns(signSecures.stream().map(SignSecure::getPattern).collect(Collectors.toList()));
}
}
// 设置令牌严格模式
if (!secureRegistry.isStrictToken()) {
secureProperties.setStrictToken(false);
}
// 设置请求头严格模式
if (!secureRegistry.isStrictHeader()) {
secureProperties.setStrictHeader(false);
}
// 设置客户端授权
if (secureRegistry.isClientEnabled() || secureProperties.getClientEnabled()) {
secureProperties.getClient().forEach(clientSecure -> {
InterceptorRegistration clientRegistration = registry.addInterceptor(secureHandler.clientInterceptor(clientSecure.getClientId()))
.addPathPatterns(clientSecure.getPathPatterns())
.excludePathPatterns(clientSecure.getExcludePatterns());
configureExcludePatterns(clientRegistration);
});
}
// 设置路径放行
if (secureRegistry.isEnabled() || secureProperties.getEnabled()) {
InterceptorRegistration tokenRegistration = registry.addInterceptor(secureHandler.tokenInterceptor(secureProperties));
configureExcludePatterns(tokenRegistration);
}
}
/**
* 配置拦截器的公共排除路径
*
* @param registration 拦截器注册对象
*/
private void configureExcludePatterns(InterceptorRegistration registration) {
registration
.excludePathPatterns(secureRegistry.getExcludePatterns())
.excludePathPatterns(secureRegistry.getDefaultExcludePatterns())
.excludePathPatterns(secureProperties.getSkipUrl());
// 宽松模式下获取放行路径且再新建一套自定义放行路径用于处理cloud网关虚拟路径导致未匹配的问题
// 严格模式下不予处理应严格按照cloud和boot的路由进行匹配
if (!secureProperties.getStrictToken()) {
registration.excludePathPatterns(secureProperties.getSkipUrl().stream()
.map(url -> StringUtil.removePrefix(url, StringPool.SLASH + bladeProperties.getName())).toList());
}
}
}
@@ -0,0 +1,55 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.config;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.secure.resolver.TokenArgumentResolver;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* Token配置
*
* @author Chill
*/
@Slf4j
@AutoConfiguration
@Order(Ordered.HIGHEST_PRECEDENCE)
@AllArgsConstructor
public class TokenConfiguration implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new TokenArgumentResolver());
}
}

Some files were not shown because too many files have changed in this diff Show More