mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2025-12-08 17:12:28 +08:00
Compare commits
74 Commits
v3.6.2
...
v3.6.3_spr
| Author | SHA1 | Date | |
|---|---|---|---|
| e616c5d8fe | |||
| cddf23c787 | |||
| 70a37309dd | |||
| 48555b5219 | |||
| 06d58f202f | |||
| 628870af9b | |||
| b46a6438e6 | |||
| 5488f99723 | |||
| 6bc1fe8d21 | |||
| 7cac16320c | |||
| 24dbd1db39 | |||
| 46b026b989 | |||
| 94c45f5e0f | |||
| 8950e19d4e | |||
| 99eb88f71c | |||
| 824d7839d8 | |||
| c88f9d95d4 | |||
| beb0bc2f64 | |||
| f741db874c | |||
| d684c09392 | |||
| 364be22dd0 | |||
| 20efa3bf9a | |||
| c7977dda3d | |||
| c27c5a9a9b | |||
| 0ab280f812 | |||
| c3066dac17 | |||
| b650d512b3 | |||
| 925ec9447d | |||
| 411a73c1bf | |||
| 84077e6e24 | |||
| 184cf97304 | |||
| 5f425b49b2 | |||
| 3ac8ee304a | |||
| 0faac01bb7 | |||
| 74d88a8fcc | |||
| f532e57862 | |||
| 3656264f8a | |||
| 3361d48cd4 | |||
| ed86ea3da1 | |||
| 3deb0e5487 | |||
| 9e4792941e | |||
| b5fd5fe782 | |||
| 33c0104a02 | |||
| 81ed5100af | |||
| 87f9dc0064 | |||
| b311fedc6b | |||
| e321a0405f | |||
| d8bc74794d | |||
| 732f05dc74 | |||
| 6ce92798c6 | |||
| f4454e9348 | |||
| d9134ae0c8 | |||
| 25180e41c8 | |||
| a99e3f2268 | |||
| d27c354bf1 | |||
| d818b1dd9d | |||
| bcdbec0091 | |||
| 098bb12b9e | |||
| 4a6c750b19 | |||
| d396e5304a | |||
| 9bed25be8c | |||
| 7109b42092 | |||
| 1667b14194 | |||
| e9514873d2 | |||
| 0ee090664e | |||
| 4a9eda4ab0 | |||
| 2416c8b251 | |||
| 5b056f9dd6 | |||
| a93998dc56 | |||
| 268c27a782 | |||
| 23ace2712a | |||
| 157feeb925 | |||
| 4e25d4162f | |||
| 47a68f31e1 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -10,5 +10,5 @@ rebel.xml
|
||||
## front
|
||||
**/*.lock
|
||||
os_del.cmd
|
||||
os_del_doc.cmd
|
||||
.svn
|
||||
|
||||
*.log
|
||||
@ -7,13 +7,13 @@
|
||||
JEECG BOOT Low Code Development Platform
|
||||
===============
|
||||
|
||||
当前最新版本: 3.6.2(发布日期:2024-01-08)
|
||||
当前最新版本: 3.6.1(发布日期:2023-12-11)
|
||||
|
||||
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||
[](http://www.jeecg.com)
|
||||
[](https://jeecg.blog.csdn.net)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
|
||||
|
||||
@ -7,13 +7,13 @@
|
||||
JEECG BOOT 低代码开发平台
|
||||
===============
|
||||
|
||||
当前最新版本: 3.6.2(发布日期:2024-01-08)
|
||||
当前最新版本: 3.6.1(发布日期:2023-12-11)
|
||||
|
||||
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||
[](http://jeecg.com/aboutusIndex)
|
||||
[](https://jeecg.blog.csdn.net)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
|
||||
@ -49,7 +49,6 @@ Jeecg-Boot低代码开发平台,可以应用在任何J2EE项目的开发中,
|
||||
| Github | [jeecgboot-vue3](https://github.com/jeecgboot/jeecgboot-vue3) | [jeecg-boot](https://github.com/jeecgboot/jeecg-boot) |
|
||||
| 码云 | [jeecgboot-vue3](https://gitee.com/jeecg/jeecgboot-vue3) | [jeecg-boot](https://gitee.com/jeecg/jeecg-boot) |
|
||||
|
||||
> 官方已推出 `SpringBoot3+JDK17版本` [分支源码下载](https://github.com/jeecgboot/jeecg-boot/tree/springboot3) | [升级SpringBoot3博客](https://blog.csdn.net/zhangdaiscott/article/details/134805602)
|
||||
|
||||
#### 项目说明
|
||||
|
||||
@ -58,6 +57,7 @@ Jeecg-Boot低代码开发平台,可以应用在任何J2EE项目的开发中,
|
||||
| `jeecgboot-vue3` | 前端源码 (Vue3版本) |
|
||||
| `jeecg-boot` | 后端JAVA源码(支持微服务) |
|
||||
| `jeecg-uniapp` | [APP开发框架,一份代码多终端适配,同时支持APP、小程序、H5](https://github.com/jeecgboot/jeecg-uniapp) |
|
||||
| `SpringBoot3+JDK17 后端分支` | [分支源码](https://github.com/jeecgboot/jeecg-boot/tree/springboot3) [升级博客](https://blog.csdn.net/zhangdaiscott/article/details/134805602) |
|
||||
| `更多开源项目` | [更多底层源码下载](http://jeecg.com/download) |
|
||||
|
||||
|
||||
@ -83,7 +83,7 @@ Docker快速启动项目
|
||||
-----------------------------------
|
||||
|
||||
- 项目官网: [http://www.jeecg.com](http://www.jeecg.com)
|
||||
- 开发文档: [https://help.jeecg.com](https://help.jeecg.com)
|
||||
- 开发文档: [http://help.jeecg.com](http://help.jeecg.com)
|
||||
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [常见问题 ](http://www.jeecg.com/doc/qa) | [视频教程](https://space.bilibili.com/454617261/channel/series) | [1分钟低代码体验](https://my.oschina.net/jeecg/blog/3083313)
|
||||
|
||||
- 在线演示 : [Vue3演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex) | [敲敲云零代码](https://qiaoqiaoyun.com)
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
11
db/增量SQL/3.6.0升级到3.6.1升级脚本.sql
Normal file
11
db/增量SQL/3.6.0升级到3.6.1升级脚本.sql
Normal file
@ -0,0 +1,11 @@
|
||||
-- 新增风格一对多内嵌和Tab风格
|
||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external)
|
||||
VALUES ('1691031996d5931315212', '1455100420297859074', 'AUTO在线一对多内嵌', '/online/cgformInnerTableList/:id', 'super/online/cgform/auto/innerTable/OnlCgformInnerTableList', 1, '', NULL, 1, NULL, '0', 1.00, 0, NULL, 1, 0, 1, 0, NULL, 'admin', '2023-08-14 18:20:20', 'admin', '2023-08-14 18:46:18', 0, 0, NULL, 0);
|
||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external)
|
||||
VALUES ('1691031996d5931315213', '1455100420297859074', 'AUTO在线Tab风格', '/online/cgformTabList/:id', 'super/online/cgform/auto/tab/OnlCgformTabList', 1, '', NULL, 1, NULL, '0', 1.00, 0, NULL, 1, 0, 1, 0, NULL, 'admin', '2023-08-14 18:20:20', 'admin', '2023-08-14 18:46:18', 0, 0, NULL, 0);
|
||||
|
||||
-- 【安全】online敏感接口,加权限注解(sql解析接口、同步数据库接口、导入表接口)
|
||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('1699374704168534017', '1460888189937176577', 'SQL解析', NULL, NULL, 0, NULL, NULL, 2, 'online:report:parseSql', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2023-09-06 18:51:17', NULL, NULL, 0, 0, '1', 0);
|
||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('1699374509749960705', '1455101470794850305', '查询数据库表名', NULL, NULL, 0, NULL, NULL, 2, 'online:form:queryTables', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2023-09-06 18:50:31', NULL, NULL, 0, 0, '1', 0);
|
||||
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('1699374269152100354', '1455101470794850305', '同步数据库', NULL, NULL, 0, NULL, NULL, 2, 'online:form:syncDb', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2023-09-06 18:49:33', NULL, NULL, 0, 0, '1', 0);
|
||||
update sys_permission set is_leaf=0 where id in ('1460888189937176577','1455101470794850305');
|
||||
45
db/增量SQL/sas升级脚本.sql
Normal file
45
db/增量SQL/sas升级脚本.sql
Normal file
@ -0,0 +1,45 @@
|
||||
CREATE TABLE `oauth2_registered_client` (
|
||||
`id` varchar(100) NOT NULL,
|
||||
`client_id` varchar(100) NOT NULL,
|
||||
`client_id_issued_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`client_secret` varchar(200) DEFAULT NULL,
|
||||
`client_secret_expires_at` timestamp NULL DEFAULT NULL,
|
||||
`client_name` varchar(200) NOT NULL,
|
||||
`client_authentication_methods` varchar(1000) NOT NULL,
|
||||
`authorization_grant_types` varchar(1000) NOT NULL,
|
||||
`redirect_uris` varchar(1000) DEFAULT NULL,
|
||||
`post_logout_redirect_uris` varchar(1000) DEFAULT NULL,
|
||||
`scopes` varchar(1000) NOT NULL,
|
||||
`client_settings` varchar(2000) NOT NULL,
|
||||
`token_settings` varchar(2000) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
INSERT INTO `oauth2_registered_client`
|
||||
(`id`,
|
||||
`client_id`,
|
||||
`client_id_issued_at`,
|
||||
`client_secret`,
|
||||
`client_secret_expires_at`,
|
||||
`client_name`,
|
||||
`client_authentication_methods`,
|
||||
`authorization_grant_types`,
|
||||
`redirect_uris`,
|
||||
`post_logout_redirect_uris`,
|
||||
`scopes`,
|
||||
`client_settings`,
|
||||
`token_settings`)
|
||||
VALUES
|
||||
('3eacac0e-0de9-4727-9a64-6bdd4be2ee1f',
|
||||
'jeecg-client',
|
||||
now(),
|
||||
'secret',
|
||||
null,
|
||||
'3eacac0e-0de9-4727-9a64-6bdd4be2ee1f',
|
||||
'client_secret_basic',
|
||||
'refresh_token,authorization_code,password,app,phone,social',
|
||||
'http://127.0.0.1:8080/jeecg-',
|
||||
'http://127.0.0.1:8080/',
|
||||
'*',
|
||||
'{"@class":"java.util.Collections$UnmodifiableMap","settings.client.require-proof-key":false,"settings.client.require-authorization-consent":true}',
|
||||
'{"@class":"java.util.Collections$UnmodifiableMap","settings.token.reuse-refresh-tokens":true,"settings.token.id-token-signature-algorithm":["org.springframework.security.oauth2.jose.jws.SignatureAlgorithm","RS256"],"settings.token.access-token-time-to-live":["java.time.Duration",300000.000000000],"settings.token.access-token-format":{"@class":"org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat","value":"self-contained"},"settings.token.refresh-token-time-to-live":["java.time.Duration",3600.000000000],"settings.token.authorization-code-time-to-live":["java.time.Duration",300000.000000000],"settings.token.device-code-time-to-live":["java.time.Duration",300000.000000000]}');
|
||||
11
db/增量SQL/版本升级说明.txt
Normal file
11
db/增量SQL/版本升级说明.txt
Normal file
@ -0,0 +1,11 @@
|
||||
版本升级方法?
|
||||
|
||||
JeecgBoot属于平台级产品,每次升级改动内容较多,目前做不到平滑升级。
|
||||
|
||||
升级方案建议:
|
||||
1.代码升级 => 本地版本通过svn或者git做好主干,在分支上做业务开发,jeecg每次版本发布,可以手工覆盖主干的代码,对比合并代码;
|
||||
2.数据库升级 => 针对数据库我们每次发布会提供增量升级SQL,可以通过执行增量SQL实现数据库的升级。
|
||||
3.兼容问题 => 每次版本发布会针对不兼容地方标注说明,需要手工修改不兼容的代码。
|
||||
|
||||
注意: 升级sql目前只提供mysql版本,执行完脚步后,新菜单需要手工进行角色授权,刷新首页才会出现。
|
||||
【20230820 放开了系统管理等模块权限注解,如果没权限请通过角色授权授权对应的按钮权限】
|
||||
15
db/版本升级说明.md
15
db/版本升级说明.md
@ -1,15 +0,0 @@
|
||||
# 版本升级方法
|
||||
|
||||
> JeecgBoot属于平台级产品,每次升级改动较大,目前做不到平滑升级。
|
||||
|
||||
### 增量升级方案
|
||||
#### 1.代码合并
|
||||
本地通过svn或git做好主干,在分支上做业务开发,jeecg每次版本发布,可以手工覆盖主干的代码,对比合并代码;
|
||||
|
||||
#### 2.数据库升级
|
||||
- 从3.6.2+版本增加flyway自动升级数据库机制,支持 mysql5.7、mysql8;
|
||||
- 其他库请手工执行SQL, 目录: `jeecg-module-system\jeecg-system-start\src\main\resources\flyway\sql\mysql`
|
||||
> 注意: 升级sql只提供mysql版本;如果有权限升级, 还需要手工角色授权,退出重新登录才好使。
|
||||
|
||||
#### 3.兼容问题
|
||||
每次发版,会针对不兼容地方重点说明。
|
||||
@ -4,11 +4,15 @@
|
||||
<parent>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<artifactId>jeecg-boot-parent</artifactId>
|
||||
<version>3.6.2</version>
|
||||
<version>3.6.1</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>jeecg-boot-base-core</artifactId>
|
||||
|
||||
<properties>
|
||||
<spring-boot.version>3.1.5</spring-boot.version>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>aliyun</id>
|
||||
@ -43,12 +47,22 @@
|
||||
<!--jeecg-tools-->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<artifactId>jeecg-boot-common</artifactId>
|
||||
<artifactId>jeecg-boot-common3</artifactId>
|
||||
</dependency>
|
||||
<!--集成springmvc框架并实现自动配置 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-undertow</artifactId>
|
||||
</dependency>
|
||||
<!-- websocket -->
|
||||
<dependency>
|
||||
@ -105,14 +119,14 @@
|
||||
<!-- druid -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-starter</artifactId>
|
||||
<artifactId>druid-spring-boot-3-starter</artifactId>
|
||||
<version>${druid.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 动态数据源 -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
|
||||
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
|
||||
<version>${dynamic-datasource-spring-boot-starter.version}</version>
|
||||
</dependency>
|
||||
|
||||
@ -145,7 +159,7 @@
|
||||
<version>${postgresql.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- Quartz定时任务 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@ -159,33 +173,25 @@
|
||||
<version>${java-jwt.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!--shiro-->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-spring-boot-starter</artifactId>
|
||||
<version>${shiro.version}</version>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
|
||||
</dependency>
|
||||
<!-- shiro-redis -->
|
||||
<dependency>
|
||||
<groupId>org.crazycake</groupId>
|
||||
<artifactId>shiro-redis</artifactId>
|
||||
<version>${shiro-redis.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-core</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<artifactId>checkstyle</artifactId>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
|
||||
</dependency>
|
||||
<!-- 添加spring security cas支持 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-cas</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- knife4j -->
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-spring-boot-starter</artifactId>
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
<version>${knife4j-spring-boot-starter.version}</version>
|
||||
</dependency>
|
||||
|
||||
@ -199,7 +205,7 @@
|
||||
|
||||
<!-- AutoPoi Excel工具类-->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework</groupId>
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>autopoi-web</artifactId>
|
||||
<version>${autopoi-web.version}</version>
|
||||
<exclusions>
|
||||
@ -242,6 +248,16 @@
|
||||
<dependency>
|
||||
<groupId>com.xkcoding.justauth</groupId>
|
||||
<artifactId>justauth-spring-boot-starter</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
package org.apache.shiro;
|
||||
|
||||
import org.apache.shiro.subject.Subject;
|
||||
|
||||
/**
|
||||
* 兼容处理Online功能使用处理,请勿修改
|
||||
* @author eightmonth@qq.com
|
||||
* @date 2024/4/29 14:05
|
||||
*/
|
||||
public class SecurityUtils {
|
||||
|
||||
|
||||
public static Subject getSubject() {
|
||||
return new Subject() {
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return Subject.super.getPrincipal();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package org.apache.shiro.subject;
|
||||
|
||||
import org.jeecg.config.security.utils.SecureUtil;
|
||||
|
||||
/**
|
||||
* 兼容处理Online功能使用处理,请勿修改
|
||||
* @author eightmonth@qq.com
|
||||
* @date 2024/4/29 14:18
|
||||
*/
|
||||
public interface Subject {
|
||||
default Object getPrincipal() {
|
||||
return SecureUtil.currentUser();
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package org.jeecg.common.api;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.jeecg.common.system.vo.*;
|
||||
|
||||
import java.util.List;
|
||||
@ -50,6 +51,13 @@ public interface CommonAPI {
|
||||
*/
|
||||
public LoginUser getUserByName(String username);
|
||||
|
||||
/**
|
||||
* 5根据用户手机号查询用户信息
|
||||
* @param username
|
||||
* @return
|
||||
*/
|
||||
public LoginUser getUserByPhone(String phone);
|
||||
|
||||
|
||||
/**
|
||||
* 6字典表的 翻译
|
||||
@ -117,17 +125,41 @@ public interface CommonAPI {
|
||||
*/
|
||||
Map<String, List<DictModel>> translateManyDict(String dictCodes, String keys);
|
||||
|
||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
/**
|
||||
* 15 字典表的 翻译,可批量
|
||||
* @param table
|
||||
* @param text
|
||||
* @param code
|
||||
* @param keys 多个用逗号分割
|
||||
* @param dataSource 数据源
|
||||
* @return
|
||||
*/
|
||||
List<DictModel> translateDictFromTableByKeys(String table, String text, String code, String keys, String dataSource);
|
||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
List<DictModel> translateDictFromTableByKeys(String table, String text, String code, String keys);
|
||||
|
||||
/**
|
||||
* 登录加载系统字典
|
||||
* @return
|
||||
*/
|
||||
Map<String,List<DictModel>> queryAllDictItems();
|
||||
|
||||
/**
|
||||
* 查询SysDepart集合
|
||||
* @param userId
|
||||
* @return
|
||||
*/
|
||||
List<SysDepartModel> queryUserDeparts(String userId);
|
||||
|
||||
/**
|
||||
* 根据用户名设置部门ID
|
||||
* @param username
|
||||
* @param orgCode
|
||||
*/
|
||||
void updateUserDepart(String username,String orgCode,Integer loginTenantId);
|
||||
|
||||
/**
|
||||
* 设置登录租户
|
||||
* @param username
|
||||
* @return
|
||||
*/
|
||||
JSONObject setLoginTenant(String username);
|
||||
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ package org.jeecg.common.api.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
package org.jeecg.common.api.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
|
||||
@ -15,7 +14,7 @@ import java.io.Serializable;
|
||||
* @date 2019年1月19日
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(value="接口返回对象", description="接口返回对象")
|
||||
@Schema(description="接口返回对象")
|
||||
public class Result<T> implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
@ -23,31 +22,31 @@ public class Result<T> implements Serializable {
|
||||
/**
|
||||
* 成功标志
|
||||
*/
|
||||
@ApiModelProperty(value = "成功标志")
|
||||
@Schema(description = "成功标志")
|
||||
private boolean success = true;
|
||||
|
||||
/**
|
||||
* 返回处理消息
|
||||
*/
|
||||
@ApiModelProperty(value = "返回处理消息")
|
||||
@Schema(description = "返回处理消息")
|
||||
private String message = "";
|
||||
|
||||
/**
|
||||
* 返回代码
|
||||
*/
|
||||
@ApiModelProperty(value = "返回代码")
|
||||
@Schema(description = "返回代码")
|
||||
private Integer code = 0;
|
||||
|
||||
/**
|
||||
* 返回数据对象 data
|
||||
*/
|
||||
@ApiModelProperty(value = "返回数据对象")
|
||||
@Schema(description = "返回数据对象")
|
||||
private T result;
|
||||
|
||||
/**
|
||||
* 时间戳
|
||||
*/
|
||||
@ApiModelProperty(value = "时间戳")
|
||||
@Schema(description = "时间戳")
|
||||
private long timestamp = System.currentTimeMillis();
|
||||
|
||||
public Result() {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package org.jeecg.common.aspect;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.alibaba.fastjson.serializer.PropertyFilter;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
@ -15,19 +15,21 @@ import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.constant.enums.ModuleType;
|
||||
import org.jeecg.common.constant.enums.OperateTypeEnum;
|
||||
import org.jeecg.config.security.utils.SecureUtil;
|
||||
import org.jeecg.modules.base.service.BaseCommonService;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.IpUtils;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Date;
|
||||
|
||||
@ -100,7 +102,7 @@ public class AutoLogAspect {
|
||||
//设置IP地址
|
||||
dto.setIp(IpUtils.getIpAddr(request));
|
||||
//获取登录用户信息
|
||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
LoginUser sysUser = SecureUtil.currentUser();
|
||||
if(sysUser!=null){
|
||||
dto.setUserid(sysUser.getUsername());
|
||||
dto.setUsername(sysUser.getRealname());
|
||||
@ -158,6 +160,9 @@ public class AutoLogAspect {
|
||||
if(value!=null && value.toString().length()>length){
|
||||
return false;
|
||||
}
|
||||
if(value instanceof MultipartFile){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@ -140,15 +140,11 @@ public class DictAspect {
|
||||
String code = field.getAnnotation(Dict.class).dicCode();
|
||||
String text = field.getAnnotation(Dict.class).dicText();
|
||||
String table = field.getAnnotation(Dict.class).dictTable();
|
||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
String dataSource = field.getAnnotation(Dict.class).ds();
|
||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
|
||||
List<String> dataList;
|
||||
String dictCode = code;
|
||||
if (!StringUtils.isEmpty(table)) {
|
||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
dictCode = String.format("%s,%s,%s,%s", table, text, code, dataSource);
|
||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
dictCode = String.format("%s,%s,%s", table, text, code);
|
||||
}
|
||||
dataList = dataListMap.computeIfAbsent(dictCode, k -> new ArrayList<>());
|
||||
this.listAddAllDeduplicate(dataList, Arrays.asList(value.split(",")));
|
||||
@ -173,15 +169,10 @@ public class DictAspect {
|
||||
String code = field.getAnnotation(Dict.class).dicCode();
|
||||
String text = field.getAnnotation(Dict.class).dicText();
|
||||
String table = field.getAnnotation(Dict.class).dictTable();
|
||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
// 自定义的字典表数据源
|
||||
String dataSource = field.getAnnotation(Dict.class).ds();
|
||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
|
||||
String fieldDictCode = code;
|
||||
if (!StringUtils.isEmpty(table)) {
|
||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
fieldDictCode = String.format("%s,%s,%s,%s", table, text, code, dataSource);
|
||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
fieldDictCode = String.format("%s,%s,%s", table, text, code);
|
||||
}
|
||||
|
||||
String value = record.getString(field.getName());
|
||||
@ -283,25 +274,9 @@ public class DictAspect {
|
||||
String[] arr = dictCode.split(",");
|
||||
String table = arr[0], text = arr[1], code = arr[2];
|
||||
String values = String.join(",", needTranslDataTable);
|
||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
// 自定义的数据源
|
||||
String dataSource = null;
|
||||
if (arr.length > 3) {
|
||||
dataSource = arr[3];
|
||||
}
|
||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
log.debug("translateDictFromTableByKeys.dictCode:" + dictCode);
|
||||
log.debug("translateDictFromTableByKeys.values:" + values);
|
||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
|
||||
//update-begin---author:wangshuai---date:2024-01-09---for:微服务下为空报错没有参数需要传递空字符串---
|
||||
if(null == dataSource){
|
||||
dataSource = "";
|
||||
}
|
||||
//update-end---author:wangshuai---date:2024-01-09---for:微服务下为空报错没有参数需要传递空字符串---
|
||||
|
||||
List<DictModel> texts = commonApi.translateDictFromTableByKeys(table, text, code, values, dataSource);
|
||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
List<DictModel> texts = commonApi.translateDictFromTableByKeys(table, text, code, values);
|
||||
log.debug("translateDictFromTableByKeys.result:" + texts);
|
||||
List<DictModel> list = translText.computeIfAbsent(dictCode, k -> new ArrayList<>());
|
||||
list.addAll(texts);
|
||||
|
||||
@ -21,7 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@ -39,16 +39,4 @@ public @interface Dict {
|
||||
* @return 返回类型: String
|
||||
*/
|
||||
String dictTable() default "";
|
||||
|
||||
|
||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
/**
|
||||
* 方法描述: 数据字典表所在数据源名称
|
||||
* 作 者: chenrui
|
||||
* 日 期: 2023年12月20日-下午4:58
|
||||
*
|
||||
* @return 返回类型: String
|
||||
*/
|
||||
String ds() default "";
|
||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
}
|
||||
|
||||
@ -69,8 +69,6 @@ public interface CommonConstant {
|
||||
|
||||
/** {@code 500 Server Error} (HTTP/1.0 - RFC 1945) */
|
||||
Integer SC_INTERNAL_SERVER_ERROR_500 = 500;
|
||||
/** {@code 404 Not Found} (HTTP/1.0 - RFC 1945) */
|
||||
Integer SC_INTERNAL_NOT_FOUND_404 = 404;
|
||||
/** {@code 200 OK} (HTTP/1.0 - RFC 1945) */
|
||||
Integer SC_OK_200 = 200;
|
||||
|
||||
@ -80,7 +78,7 @@ public interface CommonConstant {
|
||||
/** 登录用户Shiro权限缓存KEY前缀 */
|
||||
public static String PREFIX_USER_SHIRO_CACHE = "shiro:cache:org.jeecg.config.shiro.ShiroRealm.authorizationCache:";
|
||||
/** 登录用户Token令牌缓存KEY前缀 */
|
||||
String PREFIX_USER_TOKEN = "prefix_user_token:";
|
||||
String PREFIX_USER_TOKEN = "token::jeecg-client::";
|
||||
// /** Token缓存时间:3600秒即一小时 */
|
||||
// int TOKEN_EXPIRE_TIME = 3600;
|
||||
|
||||
|
||||
@ -17,9 +17,6 @@ public interface DataBaseConstant {
|
||||
|
||||
/**postgreSQL达梦数据库*/
|
||||
public static final String DB_TYPE_POSTGRESQL = "POSTGRESQL";
|
||||
|
||||
/**人大金仓数据库*/
|
||||
public static final String DB_TYPE_KINGBASEES = "KINGBASEES";
|
||||
|
||||
/**sqlserver数据库*/
|
||||
public static final String DB_TYPE_SQLSERVER = "SQLSERVER";
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
package org.jeecg.common.exception;
|
||||
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
|
||||
/**
|
||||
* @Description: jeecg-boot自定义异常
|
||||
* @author: jeecg-boot
|
||||
@ -9,24 +7,10 @@ import org.jeecg.common.constant.CommonConstant;
|
||||
public class JeecgBootException extends RuntimeException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 返回给前端的错误code
|
||||
*/
|
||||
private int errCode = CommonConstant.SC_INTERNAL_SERVER_ERROR_500;
|
||||
|
||||
public JeecgBootException(String message){
|
||||
super(message);
|
||||
}
|
||||
|
||||
public JeecgBootException(String message, int errCode){
|
||||
super(message);
|
||||
this.errCode = errCode;
|
||||
}
|
||||
|
||||
public int getErrCode() {
|
||||
return errCode;
|
||||
}
|
||||
|
||||
|
||||
public JeecgBootException(Throwable cause)
|
||||
{
|
||||
super(cause);
|
||||
|
||||
@ -2,16 +2,17 @@ package org.jeecg.common.exception;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.AuthorizationException;
|
||||
import org.apache.shiro.authz.UnauthorizedException;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.enums.SentinelErrorInfoEnum;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.data.redis.connection.PoolException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
@ -27,13 +28,31 @@ import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
@Slf4j
|
||||
public class JeecgBootExceptionHandler {
|
||||
|
||||
/**
|
||||
* 验证码错误异常
|
||||
*/
|
||||
|
||||
@ExceptionHandler(JeecgCaptchaException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public Result<?> handleJeecgCaptchaException(JeecgCaptchaException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.error(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(AuthenticationException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public Result<?> handleJeecgCaptchaException(AuthenticationException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.error(401, e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理自定义异常
|
||||
*/
|
||||
@ExceptionHandler(JeecgBootException.class)
|
||||
public Result<?> handleJeecgBootException(JeecgBootException e){
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.error(e.getErrCode(), e.getMessage());
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -67,9 +86,8 @@ public class JeecgBootExceptionHandler {
|
||||
return Result.error("数据库中已存在该记录");
|
||||
}
|
||||
|
||||
@ExceptionHandler({UnauthorizedException.class, AuthorizationException.class})
|
||||
public Result<?> handleAuthorizationException(AuthorizationException e){
|
||||
log.error(e.getMessage(), e);
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
public Result<?> handleAuthorizationException(AccessDeniedException e){
|
||||
return Result.noauth("没有权限,请联系管理员授权");
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
package org.jeecg.common.exception;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author kezhijie@wuhandsj.com
|
||||
* @date 2024/1/2 11:38
|
||||
*/
|
||||
@Data
|
||||
public class JeecgCaptchaException extends RuntimeException{
|
||||
|
||||
private Integer code;
|
||||
|
||||
private static final long serialVersionUID = -9093410345065209053L;
|
||||
|
||||
public JeecgCaptchaException(Integer code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public JeecgCaptchaException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public JeecgCaptchaException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,18 @@
|
||||
package org.jeecg.common.system.base.controller;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.beanutils.PropertyUtils;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.config.security.utils.SecureUtil;
|
||||
import org.jeecgframework.poi.excel.ExcelImportUtil;
|
||||
import org.jeecgframework.poi.excel.def.NormalExcelConstants;
|
||||
import org.jeecgframework.poi.excel.entity.ExportParams;
|
||||
@ -19,13 +20,14 @@ import org.jeecgframework.poi.excel.entity.ImportParams;
|
||||
import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
|
||||
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
@ -51,7 +53,7 @@ public class JeecgController<T, S extends IService<T>> {
|
||||
protected ModelAndView exportXls(HttpServletRequest request, T object, Class<T> clazz, String title) {
|
||||
// Step.1 组装查询条件
|
||||
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
LoginUser sysUser = SecureUtil.currentUser();
|
||||
|
||||
// 过滤选中数据
|
||||
String selections = request.getParameter("selections");
|
||||
@ -89,7 +91,7 @@ public class JeecgController<T, S extends IService<T>> {
|
||||
protected ModelAndView exportXlsSheet(HttpServletRequest request, T object, Class<T> clazz, String title,String exportFields,Integer pageNum) {
|
||||
// Step.1 组装查询条件
|
||||
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
LoginUser sysUser = SecureUtil.currentUser();
|
||||
// Step.2 计算分页sheet数据
|
||||
double total = service.count();
|
||||
int count = (int)Math.ceil(total/pageNum);
|
||||
|
||||
@ -9,10 +9,10 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
/**
|
||||
* @Description: Entity基类
|
||||
@ -30,20 +30,20 @@ public class JeecgEntity implements Serializable {
|
||||
* ID
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@ApiModelProperty(value = "ID")
|
||||
@Schema(description = "ID")
|
||||
private java.lang.String id;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@ApiModelProperty(value = "创建人")
|
||||
@Schema(description = "创建人")
|
||||
@Excel(name = "创建人", width = 15)
|
||||
private java.lang.String createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@ApiModelProperty(value = "创建时间")
|
||||
@Schema(description = "创建时间")
|
||||
@Excel(name = "创建时间", width = 20, format = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@ -52,14 +52,14 @@ public class JeecgEntity implements Serializable {
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
@ApiModelProperty(value = "更新人")
|
||||
@Schema(description = "更新人")
|
||||
@Excel(name = "更新人", width = 15)
|
||||
private java.lang.String updateBy;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@ApiModelProperty(value = "更新时间")
|
||||
@Schema(description = "更新时间")
|
||||
@Excel(name = "更新时间", width = 20, format = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
|
||||
@ -5,7 +5,7 @@ import org.jeecg.common.system.vo.SysUserCacheInfo;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package org.jeecg.common.system.util;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.JWTVerifier;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
@ -10,14 +12,17 @@ import com.google.common.base.Joiner;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Date;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.jeecg.common.api.CommonAPI;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.constant.DataBaseConstant;
|
||||
@ -29,6 +34,22 @@ import org.jeecg.common.system.vo.SysUserCacheInfo;
|
||||
import org.jeecg.common.util.DateUtils;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.security.self.SelfAuthenticationProvider;
|
||||
import org.jeecg.config.security.self.SelfAuthenticationToken;
|
||||
import org.jeecg.config.security.utils.SecureUtil;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.core.*;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
|
||||
/**
|
||||
* @Author Scott
|
||||
@ -42,6 +63,8 @@ public class JwtUtil {
|
||||
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000;
|
||||
static final String WELL_NUMBER = SymbolConstant.WELL_NUMBER + SymbolConstant.LEFT_CURLY_BRACKET;
|
||||
|
||||
public static final String DEFAULT_CLIENT = "jeecg-client";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param response
|
||||
@ -77,10 +100,9 @@ public class JwtUtil {
|
||||
public static boolean verify(String token, String username, String secret) {
|
||||
try {
|
||||
// 根据密码生成JWT效验器
|
||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
||||
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
|
||||
JwtDecoder jwtDecoder = SpringContextUtils.getBean(JwtDecoder.class);
|
||||
// 效验TOKEN
|
||||
DecodedJWT jwt = verifier.verify(token);
|
||||
jwtDecoder.decode(token);
|
||||
return true;
|
||||
} catch (Exception exception) {
|
||||
return false;
|
||||
@ -95,24 +117,33 @@ public class JwtUtil {
|
||||
public static String getUsername(String token) {
|
||||
try {
|
||||
DecodedJWT jwt = JWT.decode(token);
|
||||
return jwt.getClaim("username").asString();
|
||||
LoginUser loginUser = JSONObject.parseObject(jwt.getClaim("sub").asString(), LoginUser.class);
|
||||
return loginUser.getUsername();
|
||||
} catch (JWTDecodeException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名,5min后过期
|
||||
* 生成token
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param secret 用户的密码
|
||||
* @return 加密的token
|
||||
*/
|
||||
public static String sign(String username, String secret) {
|
||||
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
|
||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
||||
// 附带username信息
|
||||
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
|
||||
Map<String, Object> additionalParameter = new HashMap<>();
|
||||
additionalParameter.put("username", username);
|
||||
|
||||
RegisteredClientRepository registeredClientRepository = SpringContextUtils.getBean(RegisteredClientRepository.class);
|
||||
SelfAuthenticationProvider selfAuthenticationProvider = SpringContextUtils.getBean(SelfAuthenticationProvider.class);
|
||||
|
||||
OAuth2ClientAuthenticationToken client = new OAuth2ClientAuthenticationToken(Objects.requireNonNull(registeredClientRepository.findByClientId("jeecg-client")), ClientAuthenticationMethod.CLIENT_SECRET_BASIC, null);
|
||||
client.setAuthenticated(true);
|
||||
SelfAuthenticationToken selfAuthenticationToken = new SelfAuthenticationToken(client, additionalParameter);
|
||||
selfAuthenticationToken.setAuthenticated(true);
|
||||
OAuth2AccessTokenAuthenticationToken accessToken = (OAuth2AccessTokenAuthenticationToken) selfAuthenticationProvider.authenticate(selfAuthenticationToken);
|
||||
return accessToken.getAccessToken().getTokenValue();
|
||||
|
||||
}
|
||||
|
||||
@ -177,7 +208,7 @@ public class JwtUtil {
|
||||
//2.通过shiro获取登录用户信息
|
||||
LoginUser sysUser = null;
|
||||
try {
|
||||
sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
sysUser = SecureUtil.currentUser();
|
||||
} catch (Exception e) {
|
||||
log.warn("SecurityUtils.getSubject() 获取用户信息异常:" + e.getMessage());
|
||||
}
|
||||
|
||||
@ -1,13 +1,18 @@
|
||||
package org.jeecg.common.system.vo;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecg.common.desensitization.annotation.SensitiveField;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@ -20,8 +25,10 @@ import java.util.Date;
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
public class LoginUser {
|
||||
public class LoginUser implements Serializable {
|
||||
|
||||
|
||||
private static final long serialVersionUID = -7143159031677245866L;
|
||||
/**
|
||||
* 登录人id
|
||||
*/
|
||||
@ -127,4 +134,29 @@ public class LoginUser {
|
||||
/**设备id uniapp推送用*/
|
||||
private String clientId;
|
||||
|
||||
@SensitiveField
|
||||
private String salt;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// 重新构建对象过滤一些敏感字段
|
||||
LoginUser loginUser = new LoginUser();
|
||||
loginUser.setId(id);
|
||||
loginUser.setUsername(username);
|
||||
loginUser.setRealname(realname);
|
||||
loginUser.setOrgCode(orgCode);
|
||||
loginUser.setSex(sex);
|
||||
loginUser.setEmail(email);
|
||||
loginUser.setPhone(phone);
|
||||
loginUser.setDelFlag(delFlag);
|
||||
loginUser.setStatus(status);
|
||||
loginUser.setActivitiSync(activitiSync);
|
||||
loginUser.setUserIdentity(userIdentity);
|
||||
loginUser.setDepartIds(departIds);
|
||||
loginUser.setPost(post);
|
||||
loginUser.setTelephone(telephone);
|
||||
loginUser.setRelTenantIds(relTenantIds);
|
||||
loginUser.setClientId(clientId);
|
||||
return JSON.toJSONString(loginUser);
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,8 +19,6 @@ public class SysFilesModel {
|
||||
private String storeType;
|
||||
/**文件大小(kb)*/
|
||||
private Double fileSize;
|
||||
/**租户id*/
|
||||
private String tenantId;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
@ -69,12 +67,4 @@ public class SysFilesModel {
|
||||
public void setFileSize(Double fileSize) {
|
||||
this.fileSize = fileSize;
|
||||
}
|
||||
|
||||
public String getTenantId() {
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
public void setTenantId(String tenantId) {
|
||||
this.tenantId = tenantId;
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
*
|
||||
|
||||
@ -19,7 +19,7 @@ import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import javax.sql.DataSource;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
@ -302,7 +302,7 @@ public class CommonUtils {
|
||||
DB_TYPE = DataBaseConstant.DB_TYPE_ORACLE;
|
||||
}else if(dbType.indexOf(DataBaseConstant.DB_TYPE_SQLSERVER)>=0||dbType.indexOf(sqlserver)>=0) {
|
||||
DB_TYPE = DataBaseConstant.DB_TYPE_SQLSERVER;
|
||||
}else if(dbType.indexOf(DataBaseConstant.DB_TYPE_POSTGRESQL)>=0 || dbType.indexOf(DataBaseConstant.DB_TYPE_KINGBASEES)>=0) {
|
||||
}else if(dbType.indexOf(DataBaseConstant.DB_TYPE_POSTGRESQL)>=0) {
|
||||
DB_TYPE = DataBaseConstant.DB_TYPE_POSTGRESQL;
|
||||
}else if(dbType.indexOf(DataBaseConstant.DB_TYPE_MARIADB)>=0) {
|
||||
DB_TYPE = DataBaseConstant.DB_TYPE_MARIADB;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package org.jeecg.common.util;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package org.jeecg.common.util;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.constant.ServiceNameConstants;
|
||||
|
||||
@ -29,17 +29,6 @@ public class SqlInjectionUtil {
|
||||
* 字典专用—sql注入关键词
|
||||
*/
|
||||
private static String specialDictSqlXssStr = "exec |peformance_schema|information_schema|extractvalue|updatexml|geohash|gtid_subset|gtid_subtract|insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |;|+|--";
|
||||
/**
|
||||
* 完整匹配的key,不需要考虑前空格
|
||||
*/
|
||||
private static List<String> FULL_MATCHING_KEYWRODS = new ArrayList<>();
|
||||
static {
|
||||
FULL_MATCHING_KEYWRODS.add(";");
|
||||
FULL_MATCHING_KEYWRODS.add("+");
|
||||
FULL_MATCHING_KEYWRODS.add("--");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* sql注入风险的 正则关键字
|
||||
*
|
||||
@ -61,8 +50,6 @@ public class SqlInjectionUtil {
|
||||
* sql注释的正则
|
||||
*/
|
||||
private final static Pattern SQL_ANNOTATION = Pattern.compile("/\\*[\\s\\S]*\\*/");
|
||||
private final static String SQL_ANNOTATION2 = "--";
|
||||
|
||||
/**
|
||||
* sql注入提示语
|
||||
*/
|
||||
@ -141,13 +128,7 @@ public class SqlInjectionUtil {
|
||||
if (sql.startsWith(keyword.trim())) {
|
||||
return true;
|
||||
} else if (sql.contains(keyword)) {
|
||||
// 需要匹配的,sql注入关键词
|
||||
String matchingText = " " + keyword;
|
||||
if(FULL_MATCHING_KEYWRODS.contains(keyword)){
|
||||
matchingText = keyword;
|
||||
}
|
||||
|
||||
if (sql.contains(matchingText)) {
|
||||
if (sql.contains(" " + keyword)) {
|
||||
return true;
|
||||
} else {
|
||||
String regularStr = "\\s+\\S+" + keyword;
|
||||
@ -263,13 +244,6 @@ public class SqlInjectionUtil {
|
||||
* @return
|
||||
*/
|
||||
public static void checkSqlAnnotation(String str){
|
||||
if(str.contains(SQL_ANNOTATION2)){
|
||||
String error = "请注意,SQL中不允许含注释,有安全风险!";
|
||||
log.error(error);
|
||||
throw new RuntimeException(error);
|
||||
}
|
||||
|
||||
|
||||
Matcher matcher = SQL_ANNOTATION.matcher(str);
|
||||
if(matcher.find()){
|
||||
String error = "请注意,值可能存在SQL注入风险---> \\*.*\\";
|
||||
@ -286,7 +260,7 @@ public class SqlInjectionUtil {
|
||||
*
|
||||
* @param table
|
||||
*/
|
||||
private static Pattern tableNamePattern = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_\\$]{0,63}$");
|
||||
private static Pattern tableNamePattern = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_]{0,63}$");
|
||||
public static String getSqlInjectTableName(String table) {
|
||||
if(oConvertUtils.isEmpty(table)){
|
||||
return table;
|
||||
|
||||
@ -11,7 +11,13 @@ import org.jeecg.common.exception.JeecgBoot401Exception;
|
||||
import org.jeecg.common.system.util.JwtUtil;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jeecg.config.security.JeecgRedisOAuth2AuthorizationService;
|
||||
import org.springframework.data.redis.serializer.SerializationException;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @Author scott
|
||||
@ -112,7 +118,7 @@ public class TokenUtils {
|
||||
throw new JeecgBoot401Exception("账号已被锁定,请联系管理员!");
|
||||
}
|
||||
// 校验token是否超时失效 & 或者账号密码是否错误
|
||||
if (!jwtTokenRefresh(token, username, user.getPassword(), redisUtil)) {
|
||||
if (!jwtTokenRefresh(token, username, user.getPassword())) {
|
||||
throw new JeecgBoot401Exception(CommonConstant.TOKEN_IS_INVALID_MSG);
|
||||
}
|
||||
return true;
|
||||
@ -141,6 +147,15 @@ public class TokenUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean jwtTokenRefresh(String token, String userName, String passWord) {
|
||||
JeecgRedisOAuth2AuthorizationService authRedis = SpringContextUtils.getBean(JeecgRedisOAuth2AuthorizationService.class);
|
||||
OAuth2Authorization authorization = authRedis.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
|
||||
if (Objects.nonNull(authorization) && JwtUtil.verify(token, userName, passWord)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录用户
|
||||
*
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
package org.jeecg.common.util.encryption;
|
||||
|
||||
import org.apache.shiro.codec.Base64;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* @Description: AES 加密
|
||||
@ -49,7 +48,7 @@ public class AesEncryptUtil {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
|
||||
byte[] encrypted = cipher.doFinal(plaintext);
|
||||
|
||||
return Base64.encodeToString(encrypted);
|
||||
return Base64.getEncoder().encodeToString(encrypted);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
@ -67,7 +66,7 @@ public class AesEncryptUtil {
|
||||
*/
|
||||
public static String desEncrypt(String data, String key, String iv) throws Exception {
|
||||
//update-begin-author:taoyan date:2022-5-23 for:VUEN-1084 【vue3】online表单测试发现的新问题 6、解密报错 ---解码失败应该把异常抛出去,在外面处理
|
||||
byte[] encrypted1 = Base64.decode(data);
|
||||
byte[] encrypted1 = Base64.getDecoder().decode(data);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
|
||||
|
||||
@ -61,10 +61,6 @@ public class SsrfFileTypeFilter {
|
||||
FILE_TYPE_WHITE_LIST.add("7z");
|
||||
FILE_TYPE_WHITE_LIST.add("tar");
|
||||
|
||||
//app文件后缀
|
||||
FILE_TYPE_WHITE_LIST.add("apk");
|
||||
FILE_TYPE_WHITE_LIST.add("wgt");
|
||||
|
||||
//设置禁止文件的头部标记
|
||||
FILE_TYPE_MAP.put("3c25402070616765206c", "jsp");
|
||||
FILE_TYPE_MAP.put("3c3f7068700a0a2f2a2a0a202a205048", "php");
|
||||
|
||||
@ -7,7 +7,7 @@ import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.constant.SymbolConstant;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
@ -413,7 +413,7 @@ public class oConvertUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] childs = childArray.toArray(new String[]{});
|
||||
String[] childs = (String[]) childArray.toArray();
|
||||
for (String v : childs) {
|
||||
if (!isIn(v, all)) {
|
||||
return false;
|
||||
|
||||
@ -3,7 +3,7 @@ package org.jeecg.config;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import org.jeecg.common.api.CommonAPI;
|
||||
import org.jeecg.common.system.vo.DictModel;
|
||||
|
||||
@ -2,7 +2,9 @@ package org.jeecg.config;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.*;
|
||||
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure;
|
||||
import com.alibaba.druid.spring.boot3.autoconfigure.properties.DruidStatProperties;
|
||||
import jakarta.servlet.*;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
@ -11,8 +13,6 @@ import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
|
||||
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
|
||||
import com.alibaba.druid.util.Utils;
|
||||
|
||||
/**
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
package org.jeecg.config;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.SpringApplicationRunListener;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.core.env.MutablePropertySources;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author eightmonth@qq.com
|
||||
* @date 2024/4/8 11:37
|
||||
*/
|
||||
public class DruidWallConfigRegister implements SpringApplicationRunListener {
|
||||
|
||||
public SpringApplication application;
|
||||
|
||||
private String[] args;
|
||||
|
||||
|
||||
/**
|
||||
* 必备,否则启动报错
|
||||
* @param application
|
||||
* @param args
|
||||
*/
|
||||
public DruidWallConfigRegister(SpringApplication application, String[] args) {
|
||||
this.application = application;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void contextLoaded(ConfigurableApplicationContext context) {
|
||||
ConfigurableEnvironment env = context.getEnvironment();
|
||||
Map<String, Object> props = new HashMap<>();
|
||||
props.put("spring.datasource.dynamic.druid.wall.selectWhereAlwayTrueCheck", false);
|
||||
|
||||
MutablePropertySources propertySources = env.getPropertySources();
|
||||
|
||||
PropertySource<Map<String, Object>> propertySource = new MapPropertySource("jeecg-datasource-config", props);
|
||||
|
||||
propertySources.addLast(propertySource);
|
||||
}
|
||||
}
|
||||
@ -32,10 +32,6 @@ public class JeecgBaseConfig {
|
||||
*/
|
||||
private Firewall firewall;
|
||||
|
||||
/**
|
||||
* shiro拦截排除
|
||||
*/
|
||||
private Shiro shiro;
|
||||
/**
|
||||
* 上传文件配置
|
||||
*/
|
||||
@ -88,14 +84,6 @@ public class JeecgBaseConfig {
|
||||
this.signatureSecret = signatureSecret;
|
||||
}
|
||||
|
||||
public Shiro getShiro() {
|
||||
return shiro;
|
||||
}
|
||||
|
||||
public void setShiro(Shiro shiro) {
|
||||
this.shiro = shiro;
|
||||
}
|
||||
|
||||
public Path getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
@ -1,183 +1,183 @@
|
||||
package org.jeecg.config;
|
||||
|
||||
|
||||
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
|
||||
import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
|
||||
import springfox.documentation.builders.ApiInfoBuilder;
|
||||
import springfox.documentation.builders.ParameterBuilder;
|
||||
import springfox.documentation.builders.PathSelectors;
|
||||
import springfox.documentation.builders.RequestHandlerSelectors;
|
||||
import springfox.documentation.oas.annotations.EnableOpenApi;
|
||||
import springfox.documentation.schema.ModelRef;
|
||||
import springfox.documentation.service.*;
|
||||
import springfox.documentation.spi.DocumentationType;
|
||||
import springfox.documentation.spi.service.contexts.SecurityContext;
|
||||
import springfox.documentation.spring.web.plugins.Docket;
|
||||
import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;
|
||||
import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
|
||||
import springfox.documentation.swagger2.annotations.EnableSwagger2;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @Author scott
|
||||
*/
|
||||
@Configuration
|
||||
@EnableSwagger2 //开启 Swagger2
|
||||
@EnableKnife4j //开启 knife4j,可以不写
|
||||
@Import(BeanValidatorPluginsConfiguration.class)
|
||||
public class Swagger2Config implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
*
|
||||
* 显示swagger-ui.html文档展示页,还必须注入swagger资源:
|
||||
*
|
||||
* @param registry
|
||||
*/
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
|
||||
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
|
||||
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
|
||||
}
|
||||
|
||||
/**
|
||||
* swagger2的配置文件,这里可以配置swagger2的一些基本的内容,比如扫描的包等等
|
||||
*
|
||||
* @return Docket
|
||||
*/
|
||||
@Bean(value = "defaultApi2")
|
||||
public Docket defaultApi2() {
|
||||
return new Docket(DocumentationType.SWAGGER_2)
|
||||
.apiInfo(apiInfo())
|
||||
.select()
|
||||
//此包路径下的类,才生成接口文档
|
||||
.apis(RequestHandlerSelectors.basePackage("org.jeecg"))
|
||||
//加了ApiOperation注解的类,才生成接口文档
|
||||
.apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
|
||||
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
|
||||
.paths(PathSelectors.any())
|
||||
.build()
|
||||
.securitySchemes(Collections.singletonList(securityScheme()))
|
||||
.securityContexts(securityContexts())
|
||||
.globalOperationParameters(setHeaderToken());
|
||||
}
|
||||
|
||||
/***
|
||||
* oauth2配置
|
||||
* 需要增加swagger授权回调地址
|
||||
* http://localhost:8888/webjars/springfox-swagger-ui/o2c.html
|
||||
* @return
|
||||
*/
|
||||
@Bean
|
||||
SecurityScheme securityScheme() {
|
||||
return new ApiKey(CommonConstant.X_ACCESS_TOKEN, CommonConstant.X_ACCESS_TOKEN, "header");
|
||||
}
|
||||
/**
|
||||
* JWT token
|
||||
* @return
|
||||
*/
|
||||
private List<Parameter> setHeaderToken() {
|
||||
ParameterBuilder tokenPar = new ParameterBuilder();
|
||||
List<Parameter> pars = new ArrayList<>();
|
||||
tokenPar.name(CommonConstant.X_ACCESS_TOKEN).description("token").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
|
||||
pars.add(tokenPar.build());
|
||||
return pars;
|
||||
}
|
||||
|
||||
/**
|
||||
* api文档的详细信息函数,注意这里的注解引用的是哪个
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private ApiInfo apiInfo() {
|
||||
return new ApiInfoBuilder()
|
||||
// //大标题
|
||||
.title("JeecgBoot 后台服务API接口文档")
|
||||
// 版本号
|
||||
.version("1.0")
|
||||
// .termsOfServiceUrl("NO terms of service")
|
||||
// 描述
|
||||
.description("后台API接口")
|
||||
// 作者
|
||||
.contact(new Contact("北京国炬信息技术有限公司","www.jeccg.com","jeecgos@163.com"))
|
||||
.license("The Apache License, Version 2.0")
|
||||
.licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增 securityContexts 保持登录状态
|
||||
*/
|
||||
private List<SecurityContext> securityContexts() {
|
||||
return new ArrayList(
|
||||
Collections.singleton(SecurityContext.builder()
|
||||
.securityReferences(defaultAuth())
|
||||
.forPaths(PathSelectors.regex("^(?!auth).*$"))
|
||||
.build())
|
||||
);
|
||||
}
|
||||
|
||||
private List<SecurityReference> defaultAuth() {
|
||||
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
|
||||
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
|
||||
authorizationScopes[0] = authorizationScope;
|
||||
return new ArrayList(
|
||||
Collections.singleton(new SecurityReference(CommonConstant.X_ACCESS_TOKEN, authorizationScopes)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解决springboot2.6 和springfox不兼容问题
|
||||
* @return
|
||||
*/
|
||||
@Bean
|
||||
public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {
|
||||
return new BeanPostProcessor() {
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
|
||||
customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) {
|
||||
List<T> copy = mappings.stream()
|
||||
.filter(mapping -> mapping.getPatternParser() == null)
|
||||
.collect(Collectors.toList());
|
||||
mappings.clear();
|
||||
mappings.addAll(copy);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {
|
||||
try {
|
||||
Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
|
||||
field.setAccessible(true);
|
||||
return (List<RequestMappingInfoHandlerMapping>) field.get(bean);
|
||||
} catch (IllegalArgumentException | IllegalAccessException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
//package org.jeecg.config;
|
||||
//
|
||||
// 已使用swagger3config平替
|
||||
//import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
|
||||
//import io.swagger.annotations.ApiOperation;
|
||||
//import org.jeecg.common.constant.CommonConstant;
|
||||
//import org.springframework.beans.BeansException;
|
||||
//import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
//import org.springframework.context.annotation.Bean;
|
||||
//import org.springframework.context.annotation.Configuration;
|
||||
//import org.springframework.context.annotation.Import;
|
||||
//import org.springframework.util.ReflectionUtils;
|
||||
//import org.springframework.web.bind.annotation.RestController;
|
||||
//import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
//import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
//import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
|
||||
//import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
|
||||
//import springfox.documentation.builders.ApiInfoBuilder;
|
||||
//import springfox.documentation.builders.ParameterBuilder;
|
||||
//import springfox.documentation.builders.PathSelectors;
|
||||
//import springfox.documentation.builders.RequestHandlerSelectors;
|
||||
//import springfox.documentation.oas.annotations.EnableOpenApi;
|
||||
//import springfox.documentation.schema.ModelRef;
|
||||
//import springfox.documentation.service.*;
|
||||
//import springfox.documentation.spi.DocumentationType;
|
||||
//import springfox.documentation.spi.service.contexts.SecurityContext;
|
||||
//import springfox.documentation.spring.web.plugins.Docket;
|
||||
//import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;
|
||||
//import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
|
||||
//import springfox.documentation.swagger2.annotations.EnableSwagger2;
|
||||
//
|
||||
//import java.lang.reflect.Field;
|
||||
//import java.util.ArrayList;
|
||||
//import java.util.Collections;
|
||||
//import java.util.List;
|
||||
//import java.util.stream.Collectors;
|
||||
//
|
||||
///**
|
||||
// * @Author scott
|
||||
// */
|
||||
//@Configuration
|
||||
//@EnableSwagger2 //开启 Swagger2
|
||||
//@EnableKnife4j //开启 knife4j,可以不写
|
||||
//@Import(BeanValidatorPluginsConfiguration.class)
|
||||
//public class Swagger2Config implements WebMvcConfigurer {
|
||||
//
|
||||
// /**
|
||||
// *
|
||||
// * 显示swagger-ui.html文档展示页,还必须注入swagger资源:
|
||||
// *
|
||||
// * @param registry
|
||||
// */
|
||||
// @Override
|
||||
// public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
|
||||
// registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
|
||||
// registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * swagger2的配置文件,这里可以配置swagger2的一些基本的内容,比如扫描的包等等
|
||||
// *
|
||||
// * @return Docket
|
||||
// */
|
||||
// @Bean(value = "defaultApi2")
|
||||
// public Docket defaultApi2() {
|
||||
// return new Docket(DocumentationType.SWAGGER_2)
|
||||
// .apiInfo(apiInfo())
|
||||
// .select()
|
||||
// //此包路径下的类,才生成接口文档
|
||||
// .apis(RequestHandlerSelectors.basePackage("org.jeecg"))
|
||||
// //加了ApiOperation注解的类,才生成接口文档
|
||||
// .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
|
||||
// .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
|
||||
// .paths(PathSelectors.any())
|
||||
// .build()
|
||||
// .securitySchemes(Collections.singletonList(securityScheme()))
|
||||
// .securityContexts(securityContexts())
|
||||
// .globalOperationParameters(setHeaderToken());
|
||||
// }
|
||||
//
|
||||
// /***
|
||||
// * oauth2配置
|
||||
// * 需要增加swagger授权回调地址
|
||||
// * http://localhost:8888/webjars/springfox-swagger-ui/o2c.html
|
||||
// * @return
|
||||
// */
|
||||
// @Bean
|
||||
// SecurityScheme securityScheme() {
|
||||
// return new ApiKey(CommonConstant.X_ACCESS_TOKEN, CommonConstant.X_ACCESS_TOKEN, "header");
|
||||
// }
|
||||
// /**
|
||||
// * JWT token
|
||||
// * @return
|
||||
// */
|
||||
// private List<Parameter> setHeaderToken() {
|
||||
// ParameterBuilder tokenPar = new ParameterBuilder();
|
||||
// List<Parameter> pars = new ArrayList<>();
|
||||
// tokenPar.name(CommonConstant.X_ACCESS_TOKEN).description("token").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
|
||||
// pars.add(tokenPar.build());
|
||||
// return pars;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * api文档的详细信息函数,注意这里的注解引用的是哪个
|
||||
// *
|
||||
// * @return
|
||||
// */
|
||||
// private ApiInfo apiInfo() {
|
||||
// return new ApiInfoBuilder()
|
||||
// // //大标题
|
||||
// .title("JeecgBoot 后台服务API接口文档")
|
||||
// // 版本号
|
||||
// .version("1.0")
|
||||
//// .termsOfServiceUrl("NO terms of service")
|
||||
// // 描述
|
||||
// .description("后台API接口")
|
||||
// // 作者
|
||||
// .contact(new Contact("北京国炬信息技术有限公司","www.jeccg.com","jeecgos@163.com"))
|
||||
// .license("The Apache License, Version 2.0")
|
||||
// .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
|
||||
// .build();
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 新增 securityContexts 保持登录状态
|
||||
// */
|
||||
// private List<SecurityContext> securityContexts() {
|
||||
// return new ArrayList(
|
||||
// Collections.singleton(SecurityContext.builder()
|
||||
// .securityReferences(defaultAuth())
|
||||
// .forPaths(PathSelectors.regex("^(?!auth).*$"))
|
||||
// .build())
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// private List<SecurityReference> defaultAuth() {
|
||||
// AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
|
||||
// AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
|
||||
// authorizationScopes[0] = authorizationScope;
|
||||
// return new ArrayList(
|
||||
// Collections.singleton(new SecurityReference(CommonConstant.X_ACCESS_TOKEN, authorizationScopes)));
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 解决springboot2.6 和springfox不兼容问题
|
||||
// * @return
|
||||
// */
|
||||
// @Bean
|
||||
// public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {
|
||||
// return new BeanPostProcessor() {
|
||||
//
|
||||
// @Override
|
||||
// public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
// if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
|
||||
// customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
|
||||
// }
|
||||
// return bean;
|
||||
// }
|
||||
//
|
||||
// private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) {
|
||||
// List<T> copy = mappings.stream()
|
||||
// .filter(mapping -> mapping.getPatternParser() == null)
|
||||
// .collect(Collectors.toList());
|
||||
// mappings.clear();
|
||||
// mappings.addAll(copy);
|
||||
// }
|
||||
//
|
||||
// @SuppressWarnings("unchecked")
|
||||
// private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {
|
||||
// try {
|
||||
// Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
|
||||
// field.setAccessible(true);
|
||||
// return (List<RequestMappingInfoHandlerMapping>) field.get(bean);
|
||||
// } catch (IllegalArgumentException | IllegalAccessException e) {
|
||||
// throw new IllegalStateException(e);
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
//
|
||||
//
|
||||
//}
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
package org.jeecg.config;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.Paths;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class Swagger3Config implements WebMvcConfigurer {
|
||||
/**
|
||||
*
|
||||
* 显示swagger-ui.html文档展示页,还必须注入swagger资源:
|
||||
*
|
||||
* @param registry
|
||||
*/
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
|
||||
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
|
||||
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi swaggerOpenApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("default")
|
||||
.packagesToScan("org.jeecg")
|
||||
// 剔除以下几个包路径的接口生成文档
|
||||
.packagesToExclude("org.jeecg.modules.drag", "org.jeecg.modules.online", "org.jeecg.modules.jmreport")
|
||||
// 加了Operation注解的方法,才生成接口文档
|
||||
.addOpenApiMethodFilter(method -> method.isAnnotationPresent(Operation.class))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("JeecgBoot 后台服务API接口文档")
|
||||
.version("1.0")
|
||||
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
|
||||
.description( "后台API接口")
|
||||
.termsOfService("NO terms of service")
|
||||
.license(new License().name("Apache 2.0").url("http://www.apache.org/licenses/LICENSE-2.0.html"))
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package org.jeecg.config;
|
||||
|
||||
import io.undertow.server.DefaultByteBufferPool;
|
||||
import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
|
||||
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class UndertowCustomizer implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> {
|
||||
@Override
|
||||
public void customize(UndertowServletWebServerFactory factory) {
|
||||
factory.addDeploymentInfoCustomizers(deploymentInfo -> {
|
||||
WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo();
|
||||
webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(false, 1024));
|
||||
deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -10,12 +10,15 @@ import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.prometheus.PrometheusMeterRegistry;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
|
||||
import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@ -30,7 +33,6 @@ import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
|
||||
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
@ -133,8 +135,11 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
|
||||
* https://blog.csdn.net/u013810234/article/details/110097201
|
||||
*/
|
||||
@Bean
|
||||
public InMemoryHttpTraceRepository getInMemoryHttpTrace(){
|
||||
return new InMemoryHttpTraceRepository();
|
||||
public InMemoryHttpExchangeRepository getInMemoryHttpTrace(){
|
||||
InMemoryHttpExchangeRepository repository = new InMemoryHttpExchangeRepository();
|
||||
// 默认保存1000条http请求记录
|
||||
repository.setCapacity(1000);
|
||||
return repository;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ public class WebSocketConfig {
|
||||
FilterRegistrationBean bean = new FilterRegistrationBean();
|
||||
bean.setFilter(websocketFilter());
|
||||
//TODO 临时注释掉,测试下线上socket总断的问题
|
||||
bean.addUrlPatterns("/taskCountSocket/*", "/websocket/*","/eoaSocket/*","/eoaNewChatSocket/*", "/newsWebsocket/*", "/vxeSocket/*");
|
||||
bean.addUrlPatterns("/websocket/*","/eoaSocket/*","/eoaNewChatSocket/*", "/newsWebsocket/*", "/vxeSocket/*");
|
||||
return bean;
|
||||
}
|
||||
|
||||
|
||||
@ -3,8 +3,8 @@ package org.jeecg.config.filter;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.config.sign.util.BodyReaderHttpServletRequestWrapper;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
|
||||
@ -7,9 +7,9 @@ import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
|
||||
@ -2,24 +2,21 @@ package org.jeecg.config.firewall.interceptor;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.jeecg.common.api.CommonAPI;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.system.util.JwtUtil;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.CommonUtils;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.config.firewall.interceptor.enums.LowCodeUrlsEnum;
|
||||
import org.jeecg.config.security.utils.SecureUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Set;
|
||||
@ -66,7 +63,7 @@ public class LowCodeModeInterceptor implements HandlerInterceptor {
|
||||
if (jeecgBaseConfig.getFirewall()!=null && LowCodeModeInterceptor.LOW_CODE_MODE_PROD.equals(jeecgBaseConfig.getFirewall().getLowCodeMode())) {
|
||||
String requestURI = request.getRequestURI().substring(request.getContextPath().length());
|
||||
log.info("低代码模式,拦截请求路径:" + requestURI);
|
||||
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
LoginUser loginUser = SecureUtil.currentUser();
|
||||
Set<String> hasRoles = null;
|
||||
if (loginUser == null) {
|
||||
loginUser = commonAPI.getUserByName(JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest()));
|
||||
|
||||
@ -6,11 +6,11 @@ import org.apache.ibatis.executor.Executor;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.mapping.SqlCommandType;
|
||||
import org.apache.ibatis.plugin.*;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.jeecg.common.config.TenantContext;
|
||||
import org.jeecg.common.constant.TenantConstant;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.security.utils.SecureUtil;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
@ -173,7 +173,7 @@ public class MybatisInterceptor implements Interceptor {
|
||||
private LoginUser getLoginUser() {
|
||||
LoginUser sysUser = null;
|
||||
try {
|
||||
sysUser = SecurityUtils.getSubject().getPrincipal() != null ? (LoginUser) SecurityUtils.getSubject().getPrincipal() : null;
|
||||
sysUser = SecureUtil.currentUser() != null ? SecureUtil.currentUser() : null;
|
||||
} catch (Exception e) {
|
||||
//e.printStackTrace();
|
||||
sysUser = null;
|
||||
|
||||
@ -11,7 +11,7 @@ import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.config.mybatis.ThreadLocalDataHelper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
|
||||
@ -6,8 +6,8 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 动态数据源切换拦截器
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
package org.jeecg.config.security;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* spring authorization server 注册客户端便捷工具类
|
||||
* @author eightmonth@qq.com
|
||||
* @date 2024/3/7 11:22
|
||||
*/
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class ClientService {
|
||||
|
||||
private RegisteredClientRepository registeredClientRepository;
|
||||
|
||||
/**
|
||||
* 修改客户端token有效期
|
||||
* 认证码、设备码有效期与accessToken有效期保持一致
|
||||
*/
|
||||
public void updateTokenValidation(String clientId, Long accessTokenValidation, Long refreshTokenValidation){
|
||||
RegisteredClient registeredClient = findByClientId(clientId);
|
||||
RegisteredClient.Builder builder = RegisteredClient.from(registeredClient);
|
||||
TokenSettings tokenSettings = TokenSettings.builder()
|
||||
.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
|
||||
.accessTokenTimeToLive(Duration.ofSeconds(accessTokenValidation))
|
||||
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
|
||||
.reuseRefreshTokens(true)
|
||||
.refreshTokenTimeToLive(Duration.ofSeconds(refreshTokenValidation))
|
||||
.authorizationCodeTimeToLive(Duration.ofSeconds(accessTokenValidation))
|
||||
.deviceCodeTimeToLive(Duration.ofSeconds(accessTokenValidation))
|
||||
.build();
|
||||
builder.tokenSettings(tokenSettings);
|
||||
registeredClientRepository.save(builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改客户端授权类型
|
||||
* @param clientId
|
||||
* @param grantTypes
|
||||
*/
|
||||
public void updateGrantType(String clientId, Set<AuthorizationGrantType> grantTypes) {
|
||||
RegisteredClient registeredClient = findByClientId(clientId);
|
||||
RegisteredClient.Builder builder = RegisteredClient.from(registeredClient);
|
||||
for (AuthorizationGrantType grantType : grantTypes) {
|
||||
builder.authorizationGrantType(grantType);
|
||||
}
|
||||
registeredClientRepository.save(builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改客户端重定向uri
|
||||
* @param clientId
|
||||
* @param redirectUris
|
||||
*/
|
||||
public void updateRedirectUris(String clientId, String redirectUris) {
|
||||
RegisteredClient registeredClient = findByClientId(clientId);
|
||||
RegisteredClient.Builder builder = RegisteredClient.from(registeredClient);
|
||||
builder.redirectUri(redirectUris);
|
||||
registeredClientRepository.save(builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改客户端授权范围
|
||||
* @param clientId
|
||||
* @param scopes
|
||||
*/
|
||||
public void updateScopes(String clientId, Set<String> scopes) {
|
||||
RegisteredClient registeredClient = findByClientId(clientId);
|
||||
RegisteredClient.Builder builder = RegisteredClient.from(registeredClient);
|
||||
for (String scope : scopes) {
|
||||
builder.scope(scope);
|
||||
}
|
||||
registeredClientRepository.save(builder.build());
|
||||
}
|
||||
|
||||
public RegisteredClient findByClientId(String clientId) {
|
||||
return registeredClientRepository.findByClientId(clientId);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
package org.jeecg.config.security;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.CommonAPI;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.RedisUtil;
|
||||
import org.jeecg.config.security.utils.SecureUtil;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.PatternMatchUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* spring authorization server自定义权限处理,根据@PreAuthorize注解,判断当前用户是否具备权限
|
||||
* @author EightMonth
|
||||
* @date 2024/1/10 17:00
|
||||
*/
|
||||
@Service("jps")
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public class JeecgPermissionService {
|
||||
private final String SPLIT = "::";
|
||||
private final String PERM_PREFIX = "jps" + SPLIT;
|
||||
|
||||
private final CommonAPI commonAPI;
|
||||
private final RedisUtil redisUtil;
|
||||
|
||||
/**
|
||||
* 判断接口是否有任意xxx,xxx权限
|
||||
* @param permissions 权限
|
||||
* @return {boolean}
|
||||
*/
|
||||
public boolean requiresPermissions(String... permissions) {
|
||||
if (ArrayUtil.isEmpty(permissions)) {
|
||||
return false;
|
||||
}
|
||||
LoginUser loginUser = SecureUtil.currentUser();
|
||||
|
||||
Object cache = redisUtil.get(buildKey("permission", loginUser.getUsername()));
|
||||
Set<String> permissionList;
|
||||
if (Objects.nonNull(cache)) {
|
||||
permissionList = (Set<String>) cache;
|
||||
} else {
|
||||
permissionList = commonAPI.queryUserAuths(loginUser.getUsername());
|
||||
redisUtil.set(buildKey("permission", loginUser.getUsername()), permissionList);
|
||||
}
|
||||
|
||||
boolean pass = permissionList.stream().filter(StringUtils::hasText)
|
||||
.anyMatch(x -> PatternMatchUtils.simpleMatch(permissions, x));
|
||||
if (!pass) {
|
||||
log.error("权限不足,缺少权限:"+ Arrays.toString(permissions));
|
||||
}
|
||||
return pass;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断接口是否有任意xxx,xxx角色
|
||||
* @param roles 角色
|
||||
* @return {boolean}
|
||||
*/
|
||||
public boolean requiresRoles(String... roles) {
|
||||
if (ArrayUtil.isEmpty(roles)) {
|
||||
return false;
|
||||
}
|
||||
LoginUser loginUser = SecureUtil.currentUser();
|
||||
|
||||
Object cache = redisUtil.get(buildKey("role", loginUser.getUsername()));
|
||||
Set<String> roleList;
|
||||
if (Objects.nonNull(cache)) {
|
||||
roleList = (Set<String>) cache;
|
||||
} else {
|
||||
roleList = commonAPI.queryUserRoles(loginUser.getUsername());
|
||||
redisUtil.set(buildKey("role", loginUser.getUsername()), roleList);
|
||||
}
|
||||
|
||||
boolean pass = roleList.stream().filter(StringUtils::hasText)
|
||||
.anyMatch(x -> PatternMatchUtils.simpleMatch(roles, x));
|
||||
if (!pass) {
|
||||
log.error("权限不足,缺少角色:" + Arrays.toString(roles));
|
||||
}
|
||||
return pass;
|
||||
}
|
||||
|
||||
/**
|
||||
* 由于缓存key是以人的维度,角色列表、权限列表在值中,jeecg是以权限列表绑定在角色上,形成的权限集合
|
||||
* 权限发生变更时,需要清理全部人的权限缓存
|
||||
*/
|
||||
public void clearCache() {
|
||||
redisUtil.removeAll(PERM_PREFIX);
|
||||
}
|
||||
|
||||
private String buildKey(String type, String username) {
|
||||
return PERM_PREFIX + type + SPLIT + username;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
package org.jeecg.config.security;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* spring authorization server 自定义redis保存授权范围信息
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JeecgRedisOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private final static Long TIMEOUT = 10L;
|
||||
|
||||
@Override
|
||||
public void save(OAuth2AuthorizationConsent authorizationConsent) {
|
||||
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
|
||||
|
||||
redisTemplate.opsForValue().set(buildKey(authorizationConsent), authorizationConsent, TIMEOUT,
|
||||
TimeUnit.MINUTES);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(OAuth2AuthorizationConsent authorizationConsent) {
|
||||
Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
|
||||
redisTemplate.delete(buildKey(authorizationConsent));
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
|
||||
Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
|
||||
Assert.hasText(principalName, "principalName cannot be empty");
|
||||
return (OAuth2AuthorizationConsent) redisTemplate.opsForValue()
|
||||
.get(buildKey(registeredClientId, principalName));
|
||||
}
|
||||
|
||||
private static String buildKey(String registeredClientId, String principalName) {
|
||||
return "token:consent:" + registeredClientId + ":" + principalName;
|
||||
}
|
||||
|
||||
private static String buildKey(OAuth2AuthorizationConsent authorizationConsent) {
|
||||
return buildKey(authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,181 @@
|
||||
package org.jeecg.config.security;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* spring authorization server自定义redis保存认证信息
|
||||
* @author EightMonth
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JeecgRedisOAuth2AuthorizationService implements OAuth2AuthorizationService {
|
||||
|
||||
private final static Long TIMEOUT = 10L;
|
||||
|
||||
private static final String AUTHORIZATION = "token";
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Override
|
||||
public void save(OAuth2Authorization authorization) {
|
||||
Assert.notNull(authorization, "authorization cannot be null");
|
||||
|
||||
if (isState(authorization)) {
|
||||
String token = authorization.getAttribute("state");
|
||||
redisTemplate.setValueSerializer(RedisSerializer.java());
|
||||
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.STATE, token), authorization, TIMEOUT,
|
||||
TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
if (isCode(authorization)) {
|
||||
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
|
||||
.getToken(OAuth2AuthorizationCode.class);
|
||||
OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();
|
||||
long between = ChronoUnit.MINUTES.between(authorizationCodeToken.getIssuedAt(),
|
||||
authorizationCodeToken.getExpiresAt());
|
||||
redisTemplate.setValueSerializer(RedisSerializer.java());
|
||||
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()),
|
||||
authorization, between, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
if (isRefreshToken(authorization)) {
|
||||
OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
|
||||
long between = ChronoUnit.SECONDS.between(refreshToken.getIssuedAt(), refreshToken.getExpiresAt());
|
||||
redisTemplate.setValueSerializer(RedisSerializer.java());
|
||||
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()),
|
||||
authorization, between, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
if (isAccessToken(authorization)) {
|
||||
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
|
||||
long between = ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt());
|
||||
redisTemplate.setValueSerializer(RedisSerializer.java());
|
||||
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()),
|
||||
authorization, between, TimeUnit.SECONDS);
|
||||
|
||||
// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx
|
||||
String tokenUsername = String.format("%s::%s::%s", AUTHORIZATION, authorization.getPrincipalName(), accessToken.getTokenValue());
|
||||
redisTemplate.opsForValue().set(tokenUsername, accessToken.getTokenValue(), between, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(OAuth2Authorization authorization) {
|
||||
Assert.notNull(authorization, "authorization cannot be null");
|
||||
|
||||
List<String> keys = new ArrayList<>();
|
||||
if (isState(authorization)) {
|
||||
String token = authorization.getAttribute("state");
|
||||
keys.add(buildKey(OAuth2ParameterNames.STATE, token));
|
||||
}
|
||||
|
||||
if (isCode(authorization)) {
|
||||
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
|
||||
.getToken(OAuth2AuthorizationCode.class);
|
||||
OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();
|
||||
keys.add(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()));
|
||||
}
|
||||
|
||||
if (isRefreshToken(authorization)) {
|
||||
OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
|
||||
keys.add(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()));
|
||||
}
|
||||
|
||||
if (isAccessToken(authorization)) {
|
||||
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
|
||||
keys.add(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()));
|
||||
|
||||
// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx
|
||||
String key = String.format("%s::%s::%s", AUTHORIZATION, authorization.getPrincipalName(), accessToken.getTokenValue());
|
||||
keys.add(key);
|
||||
}
|
||||
|
||||
redisTemplate.delete(keys);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public OAuth2Authorization findById(String id) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
|
||||
Assert.hasText(token, "token cannot be empty");
|
||||
Assert.notNull(tokenType, "tokenType cannot be empty");
|
||||
redisTemplate.setValueSerializer(RedisSerializer.java());
|
||||
return (OAuth2Authorization) redisTemplate.opsForValue().get(buildKey(tokenType.getValue(), token));
|
||||
}
|
||||
|
||||
private String buildKey(String type, String id) {
|
||||
return String.format("%s::%s::%s", AUTHORIZATION, type, id);
|
||||
}
|
||||
|
||||
private static boolean isState(OAuth2Authorization authorization) {
|
||||
return Objects.nonNull(authorization.getAttribute("state"));
|
||||
}
|
||||
|
||||
private static boolean isCode(OAuth2Authorization authorization) {
|
||||
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
|
||||
.getToken(OAuth2AuthorizationCode.class);
|
||||
return Objects.nonNull(authorizationCode);
|
||||
}
|
||||
|
||||
private static boolean isRefreshToken(OAuth2Authorization authorization) {
|
||||
return Objects.nonNull(authorization.getRefreshToken());
|
||||
}
|
||||
|
||||
private static boolean isAccessToken(OAuth2Authorization authorization) {
|
||||
return Objects.nonNull(authorization.getAccessToken());
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展方法根据 username 查询是否存在存储的
|
||||
* @param authentication
|
||||
* @return
|
||||
*/
|
||||
public void removeByUsername(Authentication authentication) {
|
||||
// 根据 username查询对应access-token
|
||||
String authenticationName = authentication.getName();
|
||||
|
||||
// 扩展记录 access-token 、username 的关系 1::token::username::admin::xxx
|
||||
String tokenUsernameKey = String.format("%s::%s::*", AUTHORIZATION, authenticationName);
|
||||
Set<String> keys = redisTemplate.keys(tokenUsernameKey);
|
||||
if (CollUtil.isEmpty(keys)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Object> tokenList = redisTemplate.opsForValue().multiGet(keys);
|
||||
|
||||
for (Object token : tokenList) {
|
||||
// 根据token 查询存储的 OAuth2Authorization
|
||||
OAuth2Authorization authorization = this.findByToken((String) token, OAuth2TokenType.ACCESS_TOKEN);
|
||||
// 根据 OAuth2Authorization 删除相关令牌
|
||||
this.remove(authorization);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package org.jeecg.config.security;
|
||||
|
||||
/**
|
||||
* 登录模式
|
||||
* @author EightMonth
|
||||
* @date 2024/1/10 17:43
|
||||
*/
|
||||
public class LoginType {
|
||||
|
||||
/**
|
||||
* 密码模式
|
||||
*/
|
||||
public static final String PASSWORD = "password";
|
||||
|
||||
|
||||
/**
|
||||
* 手机号+验证码模式
|
||||
*/
|
||||
public static final String PHONE = "phone";
|
||||
|
||||
|
||||
/**
|
||||
* app登录
|
||||
*/
|
||||
public static final String APP = "app";
|
||||
|
||||
/**
|
||||
* 扫码登录
|
||||
*/
|
||||
public static final String SCAN = "scan";
|
||||
|
||||
/**
|
||||
* 所有联合登录,比如github\钉钉\企业微信\微信
|
||||
*/
|
||||
public static final String SOCIAL = "social";
|
||||
|
||||
public static final String SELF = "self";
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package org.jeecg.config.security;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.jeecg.common.system.util.JwtUtil;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
|
||||
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 当用户被强退时,使客户端token失效
|
||||
* @author eightmonth@qq.com
|
||||
* @date 2024/3/7 17:30
|
||||
*/
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class RedisTokenValidationFilter extends OncePerRequestFilter {
|
||||
private OAuth2AuthorizationService authorizationService;
|
||||
private JwtDecoder jwtDecoder;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
// 从请求中获取token
|
||||
DefaultBearerTokenResolver defaultBearerTokenResolver = new DefaultBearerTokenResolver();
|
||||
String token = defaultBearerTokenResolver.resolve(request);
|
||||
|
||||
|
||||
if (Objects.nonNull(token)) {
|
||||
// 检查认证信息是否已被清除,如果已被清除,则令该token失效
|
||||
OAuth2Authorization oAuth2Authorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
|
||||
if (Objects.isNull(oAuth2Authorization)) {
|
||||
throw new OAuth2AuthenticationException(BearerTokenErrors.invalidToken("认证信息已失效,请重新登录"));
|
||||
}
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,262 @@
|
||||
package org.jeecg.config.security;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.jeecg.config.security.app.AppGrantAuthenticationConvert;
|
||||
import org.jeecg.config.security.app.AppGrantAuthenticationProvider;
|
||||
import org.jeecg.config.security.password.PasswordGrantAuthenticationConvert;
|
||||
import org.jeecg.config.security.password.PasswordGrantAuthenticationProvider;
|
||||
import org.jeecg.config.security.phone.PhoneGrantAuthenticationConvert;
|
||||
import org.jeecg.config.security.phone.PhoneGrantAuthenticationProvider;
|
||||
import org.jeecg.config.security.social.SocialGrantAuthenticationConvert;
|
||||
import org.jeecg.config.security.social.SocialGrantAuthenticationProvider;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
|
||||
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.token.*;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.header.writers.frameoptions.RegExpAllowFromStrategy;
|
||||
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* spring authorization server核心配置
|
||||
* @author eightmonth@qq.com
|
||||
* @date 2024/1/2 9:29
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
@AllArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
private OAuth2AuthorizationService authorizationService;
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
|
||||
throws Exception {
|
||||
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
|
||||
// 注册自定义登录类型
|
||||
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
|
||||
.tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverter(new PasswordGrantAuthenticationConvert())
|
||||
.authenticationProvider(new PasswordGrantAuthenticationProvider(authorizationService, tokenGenerator())))
|
||||
.tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverter(new PhoneGrantAuthenticationConvert())
|
||||
.authenticationProvider(new PhoneGrantAuthenticationProvider(authorizationService, tokenGenerator())))
|
||||
.tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverter(new AppGrantAuthenticationConvert())
|
||||
.authenticationProvider(new AppGrantAuthenticationProvider(authorizationService, tokenGenerator())))
|
||||
.tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverter(new SocialGrantAuthenticationConvert())
|
||||
.authenticationProvider(new SocialGrantAuthenticationProvider(authorizationService, tokenGenerator())))
|
||||
//开启OpenID Connect 1.0(其中oidc为OpenID Connect的缩写)。 访问 /.well-known/openid-configuration即可获取认证信息
|
||||
.oidc(Customizer.withDefaults());
|
||||
http
|
||||
//将需要认证的请求,重定向到login页面行登录认证。
|
||||
.exceptionHandling((exceptions) -> exceptions
|
||||
.defaultAuthenticationEntryPointFor(
|
||||
new LoginUrlAuthenticationEntryPoint("/sys/login"),
|
||||
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
|
||||
)
|
||||
)
|
||||
// 使用jwt处理接收到的access token
|
||||
.oauth2ResourceServer(oauth2ResourceServer ->
|
||||
oauth2ResourceServer.jwt(Customizer.withDefaults()));
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(2)
|
||||
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
|
||||
throws Exception {
|
||||
http
|
||||
//设置所有请求都需要认证,未认证的请求都被重定向到login页面进行登录
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/cas/client/validateLogin")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/randomImage/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/checkCaptcha")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/login")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/mLogin")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/logout")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/thirdLogin/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/getEncryptedString")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/sms")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/phoneLogin")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/checkOnlyUser")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/register")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/phoneVerification")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/user/passwordChange")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/auth/2step-code")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/common/static/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/common/pdf/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/generic/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/getLoginQrcode/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/getQrcodeToken/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/checkAuth")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/doc.html")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.js")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.css")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.html")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.svg")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.pdf")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.jpg")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.png")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.gif")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.ico")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.ttf")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.woff")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.woff2")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/druid/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/swagger-ui.html")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/swagger**/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/webjars/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/v3/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/WW_verify*")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/annountCement/show/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/jmreport/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.js.map")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/**/*.css.map")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/view")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/page/queryById")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getAllChartData")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/onlDragDatasetHead/getTotalData")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/drag/mock/json/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/test/bigScreen/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/bigscreen/template1/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/bigscreen/template1/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/websocket/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/newsWebsocket/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/vxeSocket/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/test/seata/**")).permitAll()
|
||||
.requestMatchers(AntPathRequestMatcher.antMatcher("/error")).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
|
||||
.cors(cors -> cors
|
||||
.configurationSource(req -> {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.applyPermitDefaultValues();
|
||||
config.setAllowedMethods(Arrays.asList("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
|
||||
return config;
|
||||
}))
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
|
||||
return http.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库保存注册客户端信息
|
||||
*/
|
||||
@Bean
|
||||
public RegisteredClientRepository registeredClientRepository() {
|
||||
return new JdbcRegisteredClientRepository(jdbcTemplate);
|
||||
}
|
||||
|
||||
/**
|
||||
*配置 JWK,为JWT(id_token)提供加密密钥,用于加密/解密或签名/验签
|
||||
* JWK详细见:https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key-41
|
||||
*/
|
||||
@Bean
|
||||
public JWKSource<SecurityContext> jwkSource() {
|
||||
KeyPair keyPair = generateRsaKey();
|
||||
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
|
||||
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
|
||||
RSAKey rsaKey = new RSAKey.Builder(publicKey)
|
||||
.privateKey(privateKey)
|
||||
// 重要!生产环境需要修改!
|
||||
.keyID("jeecg")
|
||||
.build();
|
||||
JWKSet jwkSet = new JWKSet(rsaKey);
|
||||
return new ImmutableJWKSet<>(jwkSet);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return NoOpPasswordEncoder.getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
*生成RSA密钥对,给上面jwkSource() 方法的提供密钥对
|
||||
*/
|
||||
private static KeyPair generateRsaKey() {
|
||||
KeyPair keyPair;
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
||||
|
||||
// 生产环境不应该设置secureRandom,seed如果被泄露,jwt容易被伪造
|
||||
// 如果不设置secureRandom,会存在一个问题,当应用重启后,原有的token将会全部失效,因为重启的keyPair与之前已经不同
|
||||
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
|
||||
// 重要!生产环境需要修改!
|
||||
secureRandom.setSeed("jeecg".getBytes());
|
||||
keyPairGenerator.initialize(2048, secureRandom);
|
||||
keyPair = keyPairGenerator.generateKeyPair();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置jwt解析器
|
||||
*/
|
||||
@Bean
|
||||
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
|
||||
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
|
||||
}
|
||||
|
||||
/**
|
||||
*配置认证服务器请求地址
|
||||
*/
|
||||
@Bean
|
||||
public AuthorizationServerSettings authorizationServerSettings() {
|
||||
return AuthorizationServerSettings.builder().tokenEndpoint("/sys/login").build();
|
||||
}
|
||||
|
||||
/**
|
||||
*配置token生成器
|
||||
*/
|
||||
@Bean
|
||||
OAuth2TokenGenerator<?> tokenGenerator() {
|
||||
JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource()));
|
||||
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
|
||||
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
|
||||
return new DelegatingOAuth2TokenGenerator(
|
||||
jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
package org.jeecg.config.security.app;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jeecg.config.security.LoginType;
|
||||
import org.jeecg.config.security.password.PasswordGrantAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* APP模式认证转换器
|
||||
* @author EightMonth
|
||||
* @date 2024/1/1
|
||||
*/
|
||||
public class AppGrantAuthenticationConvert implements AuthenticationConverter {
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
|
||||
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||
if (!LoginType.APP.equals(grantType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
//从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||
MultiValueMap<String, String> parameters = getParameters(request);
|
||||
|
||||
// username (REQUIRED)
|
||||
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
|
||||
if (!StringUtils.hasText(username) ||
|
||||
parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
|
||||
throw new OAuth2AuthenticationException("无效请求,用户名不能为空!");
|
||||
}
|
||||
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
|
||||
if (!StringUtils.hasText(password) ||
|
||||
parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {
|
||||
throw new OAuth2AuthenticationException("无效请求,密码不能为空!");
|
||||
}
|
||||
|
||||
//收集要传入PasswordGrantAuthenticationToken构造方法的参数,
|
||||
//该参数接下来在PasswordGrantAuthenticationProvider中使用
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
//遍历从request中提取的参数,排除掉grant_type、client_id、code等字段参数,其他参数收集到additionalParameters中
|
||||
parameters.forEach((key, value) -> {
|
||||
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
|
||||
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
|
||||
!key.equals(OAuth2ParameterNames.CODE)) {
|
||||
additionalParameters.put(key, value.get(0));
|
||||
}
|
||||
});
|
||||
|
||||
//返回自定义的PasswordGrantAuthenticationToken对象
|
||||
return new PasswordGrantAuthenticationToken(clientPrincipal, additionalParameters);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||
*/
|
||||
private static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
|
||||
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
|
||||
parameterMap.forEach((key, values) -> {
|
||||
if (values.length > 0) {
|
||||
for (String value : values) {
|
||||
parameters.add(key, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,318 @@
|
||||
package org.jeecg.config.security.app;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.CommonAPI;
|
||||
import org.jeecg.common.constant.CacheConstant;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.exception.JeecgCaptchaException;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.system.vo.SysDepartModel;
|
||||
import org.jeecg.common.util.Md5Util;
|
||||
import org.jeecg.common.util.PasswordUtil;
|
||||
import org.jeecg.common.util.RedisUtil;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.config.security.password.PasswordGrantAuthenticationToken;
|
||||
import org.jeecg.modules.base.service.BaseCommonService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.*;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* APP模式认证处理器,负责处理该认证模式下的核心逻辑
|
||||
* @author EightMonth
|
||||
* @date 2024/1/1
|
||||
*/
|
||||
@Slf4j
|
||||
public class AppGrantAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||
@Autowired
|
||||
private CommonAPI commonAPI;
|
||||
@Autowired
|
||||
private RedisUtil redisUtil;
|
||||
@Autowired
|
||||
private JeecgBaseConfig jeecgBaseConfig;
|
||||
@Autowired
|
||||
private BaseCommonService baseCommonService;
|
||||
|
||||
public AppGrantAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||
this.authorizationService = authorizationService;
|
||||
this.tokenGenerator = tokenGenerator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
AppGrantAuthenticationToken appGrantAuthenticationToken = (AppGrantAuthenticationToken) authentication;
|
||||
Map<String, Object> additionalParameter = appGrantAuthenticationToken.getAdditionalParameters();
|
||||
|
||||
// 授权类型
|
||||
AuthorizationGrantType authorizationGrantType = appGrantAuthenticationToken.getGrantType();
|
||||
// 用户名
|
||||
String username = (String) additionalParameter.get(OAuth2ParameterNames.USERNAME);
|
||||
// 密码
|
||||
String password = (String) additionalParameter.get(OAuth2ParameterNames.PASSWORD);
|
||||
//请求参数权限范围
|
||||
String requestScopesStr = (String)additionalParameter.getOrDefault(OAuth2ParameterNames.SCOPE, "*");
|
||||
//请求参数权限范围专场集合
|
||||
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
|
||||
// 验证码
|
||||
String captcha = (String) additionalParameter.get("captcha");
|
||||
String checkKey = (String) additionalParameter.get("checkKey");
|
||||
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(appGrantAuthenticationToken);
|
||||
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||
|
||||
// 检查登录失败次数
|
||||
if(isLoginFailOvertimes(username)){
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "该用户登录失败次数过多,请于10分钟后再次登录!");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
}
|
||||
|
||||
if(captcha==null){
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "验证码无效");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
}
|
||||
String lowerCaseCaptcha = captcha.toLowerCase();
|
||||
// 加入密钥作为混淆,避免简单的拼接,被外部利用,用户自定义该密钥即可
|
||||
String origin = lowerCaseCaptcha+checkKey+jeecgBaseConfig.getSignatureSecret();
|
||||
String realKey = Md5Util.md5Encode(origin, "utf-8");
|
||||
Object checkCode = redisUtil.get(realKey);
|
||||
//当进入登录页时,有一定几率出现验证码错误 #1714
|
||||
if(checkCode==null || !checkCode.toString().equals(lowerCaseCaptcha)) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "验证码错误");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
}
|
||||
|
||||
if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "非法登录");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
}
|
||||
|
||||
// 通过用户名获取用户信息
|
||||
LoginUser loginUser = commonAPI.getUserByName(username);
|
||||
//update-begin---author:eightmonth ---date:2024-04-30 for:【6168】master分支切sas分支登录发生错误-----------
|
||||
if (Objects.isNull(loginUser) || !StringUtils.hasText(loginUser.getSalt())) {
|
||||
redisUtil.del(CacheConstant.SYS_USERS_CACHE+"::"+username);
|
||||
loginUser = commonAPI.getUserByName(username);
|
||||
}
|
||||
//update-end---author:eightmonth ---date::2024-04-30 for:【6168】master分支切sas分支登录发生错误--------------
|
||||
// 检查用户可行性
|
||||
checkUserIsEffective(loginUser);
|
||||
|
||||
// 不使用spring security passwordEncoder针对密码进行匹配,使用自有加密匹配,针对 spring security使用noop传输
|
||||
password = PasswordUtil.encrypt(username, password, loginUser.getSalt());
|
||||
if (!password.equals(loginUser.getPassword())) {
|
||||
addLoginFailOvertimes(username);
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "用户名或密码不正确");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
}
|
||||
|
||||
//由于在上面已验证过用户名、密码,现在构建一个已认证的对象UsernamePasswordAuthenticationToken
|
||||
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(loginUser,clientPrincipal,new ArrayList<>());
|
||||
|
||||
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||
.registeredClient(registeredClient)
|
||||
.principal(usernamePasswordAuthenticationToken)
|
||||
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||
.authorizationGrantType(authorizationGrantType)
|
||||
.authorizedScopes(requestScopeSet)
|
||||
.authorizationGrant(appGrantAuthenticationToken);
|
||||
|
||||
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||
.principalName(clientPrincipal.getName())
|
||||
.authorizedScopes(requestScopeSet)
|
||||
.attribute(Principal.class.getName(), username)
|
||||
.authorizationGrantType(authorizationGrantType);
|
||||
|
||||
|
||||
// ----- Access token -----
|
||||
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
|
||||
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (generatedAccessToken == null) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "无法生成访问token,请联系管理系。");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
}
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||
if (generatedAccessToken instanceof ClaimAccessor) {
|
||||
authorizationBuilder.token(accessToken, (metadata) -> {
|
||||
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
|
||||
});
|
||||
} else {
|
||||
authorizationBuilder.accessToken(accessToken);
|
||||
}
|
||||
|
||||
// ----- Refresh token -----
|
||||
OAuth2RefreshToken refreshToken = null;
|
||||
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
|
||||
// 不向公共客户端颁发刷新令牌
|
||||
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
|
||||
|
||||
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
|
||||
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "无法生成刷新token,请联系管理员。");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
}
|
||||
|
||||
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
|
||||
authorizationBuilder.refreshToken(refreshToken);
|
||||
}
|
||||
|
||||
OAuth2Authorization authorization = authorizationBuilder.build();
|
||||
|
||||
// 保存认证信息至redis
|
||||
authorizationService.save(authorization);
|
||||
|
||||
// 登录成功,删除redis中的验证码
|
||||
redisUtil.del(realKey);
|
||||
redisUtil.del(CommonConstant.LOGIN_FAIL + username);
|
||||
baseCommonService.addLog("用户名: " + username + ",登录成功!", CommonConstant.LOG_TYPE_1, null,loginUser);
|
||||
|
||||
JSONObject addition = new JSONObject(new LinkedHashMap<>());
|
||||
addition.put("token", accessToken.getTokenValue());
|
||||
// 设置租户
|
||||
JSONObject jsonObject = commonAPI.setLoginTenant(username);
|
||||
addition.putAll(jsonObject.getInnerMap());
|
||||
|
||||
// 设置登录用户信息
|
||||
addition.put("userInfo", loginUser);
|
||||
addition.put("sysAllDictItems", commonAPI.queryAllDictItems());
|
||||
|
||||
List<SysDepartModel> departs = commonAPI.queryUserDeparts(loginUser.getId());
|
||||
addition.put("departs", departs);
|
||||
if (departs == null || departs.size() == 0) {
|
||||
addition.put("multi_depart", 0);
|
||||
} else if (departs.size() == 1) {
|
||||
commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null);
|
||||
addition.put("multi_depart", 1);
|
||||
} else {
|
||||
//查询当前是否有登录部门
|
||||
if(oConvertUtils.isEmpty(loginUser.getOrgCode())){
|
||||
commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null);
|
||||
}
|
||||
addition.put("multi_depart", 2);
|
||||
}
|
||||
|
||||
// 兼容原有shiro登录结果处理
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("result", addition);
|
||||
map.put("code", 200);
|
||||
map.put("success", true);
|
||||
map.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
// 返回access_token、refresh_token以及其它信息给到前端
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, map);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return AppGrantAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
|
||||
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
|
||||
}
|
||||
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
|
||||
return clientPrincipal;
|
||||
}
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录失败超出次数5 返回true
|
||||
* @param username
|
||||
* @return
|
||||
*/
|
||||
private boolean isLoginFailOvertimes(String username){
|
||||
String key = CommonConstant.LOGIN_FAIL + username;
|
||||
Object failTime = redisUtil.get(key);
|
||||
if(failTime!=null){
|
||||
Integer val = Integer.parseInt(failTime.toString());
|
||||
if(val>5){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录失败次数
|
||||
* @param username
|
||||
*/
|
||||
private void addLoginFailOvertimes(String username){
|
||||
String key = CommonConstant.LOGIN_FAIL + username;
|
||||
Object failTime = redisUtil.get(key);
|
||||
Integer val = 0;
|
||||
if(failTime!=null){
|
||||
val = Integer.parseInt(failTime.toString());
|
||||
}
|
||||
// 10分钟
|
||||
redisUtil.set(key, ++val, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验用户是否有效
|
||||
*/
|
||||
private void checkUserIsEffective(LoginUser loginUser) {
|
||||
//情况1:根据用户信息查询,该用户不存在
|
||||
if (Objects.isNull(loginUser)) {
|
||||
baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null);
|
||||
throw new JeecgBootException("该用户不存在,请注册");
|
||||
}
|
||||
//情况2:根据用户信息查询,该用户已注销
|
||||
//update-begin---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
|
||||
//update-end---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null);
|
||||
throw new JeecgBootException("该用户已注销");
|
||||
}
|
||||
//情况3:根据用户信息查询,该用户已冻结
|
||||
if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
|
||||
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null);
|
||||
throw new JeecgBootException("该用户已冻结");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package org.jeecg.config.security.app;
|
||||
|
||||
import org.jeecg.config.security.LoginType;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* APP模式认证专用token类型,方法spring authorization server进行认证流转,配合convert使用
|
||||
* @author EightMonth
|
||||
* @date 2024/1/1
|
||||
*/
|
||||
public class AppGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||
|
||||
public AppGrantAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) {
|
||||
super(new AuthorizationGrantType(LoginType.APP), clientPrincipal, additionalParameters);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
package org.jeecg.config.security.password;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.config.security.LoginType;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 密码模式认证转换器
|
||||
* @author EightMonth
|
||||
* @date 2024/1/1
|
||||
*/
|
||||
public class PasswordGrantAuthenticationConvert implements AuthenticationConverter {
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
|
||||
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||
if (!LoginType.PASSWORD.equals(grantType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
//从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||
MultiValueMap<String, String> parameters = getParameters(request);
|
||||
|
||||
// username (REQUIRED)
|
||||
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
|
||||
if (!StringUtils.hasText(username) ||
|
||||
parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
|
||||
throw new OAuth2AuthenticationException("无效请求,用户名不能为空!");
|
||||
}
|
||||
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
|
||||
if (!StringUtils.hasText(password) ||
|
||||
parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {
|
||||
throw new OAuth2AuthenticationException("无效请求,密码不能为空!");
|
||||
}
|
||||
|
||||
//收集要传入PasswordGrantAuthenticationToken构造方法的参数,
|
||||
//该参数接下来在PasswordGrantAuthenticationProvider中使用
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
//遍历从request中提取的参数,排除掉grant_type、client_id、code等字段参数,其他参数收集到additionalParameters中
|
||||
parameters.forEach((key, value) -> {
|
||||
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
|
||||
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
|
||||
!key.equals(OAuth2ParameterNames.CODE)) {
|
||||
additionalParameters.put(key, value.get(0));
|
||||
}
|
||||
});
|
||||
|
||||
//返回自定义的PasswordGrantAuthenticationToken对象
|
||||
return new PasswordGrantAuthenticationToken(clientPrincipal, additionalParameters);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||
*/
|
||||
private static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
|
||||
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
|
||||
parameterMap.forEach((key, values) -> {
|
||||
if (values.length > 0) {
|
||||
for (String value : values) {
|
||||
parameters.add(key, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,317 @@
|
||||
package org.jeecg.config.security.password;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.CommonAPI;
|
||||
import org.jeecg.common.constant.CacheConstant;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.exception.JeecgCaptchaException;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.system.vo.SysDepartModel;
|
||||
import org.jeecg.common.util.Md5Util;
|
||||
import org.jeecg.common.util.PasswordUtil;
|
||||
import org.jeecg.common.util.RedisUtil;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.modules.base.service.BaseCommonService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.oauth2.core.*;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 密码模式认证处理器,负责处理该认证模式下的核心逻辑
|
||||
* @author EightMonth
|
||||
* @date 2024/1/1
|
||||
*/
|
||||
@Slf4j
|
||||
public class PasswordGrantAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||
@Autowired
|
||||
private CommonAPI commonAPI;
|
||||
@Autowired
|
||||
private RedisUtil redisUtil;
|
||||
@Autowired
|
||||
private JeecgBaseConfig jeecgBaseConfig;
|
||||
@Autowired
|
||||
private BaseCommonService baseCommonService;
|
||||
|
||||
public PasswordGrantAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||
this.authorizationService = authorizationService;
|
||||
this.tokenGenerator = tokenGenerator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
PasswordGrantAuthenticationToken passwordGrantAuthenticationToken = (PasswordGrantAuthenticationToken) authentication;
|
||||
Map<String, Object> additionalParameter = passwordGrantAuthenticationToken.getAdditionalParameters();
|
||||
|
||||
// 授权类型
|
||||
AuthorizationGrantType authorizationGrantType = passwordGrantAuthenticationToken.getGrantType();
|
||||
// 用户名
|
||||
String username = (String) additionalParameter.get(OAuth2ParameterNames.USERNAME);
|
||||
// 密码
|
||||
String password = (String) additionalParameter.get(OAuth2ParameterNames.PASSWORD);
|
||||
//请求参数权限范围
|
||||
String requestScopesStr = (String)additionalParameter.getOrDefault(OAuth2ParameterNames.SCOPE, "*");
|
||||
//请求参数权限范围专场集合
|
||||
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
|
||||
// 验证码
|
||||
String captcha = (String) additionalParameter.get("captcha");
|
||||
String checkKey = (String) additionalParameter.get("checkKey");
|
||||
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(passwordGrantAuthenticationToken);
|
||||
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||
|
||||
// 检查登录失败次数
|
||||
if(isLoginFailOvertimes(username)){
|
||||
throw new JeecgBootException("该用户登录失败次数过多,请于10分钟后再次登录!");
|
||||
}
|
||||
|
||||
if(captcha==null){
|
||||
throw new JeecgBootException("验证码无效");
|
||||
}
|
||||
String lowerCaseCaptcha = captcha.toLowerCase();
|
||||
// 加入密钥作为混淆,避免简单的拼接,被外部利用,用户自定义该密钥即可
|
||||
String origin = lowerCaseCaptcha+checkKey+jeecgBaseConfig.getSignatureSecret();
|
||||
String realKey = Md5Util.md5Encode(origin, "utf-8");
|
||||
Object checkCode = redisUtil.get(realKey);
|
||||
//当进入登录页时,有一定几率出现验证码错误 #1714
|
||||
if(checkCode==null || !checkCode.toString().equals(lowerCaseCaptcha)) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "验证码错误");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
}
|
||||
|
||||
if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "非法登录");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
}
|
||||
|
||||
// 通过用户名获取用户信息
|
||||
LoginUser loginUser = commonAPI.getUserByName(username);
|
||||
//update-begin---author:eightmonth ---date:2024-04-30 for:【6168】master分支切sas分支登录发生错误-----------
|
||||
if (Objects.isNull(loginUser) || !StringUtils.hasText(loginUser.getSalt())) {
|
||||
redisUtil.del(CacheConstant.SYS_USERS_CACHE+"::"+username);
|
||||
loginUser = commonAPI.getUserByName(username);
|
||||
}
|
||||
//update-end---author:eightmonth ---date::2024-04-30 for:【6168】master分支切sas分支登录发生错误--------------
|
||||
// 检查用户可行性
|
||||
checkUserIsEffective(loginUser);
|
||||
|
||||
// 不使用spring security passwordEncoder针对密码进行匹配,使用自有加密匹配,针对 spring security使用noop传输
|
||||
password = PasswordUtil.encrypt(username, password, loginUser.getSalt());
|
||||
if (!password.equals(loginUser.getPassword())) {
|
||||
addLoginFailOvertimes(username);
|
||||
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "用户名或密码不正确");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
}
|
||||
|
||||
//由于在上面已验证过用户名、密码,现在构建一个已认证的对象UsernamePasswordAuthenticationToken
|
||||
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(loginUser,clientPrincipal,new ArrayList<>());
|
||||
|
||||
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||
.registeredClient(registeredClient)
|
||||
.principal(usernamePasswordAuthenticationToken)
|
||||
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||
.authorizationGrantType(authorizationGrantType)
|
||||
.authorizedScopes(requestScopeSet)
|
||||
.authorizationGrant(passwordGrantAuthenticationToken);
|
||||
|
||||
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||
.principalName(clientPrincipal.getName())
|
||||
.authorizedScopes(requestScopeSet)
|
||||
.attribute(Principal.class.getName(), username)
|
||||
.authorizationGrantType(authorizationGrantType);
|
||||
|
||||
|
||||
// ----- Access token -----
|
||||
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
|
||||
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (generatedAccessToken == null) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "无法生成访问token,请联系管理系。");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
}
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||
if (generatedAccessToken instanceof ClaimAccessor) {
|
||||
authorizationBuilder.token(accessToken, (metadata) -> {
|
||||
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
|
||||
});
|
||||
} else {
|
||||
authorizationBuilder.accessToken(accessToken);
|
||||
}
|
||||
|
||||
// ----- Refresh token -----
|
||||
OAuth2RefreshToken refreshToken = null;
|
||||
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
|
||||
// 不向公共客户端颁发刷新令牌
|
||||
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
|
||||
|
||||
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
|
||||
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "无法生成访问token,请联系管理系。");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
}
|
||||
|
||||
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
|
||||
authorizationBuilder.refreshToken(refreshToken);
|
||||
}
|
||||
|
||||
OAuth2Authorization authorization = authorizationBuilder.build();
|
||||
|
||||
// 保存认证信息至redis
|
||||
authorizationService.save(authorization);
|
||||
|
||||
// 登录成功,删除redis中的验证码
|
||||
redisUtil.del(realKey);
|
||||
redisUtil.del(CommonConstant.LOGIN_FAIL + username);
|
||||
baseCommonService.addLog("用户名: " + username + ",登录成功!", CommonConstant.LOG_TYPE_1, null,loginUser);
|
||||
|
||||
JSONObject addition = new JSONObject(new LinkedHashMap<>());
|
||||
addition.put("token", accessToken.getTokenValue());
|
||||
|
||||
// 设置租户
|
||||
JSONObject jsonObject = commonAPI.setLoginTenant(username);
|
||||
addition.putAll(jsonObject.getInnerMap());
|
||||
|
||||
// 设置登录用户信息
|
||||
addition.put("userInfo", loginUser);
|
||||
addition.put("sysAllDictItems", commonAPI.queryAllDictItems());
|
||||
|
||||
List<SysDepartModel> departs = commonAPI.queryUserDeparts(loginUser.getId());
|
||||
addition.put("departs", departs);
|
||||
if (departs == null || departs.size() == 0) {
|
||||
addition.put("multi_depart", 0);
|
||||
} else if (departs.size() == 1) {
|
||||
commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null);
|
||||
addition.put("multi_depart", 1);
|
||||
} else {
|
||||
//查询当前是否有登录部门
|
||||
if(oConvertUtils.isEmpty(loginUser.getOrgCode())){
|
||||
commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null);
|
||||
}
|
||||
addition.put("multi_depart", 2);
|
||||
}
|
||||
|
||||
// 兼容原有shiro登录结果处理
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("result", addition);
|
||||
map.put("code", 200);
|
||||
map.put("success", true);
|
||||
map.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
// 返回access_token、refresh_token以及其它信息给到前端
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, map);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return PasswordGrantAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
|
||||
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
|
||||
}
|
||||
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
|
||||
return clientPrincipal;
|
||||
}
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录失败超出次数5 返回true
|
||||
* @param username
|
||||
* @return
|
||||
*/
|
||||
private boolean isLoginFailOvertimes(String username){
|
||||
String key = CommonConstant.LOGIN_FAIL + username;
|
||||
Object failTime = redisUtil.get(key);
|
||||
if(failTime!=null){
|
||||
Integer val = Integer.parseInt(failTime.toString());
|
||||
if(val>5){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录失败次数
|
||||
* @param username
|
||||
*/
|
||||
private void addLoginFailOvertimes(String username){
|
||||
String key = CommonConstant.LOGIN_FAIL + username;
|
||||
Object failTime = redisUtil.get(key);
|
||||
Integer val = 0;
|
||||
if(failTime!=null){
|
||||
val = Integer.parseInt(failTime.toString());
|
||||
}
|
||||
// 10分钟
|
||||
redisUtil.set(key, ++val, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验用户是否有效
|
||||
*/
|
||||
private void checkUserIsEffective(LoginUser loginUser) {
|
||||
//情况1:根据用户信息查询,该用户不存在
|
||||
if (Objects.isNull(loginUser)) {
|
||||
baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null);
|
||||
throw new JeecgBootException("该用户不存在,请注册");
|
||||
}
|
||||
//情况2:根据用户信息查询,该用户已注销
|
||||
//update-begin---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
|
||||
//update-end---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null);
|
||||
throw new JeecgBootException("该用户已注销");
|
||||
}
|
||||
//情况3:根据用户信息查询,该用户已冻结
|
||||
if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
|
||||
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null);
|
||||
throw new JeecgBootException("该用户已冻结");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package org.jeecg.config.security.password;
|
||||
|
||||
import org.jeecg.config.security.LoginType;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 密码模式认证专用token类型,方法spring authorization server进行认证流转,配合convert使用
|
||||
* @author EightMonth
|
||||
* @date 2024/1/1
|
||||
*/
|
||||
public class PasswordGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||
|
||||
public PasswordGrantAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) {
|
||||
super(new AuthorizationGrantType(LoginType.PASSWORD), clientPrincipal, additionalParameters);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
package org.jeecg.config.security.phone;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.jeecg.config.security.LoginType;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 手机号模式认证转换器
|
||||
* @author EightMonth
|
||||
* @date 2024/1/1
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class PhoneGrantAuthenticationConvert implements AuthenticationConverter {
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
|
||||
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||
if (!LoginType.PHONE.equals(grantType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
//从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||
MultiValueMap<String, String> parameters = getParameters(request);
|
||||
|
||||
// 验证码
|
||||
String captcha = parameters.getFirst("captcha");
|
||||
if (!StringUtils.hasText(captcha)) {
|
||||
throw new OAuth2AuthenticationException("无效请求,验证码不能为空!");
|
||||
}
|
||||
|
||||
//收集要传入PhoneGrantAuthenticationToken构造方法的参数,
|
||||
//该参数接下来在PhoneGrantAuthenticationProvider中使用
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
//遍历从request中提取的参数,排除掉grant_type、client_id、code等字段参数,其他参数收集到additionalParameters中
|
||||
parameters.forEach((key, value) -> {
|
||||
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
|
||||
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
|
||||
!key.equals(OAuth2ParameterNames.CODE)) {
|
||||
additionalParameters.put(key, value.get(0));
|
||||
}
|
||||
});
|
||||
|
||||
//返回自定义的PhoneGrantAuthenticationToken对象
|
||||
return new PhoneGrantAuthenticationToken(clientPrincipal, additionalParameters);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||
*/
|
||||
private static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
|
||||
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
|
||||
parameterMap.forEach((key, values) -> {
|
||||
if (values.length > 0) {
|
||||
for (String value : values) {
|
||||
parameters.add(key, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
return parameters;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,290 @@
|
||||
package org.jeecg.config.security.phone;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.CommonAPI;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.exception.JeecgCaptchaException;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.system.vo.SysDepartModel;
|
||||
import org.jeecg.common.util.Md5Util;
|
||||
import org.jeecg.common.util.PasswordUtil;
|
||||
import org.jeecg.common.util.RedisUtil;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.config.security.password.PasswordGrantAuthenticationToken;
|
||||
import org.jeecg.modules.base.service.BaseCommonService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.*;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 手机号模式认证处理器,负责处理该认证模式下的核心逻辑
|
||||
* @author EightMonth
|
||||
* @date 2024/1/1
|
||||
*/
|
||||
@Slf4j
|
||||
public class PhoneGrantAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||
@Autowired
|
||||
private CommonAPI commonAPI;
|
||||
@Autowired
|
||||
private RedisUtil redisUtil;
|
||||
@Autowired
|
||||
private JeecgBaseConfig jeecgBaseConfig;
|
||||
@Autowired
|
||||
private BaseCommonService baseCommonService;
|
||||
|
||||
public PhoneGrantAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||
this.authorizationService = authorizationService;
|
||||
this.tokenGenerator = tokenGenerator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
PhoneGrantAuthenticationToken phoneGrantAuthenticationToken = (PhoneGrantAuthenticationToken) authentication;
|
||||
Map<String, Object> additionalParameter = phoneGrantAuthenticationToken.getAdditionalParameters();
|
||||
|
||||
// 授权类型
|
||||
AuthorizationGrantType authorizationGrantType = phoneGrantAuthenticationToken.getGrantType();
|
||||
// 手机号
|
||||
String phone = (String) additionalParameter.get("mobile");
|
||||
|
||||
if(isLoginFailOvertimes(phone)){
|
||||
throw new JeecgBootException("该用户登录失败次数过多,请于10分钟后再次登录!");
|
||||
}
|
||||
|
||||
//请求参数权限范围
|
||||
String requestScopesStr = (String)additionalParameter.getOrDefault(OAuth2ParameterNames.SCOPE, "*");
|
||||
//请求参数权限范围专场集合
|
||||
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
|
||||
// 验证码
|
||||
String captcha = (String) additionalParameter.get("captcha");
|
||||
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(phoneGrantAuthenticationToken);
|
||||
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||
|
||||
// 通过手机号获取用户信息
|
||||
LoginUser loginUser = commonAPI.getUserByPhone(phone);
|
||||
// 检查用户可行性
|
||||
checkUserIsEffective(loginUser);
|
||||
|
||||
|
||||
String redisKey = CommonConstant.PHONE_REDIS_KEY_PRE+phone;
|
||||
Object code = redisUtil.get(redisKey);
|
||||
|
||||
if (!captcha.equals(code)) {
|
||||
//update-begin-author:taoyan date:2022-11-7 for: issues/4109 平台用户登录失败锁定用户
|
||||
addLoginFailOvertimes(phone);
|
||||
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "手机验证码错误");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
|
||||
}
|
||||
|
||||
if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "非法登录");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
}
|
||||
|
||||
//由于在上面已验证过用户名、密码,现在构建一个已认证的对象UsernamePasswordAuthenticationToken
|
||||
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(loginUser,clientPrincipal,new ArrayList<>());
|
||||
|
||||
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||
.registeredClient(registeredClient)
|
||||
.principal(usernamePasswordAuthenticationToken)
|
||||
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||
.authorizationGrantType(authorizationGrantType)
|
||||
.authorizedScopes(requestScopeSet)
|
||||
.authorizationGrant(phoneGrantAuthenticationToken);
|
||||
|
||||
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||
.principalName(clientPrincipal.getName())
|
||||
.authorizedScopes(requestScopeSet)
|
||||
.attribute(Principal.class.getName(), loginUser.getUsername())
|
||||
.authorizationGrantType(authorizationGrantType);
|
||||
|
||||
|
||||
// ----- Access token -----
|
||||
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
|
||||
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (generatedAccessToken == null) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "无法生成刷新token,请联系管理员。");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
}
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||
if (generatedAccessToken instanceof ClaimAccessor) {
|
||||
authorizationBuilder.token(accessToken, (metadata) -> {
|
||||
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
|
||||
});
|
||||
} else {
|
||||
authorizationBuilder.accessToken(accessToken);
|
||||
}
|
||||
|
||||
// ----- Refresh token -----
|
||||
OAuth2RefreshToken refreshToken = null;
|
||||
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
|
||||
// 不向公共客户端颁发刷新令牌
|
||||
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
|
||||
|
||||
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
|
||||
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "无法生成刷新token,请联系管理员。");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
}
|
||||
|
||||
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
|
||||
authorizationBuilder.refreshToken(refreshToken);
|
||||
}
|
||||
|
||||
OAuth2Authorization authorization = authorizationBuilder.build();
|
||||
|
||||
// 保存认证信息至redis
|
||||
authorizationService.save(authorization);
|
||||
|
||||
baseCommonService.addLog("用户名: " + loginUser.getUsername() + ",登录成功!", CommonConstant.LOG_TYPE_1, null,loginUser);
|
||||
|
||||
JSONObject addition = new JSONObject(new LinkedHashMap<>());
|
||||
addition.put("token", accessToken.getTokenValue());
|
||||
// 设置租户
|
||||
JSONObject jsonObject = commonAPI.setLoginTenant(loginUser.getUsername());
|
||||
addition.putAll(jsonObject.getInnerMap());
|
||||
|
||||
// 设置登录用户信息
|
||||
addition.put("userInfo", loginUser);
|
||||
addition.put("sysAllDictItems", commonAPI.queryAllDictItems());
|
||||
|
||||
List<SysDepartModel> departs = commonAPI.queryUserDeparts(loginUser.getId());
|
||||
addition.put("departs", departs);
|
||||
if (departs == null || departs.size() == 0) {
|
||||
addition.put("multi_depart", 0);
|
||||
} else if (departs.size() == 1) {
|
||||
commonAPI.updateUserDepart(loginUser.getUsername(), departs.get(0).getOrgCode(),null);
|
||||
addition.put("multi_depart", 1);
|
||||
} else {
|
||||
//查询当前是否有登录部门
|
||||
if(oConvertUtils.isEmpty(loginUser.getOrgCode())){
|
||||
commonAPI.updateUserDepart(loginUser.getUsername(), departs.get(0).getOrgCode(),null);
|
||||
}
|
||||
addition.put("multi_depart", 2);
|
||||
}
|
||||
|
||||
// 兼容原有shiro登录结果处理
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("result", addition);
|
||||
map.put("code", 200);
|
||||
map.put("success", true);
|
||||
map.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
// 返回access_token、refresh_token以及其它信息给到前端
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, map);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return PhoneGrantAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
|
||||
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
|
||||
}
|
||||
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
|
||||
return clientPrincipal;
|
||||
}
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录失败超出次数5 返回true
|
||||
* @param username
|
||||
* @return
|
||||
*/
|
||||
private boolean isLoginFailOvertimes(String username){
|
||||
String key = CommonConstant.LOGIN_FAIL + username;
|
||||
Object failTime = redisUtil.get(key);
|
||||
if(failTime!=null){
|
||||
Integer val = Integer.parseInt(failTime.toString());
|
||||
if(val>5){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录失败次数
|
||||
* @param username
|
||||
*/
|
||||
private void addLoginFailOvertimes(String username){
|
||||
String key = CommonConstant.LOGIN_FAIL + username;
|
||||
Object failTime = redisUtil.get(key);
|
||||
Integer val = 0;
|
||||
if(failTime!=null){
|
||||
val = Integer.parseInt(failTime.toString());
|
||||
}
|
||||
// 10分钟
|
||||
redisUtil.set(key, ++val, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验用户是否有效
|
||||
*/
|
||||
private void checkUserIsEffective(LoginUser loginUser) {
|
||||
//情况1:根据用户信息查询,该用户不存在
|
||||
if (Objects.isNull(loginUser)) {
|
||||
baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null);
|
||||
throw new JeecgBootException("该用户不存在,请注册");
|
||||
}
|
||||
//情况2:根据用户信息查询,该用户已注销
|
||||
//update-begin---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
|
||||
//update-end---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null);
|
||||
throw new JeecgBootException("该用户已注销");
|
||||
}
|
||||
//情况3:根据用户信息查询,该用户已冻结
|
||||
if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
|
||||
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null);
|
||||
throw new JeecgBootException("该用户已冻结");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package org.jeecg.config.security.phone;
|
||||
|
||||
import org.jeecg.config.security.LoginType;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 手机号模式认证专用token类型,方法spring authorization server进行认证流转,配合convert使用
|
||||
* @author EightMonth
|
||||
* @date 2024/1/1
|
||||
*/
|
||||
public class PhoneGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||
|
||||
public PhoneGrantAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) {
|
||||
super(new AuthorizationGrantType(LoginType.PHONE), clientPrincipal, additionalParameters);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,228 @@
|
||||
package org.jeecg.config.security.self;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.jeecg.common.api.CommonAPI;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.system.vo.SysDepartModel;
|
||||
import org.jeecg.common.util.RedisUtil;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.modules.base.service.BaseCommonService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.*;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 自用生成token处理器,不对外开放,外部请求无法通过该方式生成token
|
||||
* @author eightmonth@qq.com
|
||||
* @date 2024/3/19 11:40
|
||||
*/
|
||||
@Component
|
||||
public class SelfAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||
@Autowired
|
||||
private CommonAPI commonAPI;
|
||||
@Autowired
|
||||
private RedisUtil redisUtil;
|
||||
@Autowired
|
||||
private JeecgBaseConfig jeecgBaseConfig;
|
||||
@Autowired
|
||||
private BaseCommonService baseCommonService;
|
||||
|
||||
public SelfAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||
this.authorizationService = authorizationService;
|
||||
this.tokenGenerator = tokenGenerator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
SelfAuthenticationToken passwordGrantAuthenticationToken = (SelfAuthenticationToken) authentication;
|
||||
Map<String, Object> additionalParameter = passwordGrantAuthenticationToken.getAdditionalParameters();
|
||||
|
||||
// 授权类型
|
||||
AuthorizationGrantType authorizationGrantType = passwordGrantAuthenticationToken.getGrantType();
|
||||
// 用户名
|
||||
String username = (String) additionalParameter.get(OAuth2ParameterNames.USERNAME);
|
||||
//请求参数权限范围
|
||||
String requestScopesStr = "*";
|
||||
//请求参数权限范围专场集合
|
||||
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
|
||||
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(passwordGrantAuthenticationToken);
|
||||
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||
|
||||
if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "非法登录");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
}
|
||||
|
||||
// 通过用户名获取用户信息
|
||||
LoginUser loginUser = commonAPI.getUserByName(username);
|
||||
// 检查用户可行性
|
||||
checkUserIsEffective(loginUser);
|
||||
|
||||
//由于在上面已验证过用户名、密码,现在构建一个已认证的对象UsernamePasswordAuthenticationToken
|
||||
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(loginUser,clientPrincipal,new ArrayList<>());
|
||||
|
||||
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||
.registeredClient(registeredClient)
|
||||
.principal(usernamePasswordAuthenticationToken)
|
||||
.authorizationGrantType(authorizationGrantType)
|
||||
.authorizedScopes(requestScopeSet)
|
||||
.authorizationGrant(passwordGrantAuthenticationToken);
|
||||
|
||||
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||
.principalName(clientPrincipal.getName())
|
||||
.authorizedScopes(requestScopeSet)
|
||||
.attribute(Principal.class.getName(), username)
|
||||
.authorizationGrantType(authorizationGrantType);
|
||||
|
||||
|
||||
// ----- Access token -----
|
||||
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
|
||||
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (generatedAccessToken == null) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "无法生成刷新token,请联系管理员。");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
|
||||
}
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||
if (generatedAccessToken instanceof ClaimAccessor) {
|
||||
authorizationBuilder.token(accessToken, (metadata) -> {
|
||||
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
|
||||
});
|
||||
} else {
|
||||
authorizationBuilder.accessToken(accessToken);
|
||||
}
|
||||
|
||||
// ----- Refresh token -----
|
||||
OAuth2RefreshToken refreshToken = null;
|
||||
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
|
||||
// 不向公共客户端颁发刷新令牌
|
||||
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
|
||||
|
||||
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
|
||||
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "无法生成刷新token,请联系管理员。");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
}
|
||||
|
||||
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
|
||||
authorizationBuilder.refreshToken(refreshToken);
|
||||
}
|
||||
|
||||
OAuth2Authorization authorization = authorizationBuilder.build();
|
||||
|
||||
// 保存认证信息至redis
|
||||
authorizationService.save(authorization);
|
||||
|
||||
JSONObject addition = new JSONObject(new LinkedHashMap<>());
|
||||
addition.put("token", accessToken.getTokenValue());
|
||||
// 设置租户
|
||||
JSONObject jsonObject = commonAPI.setLoginTenant(username);
|
||||
addition.putAll(jsonObject.getInnerMap());
|
||||
|
||||
// 设置登录用户信息
|
||||
addition.put("userInfo", loginUser);
|
||||
addition.put("sysAllDictItems", commonAPI.queryAllDictItems());
|
||||
|
||||
List<SysDepartModel> departs = commonAPI.queryUserDeparts(loginUser.getId());
|
||||
addition.put("departs", departs);
|
||||
if (departs == null || departs.size() == 0) {
|
||||
addition.put("multi_depart", 0);
|
||||
} else if (departs.size() == 1) {
|
||||
commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null);
|
||||
addition.put("multi_depart", 1);
|
||||
} else {
|
||||
//查询当前是否有登录部门
|
||||
if(oConvertUtils.isEmpty(loginUser.getOrgCode())){
|
||||
commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null);
|
||||
}
|
||||
addition.put("multi_depart", 2);
|
||||
}
|
||||
|
||||
// 兼容原有shiro登录结果处理
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("result", addition);
|
||||
map.put("code", 200);
|
||||
map.put("success", true);
|
||||
map.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
// 返回access_token、refresh_token以及其它信息给到前端
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, map);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return SelfAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
|
||||
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
|
||||
}
|
||||
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
|
||||
return clientPrincipal;
|
||||
}
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验用户是否有效
|
||||
*/
|
||||
private void checkUserIsEffective(LoginUser loginUser) {
|
||||
//情况1:根据用户信息查询,该用户不存在
|
||||
if (Objects.isNull(loginUser)) {
|
||||
baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null);
|
||||
throw new JeecgBootException("该用户不存在,请注册");
|
||||
}
|
||||
//情况2:根据用户信息查询,该用户已注销
|
||||
//update-begin---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
|
||||
//update-end---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null);
|
||||
throw new JeecgBootException("该用户已注销");
|
||||
}
|
||||
//情况3:根据用户信息查询,该用户已冻结
|
||||
if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
|
||||
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null);
|
||||
throw new JeecgBootException("该用户已冻结");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package org.jeecg.config.security.self;
|
||||
|
||||
import org.jeecg.config.security.LoginType;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 自用生成token,不支持对外请求,仅为程序内部生成token
|
||||
* @author eightmonth
|
||||
* @date 2024/3/19 11:37
|
||||
*/
|
||||
public class SelfAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||
public SelfAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) {
|
||||
super(new AuthorizationGrantType(LoginType.SELF), clientPrincipal, additionalParameters);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
package org.jeecg.config.security.social;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.jeecg.config.security.LoginType;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 社交模式认证转换器,配合github、企业微信、钉钉、微信登录使用
|
||||
* @author EightMonth
|
||||
* @date 2024/1/1
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class SocialGrantAuthenticationConvert implements AuthenticationConverter {
|
||||
@Override
|
||||
public Authentication convert(HttpServletRequest request) {
|
||||
|
||||
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
|
||||
if (!LoginType.SOCIAL.equals(grantType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
//从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||
MultiValueMap<String, String> parameters = getParameters(request);
|
||||
|
||||
String token = parameters.getFirst("token");
|
||||
if (!StringUtils.hasText(token)) {
|
||||
throw new OAuth2AuthenticationException("无效请求,三方token不能为空!");
|
||||
}
|
||||
|
||||
String source = parameters.getFirst("thirdType");
|
||||
if (!StringUtils.hasText(source)) {
|
||||
throw new OAuth2AuthenticationException("无效请求,三方来源不能为空!");
|
||||
}
|
||||
|
||||
//收集要传入PhoneGrantAuthenticationToken构造方法的参数,
|
||||
//该参数接下来在PhoneGrantAuthenticationProvider中使用
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
//遍历从request中提取的参数,排除掉grant_type、client_id、code等字段参数,其他参数收集到additionalParameters中
|
||||
parameters.forEach((key, value) -> {
|
||||
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
|
||||
!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
|
||||
!key.equals(OAuth2ParameterNames.CODE)) {
|
||||
additionalParameters.put(key, value.get(0));
|
||||
}
|
||||
});
|
||||
|
||||
//返回自定义的PhoneGrantAuthenticationToken对象
|
||||
return new SocialGrantAuthenticationToken(clientPrincipal, additionalParameters);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*从request中提取请求参数,然后存入MultiValueMap<String, String>
|
||||
*/
|
||||
private static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
|
||||
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
|
||||
parameterMap.forEach((key, values) -> {
|
||||
if (values.length > 0) {
|
||||
for (String value : values) {
|
||||
parameters.add(key, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
return parameters;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,276 @@
|
||||
package org.jeecg.config.security.social;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.CommonAPI;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.system.vo.SysDepartModel;
|
||||
import org.jeecg.common.util.RedisUtil;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.config.security.password.PasswordGrantAuthenticationToken;
|
||||
import org.jeecg.modules.base.service.BaseCommonService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.*;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 社交模式认证处理器,负责处理该认证模式下的核心逻辑,配合github、企业微信、钉钉、微信登录使用
|
||||
* @author EightMonth
|
||||
* @date 2024/1/1
|
||||
*/
|
||||
@Slf4j
|
||||
public class SocialGrantAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
|
||||
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
|
||||
@Autowired
|
||||
private CommonAPI commonAPI;
|
||||
@Autowired
|
||||
private RedisUtil redisUtil;
|
||||
@Autowired
|
||||
private JeecgBaseConfig jeecgBaseConfig;
|
||||
@Autowired
|
||||
private BaseCommonService baseCommonService;
|
||||
|
||||
public SocialGrantAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
|
||||
Assert.notNull(authorizationService, "authorizationService cannot be null");
|
||||
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
|
||||
this.authorizationService = authorizationService;
|
||||
this.tokenGenerator = tokenGenerator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
SocialGrantAuthenticationToken socialGrantAuthenticationToken = (SocialGrantAuthenticationToken) authentication;
|
||||
Map<String, Object> additionalParameter = socialGrantAuthenticationToken.getAdditionalParameters();
|
||||
|
||||
// 授权类型
|
||||
AuthorizationGrantType authorizationGrantType = socialGrantAuthenticationToken.getGrantType();
|
||||
// 三方token
|
||||
String token = (String) additionalParameter.get("token");
|
||||
// 三方来源
|
||||
String source = (String) additionalParameter.get("thirdType");
|
||||
|
||||
//请求参数权限范围
|
||||
String requestScopesStr = (String)additionalParameter.getOrDefault(OAuth2ParameterNames.SCOPE, "*");
|
||||
//请求参数权限范围专场集合
|
||||
Set<String> requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet());
|
||||
|
||||
DecodedJWT jwt = JWT.decode(token);
|
||||
String username = jwt.getClaim("username").asString();
|
||||
|
||||
// 通过手机号获取用户信息
|
||||
LoginUser loginUser = commonAPI.getUserByName(username);
|
||||
// 检查用户可行性
|
||||
checkUserIsEffective(loginUser);
|
||||
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(socialGrantAuthenticationToken);
|
||||
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||
|
||||
if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "非法登录");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
|
||||
}
|
||||
|
||||
//由于在上面已验证过用户名、密码,现在构建一个已认证的对象UsernamePasswordAuthenticationToken
|
||||
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(loginUser,clientPrincipal,new ArrayList<>());
|
||||
|
||||
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
|
||||
.registeredClient(registeredClient)
|
||||
.principal(usernamePasswordAuthenticationToken)
|
||||
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
|
||||
.authorizationGrantType(authorizationGrantType)
|
||||
.authorizedScopes(requestScopeSet)
|
||||
.authorizationGrant(socialGrantAuthenticationToken);
|
||||
|
||||
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||
.principalName(clientPrincipal.getName())
|
||||
.authorizedScopes(requestScopeSet)
|
||||
.attribute(Principal.class.getName(), loginUser.getUsername())
|
||||
.authorizationGrantType(authorizationGrantType);
|
||||
|
||||
|
||||
// ----- Access token -----
|
||||
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
|
||||
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (generatedAccessToken == null) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "无法生成访问token,请联系管理系。");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
|
||||
}
|
||||
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
|
||||
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
|
||||
if (generatedAccessToken instanceof ClaimAccessor) {
|
||||
authorizationBuilder.token(accessToken, (metadata) -> {
|
||||
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
|
||||
});
|
||||
} else {
|
||||
authorizationBuilder.accessToken(accessToken);
|
||||
}
|
||||
|
||||
// ----- Refresh token -----
|
||||
OAuth2RefreshToken refreshToken = null;
|
||||
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
|
||||
// 不向公共客户端颁发刷新令牌
|
||||
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
|
||||
|
||||
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
|
||||
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
|
||||
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("message", "无法生成刷新token,请联系管理员。");
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,"fdsafas", Instant.now(), Instant.now().plusNanos(1)), null, map);
|
||||
}
|
||||
|
||||
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
|
||||
authorizationBuilder.refreshToken(refreshToken);
|
||||
}
|
||||
|
||||
OAuth2Authorization authorization = authorizationBuilder.build();
|
||||
|
||||
// 保存认证信息至redis
|
||||
authorizationService.save(authorization);
|
||||
|
||||
baseCommonService.addLog("用户名: " + loginUser.getUsername() + ",登录成功!", CommonConstant.LOG_TYPE_1, null,loginUser);
|
||||
|
||||
JSONObject addition = new JSONObject(new LinkedHashMap<>());
|
||||
addition.put("token", accessToken.getTokenValue());
|
||||
// 设置租户
|
||||
JSONObject jsonObject = commonAPI.setLoginTenant(loginUser.getUsername());
|
||||
addition.putAll(jsonObject.getInnerMap());
|
||||
|
||||
// 设置登录用户信息
|
||||
addition.put("userInfo", loginUser);
|
||||
addition.put("sysAllDictItems", commonAPI.queryAllDictItems());
|
||||
|
||||
List<SysDepartModel> departs = commonAPI.queryUserDeparts(loginUser.getId());
|
||||
addition.put("departs", departs);
|
||||
if (departs == null || departs.size() == 0) {
|
||||
addition.put("multi_depart", 0);
|
||||
} else if (departs.size() == 1) {
|
||||
commonAPI.updateUserDepart(loginUser.getUsername(), departs.get(0).getOrgCode(),null);
|
||||
addition.put("multi_depart", 1);
|
||||
} else {
|
||||
//查询当前是否有登录部门
|
||||
if(oConvertUtils.isEmpty(loginUser.getOrgCode())){
|
||||
commonAPI.updateUserDepart(loginUser.getUsername(), departs.get(0).getOrgCode(),null);
|
||||
}
|
||||
addition.put("multi_depart", 2);
|
||||
}
|
||||
|
||||
// 兼容原有shiro登录结果处理
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("result", addition);
|
||||
map.put("code", 200);
|
||||
map.put("success", true);
|
||||
map.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
|
||||
// 返回access_token、refresh_token以及其它信息给到前端
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, map);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return SocialGrantAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = null;
|
||||
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
|
||||
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
|
||||
}
|
||||
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
|
||||
return clientPrincipal;
|
||||
}
|
||||
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录失败超出次数5 返回true
|
||||
* @param username
|
||||
* @return
|
||||
*/
|
||||
private boolean isLoginFailOvertimes(String username){
|
||||
String key = CommonConstant.LOGIN_FAIL + username;
|
||||
Object failTime = redisUtil.get(key);
|
||||
if(failTime!=null){
|
||||
Integer val = Integer.parseInt(failTime.toString());
|
||||
if(val>5){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录失败次数
|
||||
* @param username
|
||||
*/
|
||||
private void addLoginFailOvertimes(String username){
|
||||
String key = CommonConstant.LOGIN_FAIL + username;
|
||||
Object failTime = redisUtil.get(key);
|
||||
Integer val = 0;
|
||||
if(failTime!=null){
|
||||
val = Integer.parseInt(failTime.toString());
|
||||
}
|
||||
// 10分钟
|
||||
redisUtil.set(key, ++val, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验用户是否有效
|
||||
*/
|
||||
private void checkUserIsEffective(LoginUser loginUser) {
|
||||
//情况1:根据用户信息查询,该用户不存在
|
||||
if (Objects.isNull(loginUser)) {
|
||||
baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null);
|
||||
throw new JeecgBootException("该用户不存在,请注册");
|
||||
}
|
||||
//情况2:根据用户信息查询,该用户已注销
|
||||
//update-begin---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||
if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) {
|
||||
//update-end---author:王帅 Date:20200601 for:if条件永远为falsebug------------
|
||||
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null);
|
||||
throw new JeecgBootException("该用户已注销");
|
||||
}
|
||||
//情况3:根据用户信息查询,该用户已冻结
|
||||
if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) {
|
||||
baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null);
|
||||
throw new JeecgBootException("该用户已冻结");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package org.jeecg.config.security.social;
|
||||
|
||||
import org.jeecg.config.security.LoginType;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 社交模式认证专用token类型,方法spring authorization server进行认证流转,配合convert使用,配合github、企业微信、钉钉、微信登录使用
|
||||
* @author EightMonth
|
||||
* @date 2024/1/1
|
||||
*/
|
||||
public class SocialGrantAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
|
||||
|
||||
public SocialGrantAuthenticationToken(Authentication clientPrincipal, Map<String, Object> additionalParameters) {
|
||||
super(new AuthorizationGrantType(LoginType.SOCIAL), clientPrincipal, additionalParameters);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package org.jeecg.config.security.utils;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
/**
|
||||
* 认证信息工具类
|
||||
* @author EightMonth
|
||||
* @date 2024/1/10 17:03
|
||||
*/
|
||||
public class SecureUtil {
|
||||
|
||||
/**
|
||||
* 通过当前认证信息获取用户信息
|
||||
* @return
|
||||
*/
|
||||
public static LoginUser currentUser() {
|
||||
String name = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||
return JSONObject.parseObject(name, LoginUser.class);
|
||||
}
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
package org.jeecg.config.shiro;
|
||||
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
|
||||
/**
|
||||
* @Author Scott
|
||||
* @create 2018-07-12 15:19
|
||||
* @desc
|
||||
**/
|
||||
public class JwtToken implements AuthenticationToken {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private String token;
|
||||
|
||||
public JwtToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
@ -1,306 +0,0 @@
|
||||
package org.jeecg.config.shiro;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
||||
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
|
||||
import org.apache.shiro.mgt.DefaultSubjectDAO;
|
||||
import org.apache.shiro.mgt.SecurityManager;
|
||||
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
|
||||
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
|
||||
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
|
||||
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
|
||||
import org.crazycake.shiro.*;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.config.shiro.filters.CustomShiroFilterFactoryBean;
|
||||
import org.jeecg.config.shiro.filters.JwtFilter;
|
||||
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import redis.clients.jedis.HostAndPort;
|
||||
import redis.clients.jedis.JedisCluster;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.Filter;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author: Scott
|
||||
* @date: 2018/2/7
|
||||
* @description: shiro 配置类
|
||||
*/
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class ShiroConfig {
|
||||
|
||||
@Resource
|
||||
private LettuceConnectionFactory lettuceConnectionFactory;
|
||||
@Autowired
|
||||
private Environment env;
|
||||
@Resource
|
||||
private JeecgBaseConfig jeecgBaseConfig;
|
||||
@Autowired(required = false)
|
||||
private RedisProperties redisProperties;
|
||||
|
||||
/**
|
||||
* Filter Chain定义说明
|
||||
*
|
||||
* 1、一个URL可以配置多个Filter,使用逗号分隔
|
||||
* 2、当设置多个过滤器时,全部验证通过,才视为通过
|
||||
* 3、部分过滤器可指定参数,如perms,roles
|
||||
*/
|
||||
@Bean("shiroFilterFactoryBean")
|
||||
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
|
||||
CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();
|
||||
shiroFilterFactoryBean.setSecurityManager(securityManager);
|
||||
// 拦截器
|
||||
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
|
||||
|
||||
//支持yml方式,配置拦截排除
|
||||
if(jeecgBaseConfig!=null && jeecgBaseConfig.getShiro()!=null){
|
||||
String shiroExcludeUrls = jeecgBaseConfig.getShiro().getExcludeUrls();
|
||||
if(oConvertUtils.isNotEmpty(shiroExcludeUrls)){
|
||||
String[] permissionUrl = shiroExcludeUrls.split(",");
|
||||
for(String url : permissionUrl){
|
||||
filterChainDefinitionMap.put(url,"anon");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 配置不会被拦截的链接 顺序判断
|
||||
filterChainDefinitionMap.put("/sys/cas/client/validateLogin", "anon"); //cas验证登录
|
||||
filterChainDefinitionMap.put("/sys/randomImage/**", "anon"); //登录验证码接口排除
|
||||
filterChainDefinitionMap.put("/sys/checkCaptcha", "anon"); //登录验证码接口排除
|
||||
filterChainDefinitionMap.put("/sys/login", "anon"); //登录接口排除
|
||||
filterChainDefinitionMap.put("/sys/mLogin", "anon"); //登录接口排除
|
||||
filterChainDefinitionMap.put("/sys/logout", "anon"); //登出接口排除
|
||||
filterChainDefinitionMap.put("/sys/thirdLogin/**", "anon"); //第三方登录
|
||||
filterChainDefinitionMap.put("/sys/getEncryptedString", "anon"); //获取加密串
|
||||
filterChainDefinitionMap.put("/sys/sms", "anon");//短信验证码
|
||||
filterChainDefinitionMap.put("/sys/phoneLogin", "anon");//手机登录
|
||||
filterChainDefinitionMap.put("/sys/user/checkOnlyUser", "anon");//校验用户是否存在
|
||||
filterChainDefinitionMap.put("/sys/user/register", "anon");//用户注册
|
||||
filterChainDefinitionMap.put("/sys/user/phoneVerification", "anon");//用户忘记密码验证手机号
|
||||
filterChainDefinitionMap.put("/sys/user/passwordChange", "anon");//用户更改密码
|
||||
filterChainDefinitionMap.put("/auth/2step-code", "anon");//登录验证码
|
||||
filterChainDefinitionMap.put("/sys/common/static/**", "anon");//图片预览 &下载文件不限制token
|
||||
filterChainDefinitionMap.put("/sys/common/pdf/**", "anon");//pdf预览
|
||||
|
||||
//filterChainDefinitionMap.put("/sys/common/view/**", "anon");//图片预览不限制token
|
||||
//filterChainDefinitionMap.put("/sys/common/download/**", "anon");//文件下载不限制token
|
||||
filterChainDefinitionMap.put("/generic/**", "anon");//pdf预览需要文件
|
||||
|
||||
filterChainDefinitionMap.put("/sys/getLoginQrcode/**", "anon"); //登录二维码
|
||||
filterChainDefinitionMap.put("/sys/getQrcodeToken/**", "anon"); //监听扫码
|
||||
filterChainDefinitionMap.put("/sys/checkAuth", "anon"); //授权接口排除
|
||||
|
||||
|
||||
//update-begin--Author:scott Date:20221116 for:排除静态资源后缀
|
||||
filterChainDefinitionMap.put("/", "anon");
|
||||
filterChainDefinitionMap.put("/doc.html", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.js", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.css", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.html", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.svg", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.pdf", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.jpg", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.png", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.gif", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.ico", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.ttf", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.woff", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.woff2", "anon");
|
||||
//update-end--Author:scott Date:20221116 for:排除静态资源后缀
|
||||
|
||||
filterChainDefinitionMap.put("/druid/**", "anon");
|
||||
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
|
||||
filterChainDefinitionMap.put("/swagger**/**", "anon");
|
||||
filterChainDefinitionMap.put("/webjars/**", "anon");
|
||||
filterChainDefinitionMap.put("/v2/**", "anon");
|
||||
|
||||
// update-begin--Author:sunjianlei Date:20210510 for:排除消息通告查看详情页面(用于第三方APP)
|
||||
filterChainDefinitionMap.put("/sys/annountCement/show/**", "anon");
|
||||
// update-end--Author:sunjianlei Date:20210510 for:排除消息通告查看详情页面(用于第三方APP)
|
||||
|
||||
//积木报表排除
|
||||
filterChainDefinitionMap.put("/jmreport/**", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.js.map", "anon");
|
||||
filterChainDefinitionMap.put("/**/*.css.map", "anon");
|
||||
|
||||
//拖拽仪表盘设计器排除
|
||||
filterChainDefinitionMap.put("/drag/view", "anon");
|
||||
filterChainDefinitionMap.put("/drag/page/queryById", "anon");
|
||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getAllChartData", "anon");
|
||||
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalData", "anon");
|
||||
filterChainDefinitionMap.put("/drag/mock/json/**", "anon");
|
||||
//大屏模板例子
|
||||
filterChainDefinitionMap.put("/test/bigScreen/**", "anon");
|
||||
filterChainDefinitionMap.put("/bigscreen/template1/**", "anon");
|
||||
filterChainDefinitionMap.put("/bigscreen/template1/**", "anon");
|
||||
//filterChainDefinitionMap.put("/test/jeecgDemo/rabbitMqClientTest/**", "anon"); //MQ测试
|
||||
//filterChainDefinitionMap.put("/test/jeecgDemo/html", "anon"); //模板页面
|
||||
//filterChainDefinitionMap.put("/test/jeecgDemo/redis/**", "anon"); //redis测试
|
||||
|
||||
//websocket排除
|
||||
filterChainDefinitionMap.put("/websocket/**", "anon");//系统通知和公告
|
||||
filterChainDefinitionMap.put("/newsWebsocket/**", "anon");//CMS模块
|
||||
filterChainDefinitionMap.put("/vxeSocket/**", "anon");//JVxeTable无痕刷新示例
|
||||
|
||||
//性能监控——安全隐患泄露TOEKN(durid连接池也有)
|
||||
//filterChainDefinitionMap.put("/actuator/**", "anon");
|
||||
//测试模块排除
|
||||
filterChainDefinitionMap.put("/test/seata/**", "anon");
|
||||
|
||||
//错误路径排除
|
||||
filterChainDefinitionMap.put("/error", "anon");
|
||||
// 企业微信证书排除
|
||||
filterChainDefinitionMap.put("/WW_verify*", "anon");
|
||||
|
||||
// 添加自己的过滤器并且取名为jwt
|
||||
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
|
||||
//如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
|
||||
Object cloudServer = env.getProperty(CommonConstant.CLOUD_SERVER_KEY);
|
||||
filterMap.put("jwt", new JwtFilter(cloudServer==null));
|
||||
shiroFilterFactoryBean.setFilters(filterMap);
|
||||
// <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
|
||||
filterChainDefinitionMap.put("/**", "jwt");
|
||||
|
||||
// 未授权界面返回JSON
|
||||
shiroFilterFactoryBean.setUnauthorizedUrl("/sys/common/403");
|
||||
shiroFilterFactoryBean.setLoginUrl("/sys/common/403");
|
||||
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
|
||||
return shiroFilterFactoryBean;
|
||||
}
|
||||
|
||||
@Bean("securityManager")
|
||||
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
|
||||
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
|
||||
securityManager.setRealm(myRealm);
|
||||
|
||||
/*
|
||||
* 关闭shiro自带的session,详情见文档
|
||||
* http://shiro.apache.org/session-management.html#SessionManagement-
|
||||
* StatelessApplications%28Sessionless%29
|
||||
*/
|
||||
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
|
||||
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
|
||||
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
|
||||
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
|
||||
securityManager.setSubjectDAO(subjectDAO);
|
||||
//自定义缓存实现,使用redis
|
||||
securityManager.setCacheManager(redisCacheManager());
|
||||
return securityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下面的代码是添加注解支持
|
||||
* @return
|
||||
*/
|
||||
@Bean
|
||||
@DependsOn("lifecycleBeanPostProcessor")
|
||||
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
|
||||
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
|
||||
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
|
||||
/**
|
||||
* 解决重复代理问题 github#994
|
||||
* 添加前缀判断 不匹配 任何Advisor
|
||||
*/
|
||||
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
|
||||
defaultAdvisorAutoProxyCreator.setAdvisorBeanNamePrefix("_no_advisor");
|
||||
return defaultAdvisorAutoProxyCreator;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
|
||||
return new LifecycleBeanPostProcessor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
|
||||
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
|
||||
advisor.setSecurityManager(securityManager);
|
||||
return advisor;
|
||||
}
|
||||
|
||||
/**
|
||||
* cacheManager 缓存 redis实现
|
||||
* 使用的是shiro-redis开源插件
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public RedisCacheManager redisCacheManager() {
|
||||
log.info("===============(1)创建缓存管理器RedisCacheManager");
|
||||
RedisCacheManager redisCacheManager = new RedisCacheManager();
|
||||
redisCacheManager.setRedisManager(redisManager());
|
||||
//redis中针对不同用户缓存(此处的id需要对应user实体中的id字段,用于唯一标识)
|
||||
redisCacheManager.setPrincipalIdFieldName("id");
|
||||
//用户权限信息缓存时间
|
||||
redisCacheManager.setExpire(200000);
|
||||
return redisCacheManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置shiro redisManager
|
||||
* 使用的是shiro-redis开源插件
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Bean
|
||||
public IRedisManager redisManager() {
|
||||
log.info("===============(2)创建RedisManager,连接Redis..");
|
||||
IRedisManager manager;
|
||||
// sentinel cluster redis(【issues/5569】shiro集成 redis 不支持 sentinel 方式部署的redis集群 #5569)
|
||||
if (Objects.nonNull(redisProperties)
|
||||
&& Objects.nonNull(redisProperties.getSentinel())
|
||||
&& !CollectionUtils.isEmpty(redisProperties.getSentinel().getNodes())) {
|
||||
RedisSentinelManager sentinelManager = new RedisSentinelManager();
|
||||
sentinelManager.setMasterName(redisProperties.getSentinel().getMaster());
|
||||
sentinelManager.setHost(String.join(",", redisProperties.getSentinel().getNodes()));
|
||||
sentinelManager.setPassword(redisProperties.getSentinel().getPassword());
|
||||
sentinelManager.setDatabase(redisProperties.getDatabase());
|
||||
|
||||
return sentinelManager;
|
||||
}
|
||||
|
||||
// redis 单机支持,在集群为空,或者集群无机器时候使用 add by jzyadmin@163.com
|
||||
if (lettuceConnectionFactory.getClusterConfiguration() == null || lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().isEmpty()) {
|
||||
RedisManager redisManager = new RedisManager();
|
||||
redisManager.setHost(lettuceConnectionFactory.getHostName() + ":" + lettuceConnectionFactory.getPort());
|
||||
//(lettuceConnectionFactory.getPort());
|
||||
redisManager.setDatabase(lettuceConnectionFactory.getDatabase());
|
||||
redisManager.setTimeout(0);
|
||||
if (!StringUtils.isEmpty(lettuceConnectionFactory.getPassword())) {
|
||||
redisManager.setPassword(lettuceConnectionFactory.getPassword());
|
||||
}
|
||||
manager = redisManager;
|
||||
}else{
|
||||
// redis集群支持,优先使用集群配置
|
||||
RedisClusterManager redisManager = new RedisClusterManager();
|
||||
Set<HostAndPort> portSet = new HashSet<>();
|
||||
lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().forEach(node -> portSet.add(new HostAndPort(node.getHost() , node.getPort())));
|
||||
//update-begin--Author:scott Date:20210531 for:修改集群模式下未设置redis密码的bug issues/I3QNIC
|
||||
if (oConvertUtils.isNotEmpty(lettuceConnectionFactory.getPassword())) {
|
||||
JedisCluster jedisCluster = new JedisCluster(portSet, 2000, 2000, 5,
|
||||
lettuceConnectionFactory.getPassword(), new GenericObjectPoolConfig());
|
||||
redisManager.setPassword(lettuceConnectionFactory.getPassword());
|
||||
redisManager.setJedisCluster(jedisCluster);
|
||||
} else {
|
||||
JedisCluster jedisCluster = new JedisCluster(portSet);
|
||||
redisManager.setJedisCluster(jedisCluster);
|
||||
}
|
||||
//update-end--Author:scott Date:20210531 for:修改集群模式下未设置redis密码的bug issues/I3QNIC
|
||||
manager = redisManager;
|
||||
}
|
||||
return manager;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,229 +0,0 @@
|
||||
package org.jeecg.config.shiro;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authc.AuthenticationException;
|
||||
import org.apache.shiro.authc.AuthenticationInfo;
|
||||
import org.apache.shiro.authc.AuthenticationToken;
|
||||
import org.apache.shiro.authc.SimpleAuthenticationInfo;
|
||||
import org.apache.shiro.authz.AuthorizationInfo;
|
||||
import org.apache.shiro.authz.SimpleAuthorizationInfo;
|
||||
import org.apache.shiro.realm.AuthorizingRealm;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.jeecg.common.api.CommonAPI;
|
||||
import org.jeecg.common.config.TenantContext;
|
||||
import org.jeecg.common.constant.CacheConstant;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.system.util.JwtUtil;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.RedisUtil;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @Description: 用户登录鉴权和获取用户授权
|
||||
* @Author: Scott
|
||||
* @Date: 2019-4-23 8:13
|
||||
* @Version: 1.1
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class ShiroRealm extends AuthorizingRealm {
|
||||
@Lazy
|
||||
@Resource
|
||||
private CommonAPI commonApi;
|
||||
|
||||
@Lazy
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
/**
|
||||
* 必须重写此方法,不然Shiro会报错
|
||||
*/
|
||||
@Override
|
||||
public boolean supports(AuthenticationToken token) {
|
||||
return token instanceof JwtToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限信息认证(包括角色以及权限)是用户访问controller的时候才进行验证(redis存储的此处权限信息)
|
||||
* 触发检测用户权限时才会调用此方法,例如checkRole,checkPermission
|
||||
*
|
||||
* @param principals 身份信息
|
||||
* @return AuthorizationInfo 权限信息
|
||||
*/
|
||||
@Override
|
||||
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
|
||||
log.debug("===============Shiro权限认证开始============ [ roles、permissions]==========");
|
||||
String username = null;
|
||||
if (principals != null) {
|
||||
LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();
|
||||
username = sysUser.getUsername();
|
||||
}
|
||||
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
|
||||
|
||||
// 设置用户拥有的角色集合,比如“admin,test”
|
||||
Set<String> roleSet = commonApi.queryUserRoles(username);
|
||||
//System.out.println(roleSet.toString());
|
||||
info.setRoles(roleSet);
|
||||
|
||||
// 设置用户拥有的权限集合,比如“sys:role:add,sys:user:add”
|
||||
Set<String> permissionSet = commonApi.queryUserAuths(username);
|
||||
info.addStringPermissions(permissionSet);
|
||||
//System.out.println(permissionSet);
|
||||
log.info("===============Shiro权限认证成功==============");
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息认证是在用户进行登录的时候进行验证(不存redis)
|
||||
* 也就是说验证用户输入的账号和密码是否正确,错误抛出异常
|
||||
*
|
||||
* @param auth 用户登录的账号密码信息
|
||||
* @return 返回封装了用户信息的 AuthenticationInfo 实例
|
||||
* @throws AuthenticationException
|
||||
*/
|
||||
@Override
|
||||
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
|
||||
log.debug("===============Shiro身份认证开始============doGetAuthenticationInfo==========");
|
||||
String token = (String) auth.getCredentials();
|
||||
if (token == null) {
|
||||
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
|
||||
log.info("————————身份认证失败——————————IP地址: "+ oConvertUtils.getIpAddrByRequest(req) +",URL:"+req.getRequestURI());
|
||||
throw new AuthenticationException("token为空!");
|
||||
}
|
||||
// 校验token有效性
|
||||
LoginUser loginUser = null;
|
||||
try {
|
||||
loginUser = this.checkUserTokenIsEffect(token);
|
||||
} catch (AuthenticationException e) {
|
||||
JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
return new SimpleAuthenticationInfo(loginUser, token, getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验token的有效性
|
||||
*
|
||||
* @param token
|
||||
*/
|
||||
public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
|
||||
// 解密获得username,用于和数据库进行对比
|
||||
String username = JwtUtil.getUsername(token);
|
||||
if (username == null) {
|
||||
throw new AuthenticationException("token非法无效!");
|
||||
}
|
||||
|
||||
// 查询用户信息
|
||||
log.debug("———校验token是否有效————checkUserTokenIsEffect——————— "+ token);
|
||||
LoginUser loginUser = TokenUtils.getLoginUser(username, commonApi, redisUtil);
|
||||
//LoginUser loginUser = commonApi.getUserByName(username);
|
||||
if (loginUser == null) {
|
||||
throw new AuthenticationException("用户不存在!");
|
||||
}
|
||||
// 判断用户状态
|
||||
if (loginUser.getStatus() != 1) {
|
||||
throw new AuthenticationException("账号已被锁定,请联系管理员!");
|
||||
}
|
||||
// 校验token是否超时失效 & 或者账号密码是否错误
|
||||
if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
|
||||
throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);
|
||||
}
|
||||
//update-begin-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
|
||||
String userTenantIds = loginUser.getRelTenantIds();
|
||||
if(oConvertUtils.isNotEmpty(userTenantIds)){
|
||||
String contextTenantId = TenantContext.getTenant();
|
||||
log.debug("登录租户:" + contextTenantId);
|
||||
log.debug("用户拥有那些租户:" + userTenantIds);
|
||||
//登录用户无租户,前端header中租户ID值为 0
|
||||
String str ="0";
|
||||
if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
|
||||
//update-begin-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
|
||||
String[] arr = userTenantIds.split(",");
|
||||
if(!oConvertUtils.isIn(contextTenantId, arr)){
|
||||
boolean isAuthorization = false;
|
||||
//========================================================================
|
||||
// 查询用户信息(如果租户不匹配从数据库中重新查询一次用户信息)
|
||||
String loginUserKey = CacheConstant.SYS_USERS_CACHE + "::" + username;
|
||||
redisUtil.del(loginUserKey);
|
||||
LoginUser loginUserFromDb = commonApi.getUserByName(username);
|
||||
if (oConvertUtils.isNotEmpty(loginUserFromDb.getRelTenantIds())) {
|
||||
String[] newArray = loginUserFromDb.getRelTenantIds().split(",");
|
||||
if (oConvertUtils.isIn(contextTenantId, newArray)) {
|
||||
isAuthorization = true;
|
||||
}
|
||||
}
|
||||
//========================================================================
|
||||
|
||||
//*********************************************
|
||||
if(!isAuthorization){
|
||||
log.info("租户异常——登录租户:" + contextTenantId);
|
||||
log.info("租户异常——用户拥有租户组:" + userTenantIds);
|
||||
throw new AuthenticationException("登录租户授权变更,请重新登陆!");
|
||||
}
|
||||
//*********************************************
|
||||
}
|
||||
//update-end-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
|
||||
}
|
||||
}
|
||||
//update-end-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能)
|
||||
* 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍
|
||||
* 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
|
||||
* 3、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
|
||||
* 4、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
|
||||
* 注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。
|
||||
* 用户过期时间 = Jwt有效时间 * 2。
|
||||
*
|
||||
* @param userName
|
||||
* @param passWord
|
||||
* @return
|
||||
*/
|
||||
public boolean jwtTokenRefresh(String token, String userName, String passWord) {
|
||||
String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
|
||||
if (oConvertUtils.isNotEmpty(cacheToken)) {
|
||||
// 校验token有效性
|
||||
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
|
||||
String newAuthorization = JwtUtil.sign(userName, passWord);
|
||||
// 设置超时时间
|
||||
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
|
||||
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000);
|
||||
log.debug("——————————用户在线操作,更新token保证不掉线—————————jwtTokenRefresh——————— "+ token);
|
||||
}
|
||||
//update-begin--Author:scott Date:20191005 for:解决每次请求,都重写redis中 token缓存问题
|
||||
// else {
|
||||
// // 设置超时时间
|
||||
// redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
|
||||
// redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
|
||||
// }
|
||||
//update-end--Author:scott Date:20191005 for:解决每次请求,都重写redis中 token缓存问题
|
||||
return true;
|
||||
}
|
||||
|
||||
//redis中不存在此TOEKN,说明token非法返回false
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前用户的权限认证缓存
|
||||
*
|
||||
* @param principals 权限信息
|
||||
*/
|
||||
@Override
|
||||
public void clearCache(PrincipalCollection principals) {
|
||||
super.clearCache(principals);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,77 +0,0 @@
|
||||
package org.jeecg.config.shiro.filters;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
|
||||
import org.apache.shiro.web.filter.InvalidRequestFilter;
|
||||
import org.apache.shiro.web.filter.mgt.DefaultFilter;
|
||||
import org.apache.shiro.web.filter.mgt.FilterChainManager;
|
||||
import org.apache.shiro.web.filter.mgt.FilterChainResolver;
|
||||
import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;
|
||||
import org.apache.shiro.web.mgt.WebSecurityManager;
|
||||
import org.apache.shiro.web.servlet.AbstractShiroFilter;
|
||||
import org.apache.shiro.mgt.SecurityManager;
|
||||
import org.springframework.beans.factory.BeanInitializationException;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 自定义ShiroFilterFactoryBean解决资源中文路径问题
|
||||
* @author: jeecg-boot
|
||||
*/
|
||||
@Slf4j
|
||||
public class CustomShiroFilterFactoryBean extends ShiroFilterFactoryBean {
|
||||
@Override
|
||||
public Class getObjectType() {
|
||||
return MySpringShiroFilter.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractShiroFilter createInstance() throws Exception {
|
||||
|
||||
SecurityManager securityManager = getSecurityManager();
|
||||
if (securityManager == null) {
|
||||
String msg = "SecurityManager property must be set.";
|
||||
throw new BeanInitializationException(msg);
|
||||
}
|
||||
|
||||
if (!(securityManager instanceof WebSecurityManager)) {
|
||||
String msg = "The security manager does not implement the WebSecurityManager interface.";
|
||||
throw new BeanInitializationException(msg);
|
||||
}
|
||||
|
||||
FilterChainManager manager = createFilterChainManager();
|
||||
//Expose the constructed FilterChainManager by first wrapping it in a
|
||||
// FilterChainResolver implementation. The AbstractShiroFilter implementations
|
||||
// do not know about FilterChainManagers - only resolvers:
|
||||
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
|
||||
chainResolver.setFilterChainManager(manager);
|
||||
|
||||
Map<String, Filter> filterMap = manager.getFilters();
|
||||
Filter invalidRequestFilter = filterMap.get(DefaultFilter.invalidRequest.name());
|
||||
if (invalidRequestFilter instanceof InvalidRequestFilter) {
|
||||
//此处是关键,设置false跳过URL携带中文400,servletPath中文校验bug
|
||||
((InvalidRequestFilter) invalidRequestFilter).setBlockNonAscii(false);
|
||||
}
|
||||
//Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
|
||||
//FilterChainResolver. It doesn't matter that the instance is an anonymous inner class
|
||||
//here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
|
||||
//injection of the SecurityManager and FilterChainResolver:
|
||||
return new MySpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
|
||||
}
|
||||
|
||||
private static final class MySpringShiroFilter extends AbstractShiroFilter {
|
||||
protected MySpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
|
||||
if (webSecurityManager == null) {
|
||||
throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
|
||||
} else {
|
||||
this.setSecurityManager(webSecurityManager);
|
||||
if (resolver != null) {
|
||||
this.setFilterChainResolver(resolver);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,124 +0,0 @@
|
||||
package org.jeecg.config.shiro.filters;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
|
||||
import org.jeecg.common.config.TenantContext;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.system.util.JwtUtil;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.shiro.JwtToken;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* @Description: 鉴权登录拦截器
|
||||
* @Author: Scott
|
||||
* @Date: 2018/10/7
|
||||
**/
|
||||
@Slf4j
|
||||
public class JwtFilter extends BasicHttpAuthenticationFilter {
|
||||
|
||||
/**
|
||||
* 默认开启跨域设置(使用单体)
|
||||
* 微服务情况下,此属性设置为false
|
||||
*/
|
||||
private boolean allowOrigin = true;
|
||||
|
||||
public JwtFilter(){}
|
||||
public JwtFilter(boolean allowOrigin){
|
||||
this.allowOrigin = allowOrigin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行登录认证
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @param mappedValue
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
|
||||
try {
|
||||
executeLogin(request, response);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
JwtUtil.responseError(response,401,CommonConstant.TOKEN_IS_INVALID_MSG);
|
||||
return false;
|
||||
//throw new AuthenticationException("Token失效,请重新登录", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
|
||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||
String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
|
||||
// update-begin--Author:lvdandan Date:20210105 for:JT-355 OA聊天添加token验证,获取token参数
|
||||
if (oConvertUtils.isEmpty(token)) {
|
||||
token = httpServletRequest.getParameter("token");
|
||||
}
|
||||
// update-end--Author:lvdandan Date:20210105 for:JT-355 OA聊天添加token验证,获取token参数
|
||||
|
||||
JwtToken jwtToken = new JwtToken(token);
|
||||
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
|
||||
getSubject(request, response).login(jwtToken);
|
||||
// 如果没有抛出异常则代表登入成功,返回true
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对跨域提供支持
|
||||
*/
|
||||
@Override
|
||||
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
|
||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||
if(allowOrigin){
|
||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, httpServletRequest.getHeader(HttpHeaders.ORIGIN));
|
||||
// 允许客户端请求方法
|
||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET,POST,OPTIONS,PUT,DELETE");
|
||||
// 允许客户端提交的Header
|
||||
String requestHeaders = httpServletRequest.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
|
||||
if (StringUtils.isNotEmpty(requestHeaders)) {
|
||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders);
|
||||
}
|
||||
// 允许客户端携带凭证信息(是否允许发送Cookie)
|
||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
|
||||
}
|
||||
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
|
||||
if (RequestMethod.OPTIONS.name().equalsIgnoreCase(httpServletRequest.getMethod())) {
|
||||
httpServletResponse.setStatus(HttpStatus.OK.value());
|
||||
return false;
|
||||
}
|
||||
//update-begin-author:taoyan date:20200708 for:多租户用到
|
||||
String tenantId = httpServletRequest.getHeader(CommonConstant.TENANT_ID);
|
||||
TenantContext.setTenant(tenantId);
|
||||
//update-end-author:taoyan date:20200708 for:多租户用到
|
||||
|
||||
return super.preHandle(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* JwtFilter中ThreadLocal需要及时清除 #3634
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @param exception
|
||||
* @throws Exception
|
||||
*/
|
||||
@Override
|
||||
public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception {
|
||||
//log.info("------清空线程中多租户的ID={}------",TenantContext.getTenant());
|
||||
TenantContext.clear();
|
||||
}
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
package org.jeecg.config.shiro.filters;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.web.filter.AccessControlFilter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* @Author Scott
|
||||
* @create 2019-02-01 15:56
|
||||
* @desc 鉴权请求URL访问权限拦截器
|
||||
*/
|
||||
@Slf4j
|
||||
public class ResourceCheckFilter extends AccessControlFilter {
|
||||
|
||||
private String errorUrl;
|
||||
|
||||
public String getErrorUrl() {
|
||||
return errorUrl;
|
||||
}
|
||||
|
||||
public void setErrorUrl(String errorUrl) {
|
||||
this.errorUrl = errorUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 表示是否允许访问 ,如果允许访问返回true,否则false;
|
||||
*
|
||||
* @param servletRequest
|
||||
* @param servletResponse
|
||||
* @param o 表示写在拦截器中括号里面的字符串 mappedValue 就是 [urls] 配置中拦截器参数部分
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
@Override
|
||||
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
|
||||
Subject subject = getSubject(servletRequest, servletResponse);
|
||||
String url = getPathWithinApplication(servletRequest);
|
||||
log.info("当前用户正在访问的 url => " + url);
|
||||
return subject.isPermitted(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* onAccessDenied:表示当访问拒绝时是否已经处理了; 如果返回 true 表示需要继续处理; 如果返回 false
|
||||
* 表示该拦截器实例已经处理了,将直接返回即可。
|
||||
*
|
||||
* @param servletRequest
|
||||
* @param servletResponse
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
@Override
|
||||
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
|
||||
log.info("当 isAccessAllowed 返回 false 的时候,才会执行 method onAccessDenied ");
|
||||
|
||||
HttpServletRequest request = (HttpServletRequest) servletRequest;
|
||||
HttpServletResponse response = (HttpServletResponse) servletResponse;
|
||||
response.sendRedirect(request.getContextPath() + this.errorUrl);
|
||||
|
||||
// 返回 false 表示已经处理,例如页面跳转啥的,表示不在走以下的拦截器了(如果还有配置的话)
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@ -10,7 +10,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 签名 拦截器配置
|
||||
|
||||
@ -4,8 +4,8 @@ package org.jeecg.config.sign.interceptor;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.SortedMap;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
package org.jeecg.config.sign.util;
|
||||
|
||||
import javax.servlet.ReadListener;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import jakarta.servlet.ReadListener;
|
||||
import jakarta.servlet.ServletInputStream;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import java.io.*;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import java.util.Map;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.constant.SymbolConstant;
|
||||
|
||||
@ -35,5 +35,4 @@ public class Firewall {
|
||||
public void setLowCodeMode(String lowCodeMode) {
|
||||
this.lowCodeMode = lowCodeMode;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
package org.jeecg.config.vo;
|
||||
|
||||
/**
|
||||
* @Description: TODO
|
||||
* @author: scott
|
||||
* @date: 2022年01月21日 14:23
|
||||
*/
|
||||
public class Shiro {
|
||||
private String excludeUrls = "";
|
||||
|
||||
public String getExcludeUrls() {
|
||||
return excludeUrls;
|
||||
}
|
||||
|
||||
public void setExcludeUrls(String excludeUrls) {
|
||||
this.excludeUrls = excludeUrls;
|
||||
}
|
||||
}
|
||||
@ -2,8 +2,8 @@ package org.jeecg.modules.base.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.jeecg.common.api.dto.LogDTO;
|
||||
import org.jeecg.config.security.utils.SecureUtil;
|
||||
import org.jeecg.modules.base.mapper.BaseCommonMapper;
|
||||
import org.jeecg.modules.base.service.BaseCommonService;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
@ -12,8 +12,8 @@ import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
@ -61,7 +61,7 @@ public class BaseCommonServiceImpl implements BaseCommonService {
|
||||
//获取登录用户信息
|
||||
if(user==null){
|
||||
try {
|
||||
user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
user = SecureUtil.currentUser();
|
||||
} catch (Exception e) {
|
||||
//e.printStackTrace();
|
||||
}
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
org.springframework.boot.SpringApplicationRunListener=\
|
||||
org.jeecg.config.DruidWallConfigRegister
|
||||
@ -56,7 +56,7 @@
|
||||
</div>
|
||||
<div style="width: 600px; margin: 0 auto; margin-top: 50px; font-size: 12px; -webkit-font-smoothing: subpixel-antialiased; text-size-adjust: 100%;">
|
||||
<p style="text-align: center; line-height: 20.4px; text-size-adjust: 100%; font-family: 'Microsoft YaHei'!important; padding: 0px !important; margin: 0px !important; color: #7e8890 !important;">
|
||||
<span class="appleLinks">Copyright © 2023-2024 北京国炬信息技术有限公司. 保留所有权利。</span>
|
||||
<span class="appleLinks">Copyright © 2023-2024 北京国炬科技股份有限公司. 保留所有权利。</span>
|
||||
</p>
|
||||
<p style="text-align: center;line-height: 20.4px; text-size-adjust: 100%; font-family: 'Microsoft YaHei'!important; padding: 0px !important; margin: 0px; color: #7e8890 !important; margin-top: 10px;">
|
||||
<span class="appleLinks">邮件由系统自动发送,请勿直接回复本邮件!</span>
|
||||
|
||||
@ -6,10 +6,7 @@
|
||||
<body>
|
||||
<div class="box-content">
|
||||
<div class="info-top">
|
||||
<img src="https://qiaoqiaoyun.oss-cn-beijing.aliyuncs.com/site/qqyunemaillogo.png" style="width: 35px;height:35px; background: #5e8ee5; border-radius: 5px;" />
|
||||
<div style="color:#fff;">
|
||||
<strong>【重要】新数据提醒</strong>
|
||||
</div>
|
||||
<img src="https://jeecgdev.oss-cn-beijing.aliyuncs.com/temp/logo(1)_1697180761742.png" style="float: left; margin: 0 10px 0 0; width: 32px;height:32px" /><div style="color:#fff"><strong>【重要】新数据提醒</strong></div>
|
||||
</div>
|
||||
<div class="info-wrap">
|
||||
<div class="tips" style="padding:15px;">
|
||||
@ -26,12 +23,12 @@
|
||||
<a style="color: #006eff;" href="${moreLink}" target="_blank" rel="noopener">[查看所有数据]</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">敲敲云平台</div>
|
||||
<div class="footer">北京国炬平台</div>
|
||||
<div class="footer" id="currentTime"></div>
|
||||
</div>
|
||||
<div style="width: 600px; margin: 0 auto; margin-top: 50px; font-size: 12px; -webkit-font-smoothing: subpixel-antialiased; text-size-adjust: 100%;">
|
||||
<p style="text-align: center; line-height: 20.4px; text-size-adjust: 100%; font-family: 'Microsoft YaHei'!important; padding: 0px !important; margin: 0px !important; color: #7e8890 !important;">
|
||||
<span class="appleLinks">Copyright © 2023-2024 北京敲敲云科技有限公司. 保留所有权利。</span>
|
||||
<span class="appleLinks">Copyright © 2023-2024 北京国炬科技股份有限公司. 保留所有权利。</span>
|
||||
</p>
|
||||
<p style="text-align: center;line-height: 20.4px; text-size-adjust: 100%; font-family: 'Microsoft YaHei'!important; padding: 0px !important; margin: 0px; color: #7e8890 !important; margin-top: 10px;">
|
||||
<span class="appleLinks">邮件由系统自动发送,请勿直接回复本邮件!</span>
|
||||
@ -49,8 +46,6 @@
|
||||
}
|
||||
|
||||
.info-top{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px 25px;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>jeecg-boot-parent</artifactId>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<version>3.6.2</version>
|
||||
<version>3.6.1</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 服务端提供方——feign接口
|
||||
@ -24,7 +24,7 @@ public class JcloudDemoProviderController {
|
||||
private JcloudDemoService jcloudDemoService;
|
||||
|
||||
@GetMapping("/getMessage")
|
||||
public String getMessage(@RequestParam(name = "name") String name) {
|
||||
public String getMessage(@RequestParam String name) {
|
||||
String msg = jcloudDemoService.getMessage(name);
|
||||
log.info(" 微服务被调用:{} ",msg);
|
||||
return msg;
|
||||
|
||||
@ -6,8 +6,8 @@ import org.apache.commons.io.IOUtils;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import javax.swing.filechooser.FileSystemView;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
@ -18,7 +18,7 @@ import org.jeecg.modules.demo.mock.vxe.entity.MockEntity;
|
||||
import org.jeecg.modules.demo.mock.vxe.websocket.VxeSocket;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URLDecoder;
|
||||
|
||||
@ -6,12 +6,12 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.constant.VxeSocketConst;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.websocket.OnClose;
|
||||
import javax.websocket.OnMessage;
|
||||
import javax.websocket.OnOpen;
|
||||
import javax.websocket.Session;
|
||||
import javax.websocket.server.PathParam;
|
||||
import javax.websocket.server.ServerEndpoint;
|
||||
import jakarta.websocket.OnClose;
|
||||
import jakarta.websocket.OnMessage;
|
||||
import jakarta.websocket.OnOpen;
|
||||
import jakarta.websocket.Session;
|
||||
import jakarta.websocket.server.PathParam;
|
||||
import jakarta.websocket.server.ServerEndpoint;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user