first commit
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
|
||||
FROM bellsoft/liberica-openjdk-rocky:17.0.15-cds
|
||||
#FROM bellsoft/liberica-openjdk-rocky:21.0.7-cds
|
||||
|
||||
LABEL maintainer="Lion Li"
|
||||
|
||||
RUN mkdir -p /ruoyi/server/logs \
|
||||
/ruoyi/server/temp \
|
||||
/ruoyi/skywalking/agent
|
||||
|
||||
WORKDIR /ruoyi/server
|
||||
|
||||
ENV SERVER_PORT=8080 SNAIL_PORT=28080 LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS=""
|
||||
|
||||
EXPOSE ${SERVER_PORT}
|
||||
# 暴露 snail job 客户端端口 用于定时任务调度中心通信
|
||||
EXPOSE ${SNAIL_PORT}
|
||||
|
||||
ADD ./target/ruoyi-server.jar ./app.jar
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -Dserver.port=${SERVER_PORT} \
|
||||
-Dsnail-job.port=${SNAIL_PORT} \
|
||||
# 应用名称 如果想区分集群节点监控 改成不同的名称即可
|
||||
#-Dskywalking.agent.service_name=ruoyi-server \
|
||||
#-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar \
|
||||
-XX:+HeapDumpOnOutOfMemoryError -XX:+UseZGC ${JAVA_OPTS} \
|
||||
-jar app.jar
|
||||
|
||||
@@ -0,0 +1,718 @@
|
||||
# 登录模块综合开发指南(LOGIN_COMPREHENSIVE_GUIDE)
|
||||
|
||||
版本:v1.1.0(2025-12-27)
|
||||
适用范围:Sys(后台管理端)、App(移动端)两套登录体系及微信公众号整合
|
||||
最后更新:2025-12-27
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
1. 登录模块概述
|
||||
2. 详细开发指南
|
||||
3. 登录流程示意图
|
||||
4. 实现方案
|
||||
5. 模块规范
|
||||
6. App 会话管理 API
|
||||
7. Sys 在线用户管理 API
|
||||
8. Sys 登录日志管理 API
|
||||
9. 相关代码路径参考
|
||||
|
||||
---
|
||||
|
||||
## 1. 登录模块概述
|
||||
|
||||
- 背景与目的
|
||||
- 系统同时支持 Sys 与 App 两套登录体系,分别面向管理员与 C 端用户,要求登录态、权限与数据完全隔离,同时提供统一的扩展与维护指引。
|
||||
- 采用策略模式(Strategy Pattern)实现登录方式的可插拔扩展,基于 Sa-Token 管理会话与权限,支持验证码、限流、并发与设备维度会话。
|
||||
- 集成微信公众号能力:App 用于授权登录与业务交互;Sys 用于管理员绑定与模板消息推送。
|
||||
|
||||
- 目标
|
||||
- 统一规范开发流程与接口定义,保障多端、多方式登录的稳定性与可扩展性。
|
||||
- 为新增登录方式与微信能力提供清晰的落地步骤与测试方案。
|
||||
|
||||
---
|
||||
|
||||
## 2. 详细开发指南
|
||||
|
||||
### 2.1 多用户体系与会话隔离
|
||||
- 物理隔离 + 逻辑统一:`sys_user` 与 `app_user` 两套用户体系;统一使用 Sa-Token 管理会话。
|
||||
- LoginId 前缀隔离:
|
||||
- Sys:`sys_user:{id}`
|
||||
- App:`app:{id}`
|
||||
- 会话校验(Sys 全局拦截):参考 SecurityConfig 的 `StpUtil.checkLogin()` 与路由校验,确保 Token 与 clientid 一致。
|
||||
|
||||
### 2.2 App 开发要点
|
||||
- 策略模式登录:`IAppAuthStrategy` 以 `grantType` 路由到具体策略(密码、短信、社交、微信等)。
|
||||
- 验证码与密码重试:`captcha.enable=true` 可开关;错误次数与锁定时间通过配置项控制。
|
||||
- 信息补全向导(过滤器):登录后若用户资料未完善,仅放行“查询个人信息/绑定手机号”接口,其余拦截返回 403。
|
||||
|
||||
### 2.3 Sys 开发要点
|
||||
- 安全加固:
|
||||
- 异地登录检测:比对最近一次登录 IP 归属地变化并告警(日志/邮件)。
|
||||
- 强制改密策略:登录后检查密码更新时间是否超期,超期在 Session 写入标记,拦截除改密外所有接口。
|
||||
- 管理员微信绑定与消息推送:后台生成带参二维码,管理员扫码绑定 `openId/unionId`,用于模板消息推送。
|
||||
|
||||
(最小改动策略)
|
||||
- 保留现有 Sys 登录与权限实现,不做核心代码改造。
|
||||
- 新增或接入的仅为“管理员微信绑定二维码生成”“SCAN/SUBSCRIBE 回调绑定”“模板消息推送”三个能力点;其余增强(如异地登录与强制改密)可按需增量加入,不影响主流程。
|
||||
|
||||
### 2.4 微信公众号整合(双端)
|
||||
- App:OAuth2 获取 `openId/unionId`,优先 `unionId` 进行跨应用账号识别;若未绑定则返回 401 并引导绑定手机号。
|
||||
- Sys:生成 `scene_str=sys_user:{userId}` 的二维码,接收 `SCAN/SUBSCRIBE` 事件解析并绑定,后续通过模板消息进行系统通知与订单推送。
|
||||
|
||||
---
|
||||
|
||||
## 3. 登录流程示意图
|
||||
|
||||
### 3.1 App 登录总体流程
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as 客户端
|
||||
participant Controller as AppAuthController
|
||||
participant Strategy as IAppAuthStrategy
|
||||
participant Service as AppLoginService
|
||||
participant Redis as Redis
|
||||
Client->>Controller: POST /app/auth/login
|
||||
Controller->>Strategy: authenticate(body, client, grantType)
|
||||
Strategy->>Service: checkLogin(...)
|
||||
Service->>Redis: PWD_ERR_CNT_KEY
|
||||
Strategy->>Controller: LoginVo(token, expire)
|
||||
Controller-->>Client: 返回结果
|
||||
```
|
||||
|
||||
### 3.2 App 微信公众号授权登录
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant H5 as 前端网页
|
||||
participant Wx as 微信OAuth2
|
||||
participant AppAPI as AppAuthController
|
||||
participant MP as WxMpService
|
||||
H5->>Wx: 跳转授权,获取code
|
||||
H5->>AppAPI: grantType=wechat_mp, code
|
||||
AppAPI->>MP: getAccessToken(code)
|
||||
MP-->>AppAPI: openId, accessToken
|
||||
AppAPI->>MP: userInfo(openId)
|
||||
MP-->>AppAPI: unionId
|
||||
AppAPI->>DB: 查询app_social_auth by unionId
|
||||
alt 已绑定
|
||||
AppAPI-->>H5: 登录成功
|
||||
else 未绑定
|
||||
AppAPI-->>H5: 401_BIND_REQUIRED + tempKey
|
||||
end
|
||||
```
|
||||
|
||||
### 3.3 Sys 管理员绑定与模板消息推送
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[管理员点击绑定] --> B[生成带参二维码 scene_str=sys_user:{userId}]
|
||||
B --> C[管理员扫码]
|
||||
C --> D[WxMpPortal接收SCAN/SUBSCRIBE]
|
||||
D --> E[解析EventKey与OpenId]
|
||||
E --> F[查询UnionId]
|
||||
F --> G[更新管理员绑定信息]
|
||||
G --> H[SysMessageService发送模板消息]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 实现方案
|
||||
|
||||
### 4.1 架构与技术选型
|
||||
- Sa-Token:统一的会话与权限管理(Token 名称、并发、多设备)。
|
||||
- 策略模式:按 `grantType` 定位不同登录策略,低耦合、易扩展。
|
||||
- WxJava:`ruoyi-mp` 模块提供的公众号 OAuth2 与模板消息能力。
|
||||
- 线程池与并发:按需启用全局线程池或在 JDK21 使用虚拟线程。
|
||||
|
||||
### 4.2 关键实现细节
|
||||
- LoginId 前缀实现:`LoginUser.getLoginId()` 返回 `userType + ":" + userId`。
|
||||
- App 密码策略:开启验证码校验时执行 `validateCaptcha()`;错误次数与锁定时间通过配置项 `user.password.maxRetryCount`、`user.password.lockTime` 控制。
|
||||
- App 信息补全:过滤器中检查 `infoComplete`,仅放行指定白名单接口(配置项 `user.auth.whitelist`),其余拦截返回 403。
|
||||
- App 微信授权:`getAccessToken(code) → userInfo(openId)`,优先使用 `unionId` 识别;未绑定返回 401 与临时 Key,引导绑定手机号。
|
||||
- Sys 管理员绑定:二维码 `scene_str=sys_user:{userId}`;接收事件后解析 `EventKey` 与 `OpenId`,查询 `unionId` 并写入绑定信息;后续根据 `openId` 推送模板消息。
|
||||
- 异地登录检测:在记录登录信息时调用 `LoginRiskService.checkRisk(userId, ip)`,比较归属地变化并告警。
|
||||
- 强制改密:登录后检查密码更新时间是否超期,标记 Session 并在拦截器中只放行改密接口。
|
||||
|
||||
### 4.4 配置项说明
|
||||
- **验证码配置**(`application.yml`):
|
||||
```yaml
|
||||
captcha:
|
||||
enable: true # 是否启用验证码校验
|
||||
type: MATH # 验证码类型(MATH数学计算 / CHAR字符验证)
|
||||
category: CIRCLE # 干扰类型(LINE线段 / CIRCLE圆圈 / SHEAR扭曲)
|
||||
numberLength: 1 # 数字验证码位数
|
||||
charLength: 4 # 字符验证码长度
|
||||
```
|
||||
- **用户密码策略配置**(`application.yml`):
|
||||
```yaml
|
||||
user:
|
||||
password:
|
||||
maxRetryCount: 5 # 密码最大错误次数
|
||||
lockTime: 10 # 密码锁定时间(分钟)
|
||||
```
|
||||
- **App端验证码与白名单配置**(`application.yml`):
|
||||
```yaml
|
||||
user:
|
||||
auth:
|
||||
captcha:
|
||||
enabled: false # App端验证码开关
|
||||
type: math # 验证码类型
|
||||
whitelist: /app/user/profile,/app/user/bind,/app/auth/logout # 信息补全白名单
|
||||
```
|
||||
- **客户端配置**:在 `sys_client` 表中配置客户端信息,包括 `client_id`、`grant_type`(支持多个,逗号分隔)、`status`、`timeout`、`active_timeout` 等。
|
||||
|
||||
### 4.3 OAuth 与 MP 职责划分
|
||||
- 第三方登录(OAuth):使用 justauth
|
||||
- 属性来源:`application*.yml` 中的 `justauth.type.*`
|
||||
- 代码路径:属性绑定 [SocialProperties](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialProperties.java),工厂方法 [SocialUtils.getAuthRequest](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java#L35-L74)
|
||||
- 使用位置:App 端策略 [AppWechatMpAuthStrategy](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/app/service/impl/AppWechatMpAuthStrategy.java)、[AppWechatOpenAuthStrategy](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/app/service/impl/AppWechatOpenAuthStrategy.java)、[AppWechatMiniprogramAuthStrategy](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/app/service/impl/AppWechatMiniprogramAuthStrategy.java)
|
||||
- 说明:source 包含 `wechat_mp`、`wechat_open`、`wechat_miniprogram`、`github`、`gitee` 等,统一由 justauth 提供 OAuth 能力
|
||||
- 公众号服务端能力(MP):使用 `wx.*`
|
||||
- 属性来源:`application*.yml` 中的 `wx.mp.*`、`wx.miniapp.*`、`wx.pay.*`
|
||||
- 代码路径:属性绑定 [WxMpProperties](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-modules/ruoyi-mp/src/main/java/org/dromara/mp/config/WxMpProperties.java),装配 [WxMpConfiguration](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-modules/ruoyi-mp/src/main/java/org/dromara/mp/config/WxMpConfiguration.java)
|
||||
- 使用位置:消息入口 [WxMpPortalController](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-modules/ruoyi-mp/src/main/java/org/dromara/mp/controller/WxMpPortalController.java)、二维码接口 [WxMpQrCodeController](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-modules/ruoyi-mp/src/main/java/org/dromara/mp/controller/WxMpQrCodeController.java)
|
||||
- 说明:MP 提供消息接收、二维码、模板消息推送等服务端功能,不用于 OAuth 登录
|
||||
|
||||
---
|
||||
|
||||
## 5. 模块规范
|
||||
|
||||
### 5.1 接口定义与参数
|
||||
|
||||
#### Sys 登录接口
|
||||
- **路径**:`/auth/login`
|
||||
- **方法**:`POST`
|
||||
- **支持的grantType**:
|
||||
- `password` - 密码登录(参数:`username`, `password`, 可选 `code/uuid`验证码)
|
||||
- `sms` - 短信登录(参数:`phonenumber`, `smsCode`)
|
||||
- `email` - 邮箱登录(参数:`email`, `emailCode`)
|
||||
- `social` - 第三方登录(参数:`source`, `socialCode`, `socialState`)
|
||||
- **通用参数**:`clientId`(必填)
|
||||
- **返回**:`LoginVo`(包含 `accessToken`, `expireIn`, `clientId`)
|
||||
- **代码路径**:[AuthController.login](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/web/controller/AuthController.java#L75-L101)
|
||||
|
||||
#### Sys 退出登录接口
|
||||
- **路径**:`/auth/logout`
|
||||
- **方法**:`POST`
|
||||
- **权限**:需要登录态
|
||||
- **说明**:退出当前登录会话,清除Token
|
||||
- **代码路径**:[AuthController.logout](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/web/controller/AuthController.java#L164-L168)
|
||||
|
||||
#### Sys 用户注册接口
|
||||
- **路径**:`/auth/register`
|
||||
- **方法**:`POST`
|
||||
- **参数**:`username`, `password`, `userType`, 可选 `code/uuid`(验证码)
|
||||
- **说明**:系统用户注册,需系统开启注册功能
|
||||
- **代码路径**:[AuthController.register](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/web/controller/AuthController.java#L174-L181)
|
||||
|
||||
#### Sys 第三方绑定接口
|
||||
- **获取绑定URL**:`GET /auth/binding/{source}?domain={domain}`
|
||||
- 生成第三方授权跳转URL,用于绑定第三方账号
|
||||
- **回调绑定**:`POST /auth/social/callback`
|
||||
- 前端回调后绑定第三方账号(需token)
|
||||
- **取消绑定**:`DELETE /auth/unlock/{socialId}`
|
||||
- 取消已绑定的第三方账号(需token)
|
||||
- **代码路径**:[AuthController](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/web/controller/AuthController.java#L109-L158)
|
||||
|
||||
#### App 登录接口
|
||||
- **路径**:`/app/auth/login`
|
||||
- **方法**:`POST`
|
||||
- **支持的grantType**:
|
||||
- `app_password` - 密码登录(参数:`username`, `password`)
|
||||
- `sms` - 短信登录(参数:`phonenumber`, `smsCode`)
|
||||
- `email` - 邮箱登录(参数:`email`, `emailCode`)
|
||||
- `social` - 第三方登录(参数:`source`, `socialCode`, `socialState`)
|
||||
- `wechat_mp` - 微信公众号登录(参数:`socialCode`, `socialState`)
|
||||
- `wechat_open` - 微信开放平台登录(参数:`socialCode`, `socialState`)
|
||||
- `wechat_miniprogram` - 微信小程序登录(参数:`socialCode`, `socialState`)
|
||||
- **通用参数**:`clientId`(必填)
|
||||
- **返回**:`LoginVo`(包含 `accessToken`, `expireIn`, `clientId`)
|
||||
- **代码路径**:[AppAuthController.login](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/app/controller/AppAuthController.java#L69-L93)
|
||||
|
||||
#### App 退出登录接口
|
||||
- **路径**:`/app/auth/logout`
|
||||
- **方法**:`POST`
|
||||
- **权限**:需要登录态
|
||||
- **说明**:退出当前登录会话,清除Token
|
||||
- **代码路径**:[AppAuthController.logout](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/app/controller/AppAuthController.java#L100-L104)
|
||||
|
||||
#### App 用户注册接口
|
||||
- **路径**:`/app/auth/register`
|
||||
- **方法**:`POST`
|
||||
- **参数**:`username`, `password`
|
||||
- **限流**:60秒内最多1次(按IP)
|
||||
- **说明**:App用户注册,需系统开启注册功能
|
||||
- **代码路径**:[AppAuthController.register](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/app/controller/AppAuthController.java#L115-L132)
|
||||
|
||||
#### App 配置接口
|
||||
- **路径**:`/app/auth/config`
|
||||
- **方法**:`GET`
|
||||
- **说明**:获取App端配置信息(如注册开关等)
|
||||
- **代码路径**:[AppAuthController.getConfig](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/app/controller/AppAuthController.java#L139-L145)
|
||||
|
||||
#### 验证码接口
|
||||
- **图形验证码**:`GET /auth/code`
|
||||
- 返回:`CaptchaVo`(包含 `uuid`, `img`(Base64图片), `captchaEnabled`)
|
||||
- 限流:60秒内最多10次(按IP)
|
||||
- **代码路径**:[CaptchaController.getCode](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/web/controller/CaptchaController.java#L112-L121)
|
||||
- **短信验证码**:`GET /resource/sms/code?phonenumber={phonenumber}`
|
||||
- 限流:60秒内最多1次(按手机号)
|
||||
- **代码路径**:[CaptchaController.smsCode](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/web/controller/CaptchaController.java#L59-L76)
|
||||
- **邮箱验证码**:`GET /resource/email/code?email={email}`
|
||||
- 限流:60秒内最多1次(按邮箱)
|
||||
- **代码路径**:[CaptchaController.emailCode](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/web/controller/CaptchaController.java#L83-L90)
|
||||
|
||||
#### 管理员绑定微信(建议)
|
||||
- **生成绑定二维码**:`POST /wx/mp/qrcode/{appid}/forever?sceneStr=sys_user:{userId}`
|
||||
- 生成永久二维码,scene_str = sys_user:{userId}
|
||||
- **获取二维码URL**:`GET /wx/mp/qrcode/{appid}/url?ticket={ticket}`
|
||||
- 根据ticket获取二维码图片URL
|
||||
- **事件回调**:`WxMpPortalController` 处理 `SCAN/SUBSCRIBE` 事件并完成绑定
|
||||
- **代码路径**:[WxMpPortalController](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-modules/ruoyi-mp/src/main/java/org/dromara/mp/controller/WxMpPortalController.java)
|
||||
|
||||
### 5.2 会话与权限规范
|
||||
- Token 名称:`Authorization`(统一)。
|
||||
- 并发策略:`is-concurrent=true` 支持多设备,按需要设置 `deviceType`。
|
||||
- 登录态隔离:Sys 与 App 使用不同 LoginId 前缀与独立会话空间。
|
||||
- 鉴权方式:Sys 采用注解鉴权(`@SaCheckPermission` 等);App 采用资源级鉴权(业务层校验 `userId` 所属资源)。
|
||||
|
||||
### 5.3 错误码与处理
|
||||
- `401_BIND_REQUIRED`:微信授权成功但未绑定手机号,引导前端跳转绑定流程。
|
||||
- `403_UPDATE_PWD`:Sys 账号密码超期未改,拦截除改密外所有接口。
|
||||
- `403_INFO_INCOMPLETE`:App 用户信息未补全,仅放行资料完善接口。
|
||||
|
||||
### 5.4 约束与约定
|
||||
- 不混用 Sys 与 App 的用户体系与权限模型。
|
||||
- 微信授权优先使用 `unionId` 进行跨应用识别;若缺失则降级使用 `openId`。
|
||||
- 新增登录方式遵循 `{grantType}AppAuthStrategy` 命名与策略路由约定。
|
||||
|
||||
### 5.5 App 角色与关联关系
|
||||
- 表结构与关系:
|
||||
- 用户表:`app_user`,参见 [ry_app.sql:app_user](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/script/sql/ry_app.sql#L6-L27)
|
||||
- 角色表:`app_role`(角色键如 `app_user`、`app_vip`),参见 [ry_app.sql:app_role](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/script/sql/ry_app.sql#L35-L53)
|
||||
- 关联表:`app_user_role`(多对多关联),参见 [ry_app.sql:app_user_role](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/script/sql/ry_app.sql#L56-L64)
|
||||
- 规范说明:
|
||||
- App 端角色用于业务权益与标签管理(如普通用户、VIP),不等同于 Sys 端的 RBAC 功能权限。
|
||||
- 业务层根据 `role_key`(如 `app_vip`)判断权益与展示(价格、特权、活动资格等),避免与后台菜单/按钮权限混用。
|
||||
- 登录后构建的 `AppLoginUser` 可加载角色集合用于业务判定;权限校验仍以资源归属为主(基于 `userId`)。
|
||||
- 第三方授权表:
|
||||
- `app_social_auth` 用于存储平台标识(`platform`/`platform_uid`)与微信 `open_id`/`union_id`,参见 [ry_app.sql:app_social_auth](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/script/sql/ry_app.sql#L67-L85)
|
||||
- App 微信登录优先使用 `union_id` 进行账号打通;若缺失则降级使用 `open_id`。
|
||||
|
||||
### 5.6 调用实例(curl)
|
||||
- Sys 密码登录(grantType=password):
|
||||
```bash
|
||||
curl -sS -X POST "http://localhost:8080/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"clientId": "e5cd7e4891bf95d1d19206ce24a7b32e",
|
||||
"grantType": "password",
|
||||
"username": "admin",
|
||||
"password": "admin123",
|
||||
"code": "a1b2",
|
||||
"uuid": "captcha-uuid"
|
||||
}'
|
||||
```
|
||||
- Sys 短信登录(grantType=sms):
|
||||
```bash
|
||||
curl -sS -X POST "http://localhost:8080/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"clientId": "e5cd7e4891bf95d1d19206ce24a7b32e",
|
||||
"grantType": "sms",
|
||||
"phonenumber": "13800000000",
|
||||
"smsCode": "123456"
|
||||
}'
|
||||
```
|
||||
- Sys 邮箱登录(grantType=email):
|
||||
```bash
|
||||
curl -sS -X POST "http://localhost:8080/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"clientId": "e5cd7e4891bf95d1d19206ce24a7b32e",
|
||||
"grantType": "email",
|
||||
"email": "admin@example.com",
|
||||
"emailCode": "654321"
|
||||
}'
|
||||
```
|
||||
- Sys 第三方登录(grantType=social,source∈{github,gitee,wechat_open,wechat_mp…}):
|
||||
```bash
|
||||
curl -sS -X POST "http://localhost:8080/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"clientId": "e5cd7e4891bf95d1d19206ce24a7b32e",
|
||||
"grantType": "social",
|
||||
"source": "github",
|
||||
"socialCode": "oauth-code",
|
||||
"socialState": "state-123"
|
||||
}'
|
||||
```
|
||||
- App 密码登录(grantType=app_password):
|
||||
```bash
|
||||
curl -sS -X POST "http://localhost:8080/app/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"clientId": "428a8310cd442757ae699df5d894f051",
|
||||
"grantType": "app_password",
|
||||
"username": "appuser",
|
||||
"password": "admin123"
|
||||
}'
|
||||
```
|
||||
- App 短信登录(grantType=sms):
|
||||
```bash
|
||||
curl -sS -X POST "http://localhost:8080/app/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"clientId": "428a8310cd442757ae699df5d894f051",
|
||||
"grantType": "sms",
|
||||
"phonenumber": "13800000000",
|
||||
"smsCode": "123456"
|
||||
}'
|
||||
```
|
||||
- App 邮箱登录(grantType=email):
|
||||
```bash
|
||||
curl -sS -X POST "http://localhost:8080/app/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"clientId": "428a8310cd442757ae699df5d894f051",
|
||||
"grantType": "email",
|
||||
"email": "user@example.com",
|
||||
"emailCode": "654321"
|
||||
}'
|
||||
```
|
||||
- App 第三方登录(grantType=social,source∈{github,gitee,wechat_open…}):
|
||||
```bash
|
||||
curl -sS -X POST "http://localhost:8080/app/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"clientId": "428a8310cd442757ae699df5d894f051",
|
||||
"grantType": "social",
|
||||
"source": "gitee",
|
||||
"socialCode": "oauth-code",
|
||||
"socialState": "state-xyz"
|
||||
}'
|
||||
```
|
||||
- App 微信公众号登录(grantType=wechat_mp,前端已拿到 code 与 state):
|
||||
```bash
|
||||
curl -sS -X POST "http://localhost:8080/app/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"clientId": "428a8310cd442757ae699df5d894f051",
|
||||
"grantType": "wechat_mp",
|
||||
"socialCode": "wx-oauth-code",
|
||||
"socialState": "state-123"
|
||||
}'
|
||||
```
|
||||
- App 微信开放平台登录(grantType=wechat_open):
|
||||
```bash
|
||||
curl -sS -X POST "http://localhost:8080/app/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"clientId": "428a8310cd442757ae699df5d894f051",
|
||||
"grantType": "wechat_open",
|
||||
"socialCode": "wx-open-code",
|
||||
"socialState": "state-456"
|
||||
}'
|
||||
```
|
||||
- App 微信小程序登录(grantType=wechat_miniprogram):
|
||||
```bash
|
||||
curl -sS -X POST "http://localhost:8080/app/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"clientId": "428a8310cd442757ae699df5d894f051",
|
||||
"grantType": "wechat_miniprogram",
|
||||
"socialCode": "wx-mini-code",
|
||||
"socialState": "state-789"
|
||||
}'
|
||||
```
|
||||
- Sys 管理员绑定二维码(生成永久二维码 ticket 与 URL):
|
||||
```bash
|
||||
# 生成永久二维码,scene_str = sys_user:{userId}
|
||||
curl -sS -X POST "http://localhost:8080/wx/mp/qrcode/{appid}/forever" \
|
||||
-d "sceneStr=sys_user:1"
|
||||
|
||||
# 根据 ticket 获取二维码 URL
|
||||
curl -sS -X GET "http://localhost:8080/wx/mp/qrcode/{appid}/url" \
|
||||
--data-urlencode "ticket=YOUR_TICKET"
|
||||
```
|
||||
- 获取公众号配置:
|
||||
```bash
|
||||
curl -sS "http://localhost:8080/wx/mp/portal/{appid}/config"
|
||||
```
|
||||
- App 用户信息补全白名单接口示例(占位):
|
||||
```bash
|
||||
# 获取个人信息
|
||||
curl -sS "http://localhost:8080/app/user/profile" -H "Authorization: Bearer YOUR_TOKEN"
|
||||
|
||||
# 绑定手机号
|
||||
curl -sS -X POST "http://localhost:8080/app/user/bind" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-d '{ "phone": "13800000000", "smsCode": "123456" }'
|
||||
```
|
||||
- Sys 退出登录:
|
||||
```bash
|
||||
curl -sS -X POST "http://localhost:8080/auth/logout" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
- App 退出登录:
|
||||
```bash
|
||||
curl -sS -X POST "http://localhost:8080/app/auth/logout" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
- Sys 用户注册:
|
||||
```bash
|
||||
curl -sS -X POST "http://localhost:8080/auth/register" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "newuser",
|
||||
"password": "password123",
|
||||
"userType": "sys_user",
|
||||
"code": "a1b2",
|
||||
"uuid": "captcha-uuid"
|
||||
}'
|
||||
```
|
||||
- App 用户注册:
|
||||
```bash
|
||||
curl -sS -X POST "http://localhost:8080/app/auth/register" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "appuser",
|
||||
"password": "password123"
|
||||
}'
|
||||
```
|
||||
- 获取图形验证码:
|
||||
```bash
|
||||
curl -sS "http://localhost:8080/auth/code"
|
||||
# 返回: {"code":200,"data":{"uuid":"xxx","img":"data:image/png;base64,...","captchaEnabled":true}}
|
||||
```
|
||||
- 获取短信验证码:
|
||||
```bash
|
||||
curl -sS "http://localhost:8080/resource/sms/code?phonenumber=13800000000"
|
||||
```
|
||||
- 获取邮箱验证码:
|
||||
```bash
|
||||
curl -sS "http://localhost:8080/resource/email/code?email=user@example.com"
|
||||
```
|
||||
- 获取App配置:
|
||||
```bash
|
||||
curl -sS "http://localhost:8080/app/auth/config"
|
||||
# 返回: {"code":200,"data":{"registerEnabled":true}}
|
||||
```
|
||||
- Sys 获取第三方绑定URL:
|
||||
```bash
|
||||
curl -sS "http://localhost:8080/auth/binding/github?domain=http://localhost:8080"
|
||||
# 返回: {"code":200,"data":"https://github.com/login/oauth/authorize?..."}
|
||||
```
|
||||
- Sys 第三方绑定回调:
|
||||
```bash
|
||||
curl -sS -X POST "http://localhost:8080/auth/social/callback" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-d '{
|
||||
"source": "github",
|
||||
"socialCode": "oauth-code",
|
||||
"socialState": "state-123"
|
||||
}'
|
||||
```
|
||||
- Sys 取消第三方绑定:
|
||||
```bash
|
||||
curl -sS -X DELETE "http://localhost:8080/auth/unlock/1" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. App 会话管理 API
|
||||
|
||||
- 基础路径:`/app/session`
|
||||
- 鉴权:登录态(Sa-Token)
|
||||
|
||||
### 6.1 接口列表
|
||||
- `GET /app/session/active` 获取当前用户活跃会话列表
|
||||
- `POST /app/session/terminate` 终止指定会话(参数:`sessionId`)
|
||||
- `POST /app/session/heartbeat` 会话续期(参数:`sessionId`,默认延长至 30 天)
|
||||
- `GET /app/session/sse` 订阅会话事件(SSE),事件名:`session`,数据格式:`<TYPE>:<SESSION_ID>`
|
||||
|
||||
### 6.2 会话数据结构(AppSession)
|
||||
- `sessionId` 会话ID(token值)
|
||||
- `userId` 用户ID
|
||||
- `deviceType` 设备类型(Web/iOS/Android/Desktop)
|
||||
- `deviceId` 设备标识(`X-Device-Id` 或生成)
|
||||
- `userAgent` UA 字符串
|
||||
- `ip` 登录IP
|
||||
- `location` 地理位置(预留)
|
||||
- `loginAt` 登录时间戳(毫秒)
|
||||
- `expireAt` 过期时间戳(毫秒)
|
||||
- `active` 活跃状态
|
||||
- `token` 令牌原文
|
||||
|
||||
### 6.3 存储键规范
|
||||
- 索引:`APP:SESS_INDEX:{userId}`
|
||||
- 会话:`APP:SESS:{userId}:{sessionId}`(TTL 与 token 同步)
|
||||
- 事件通道:`APP:SESS_EVT:{userId}`
|
||||
|
||||
### 6.4 调用示例(curl)
|
||||
- 获取活跃会话:
|
||||
```bash
|
||||
curl -sS "http://localhost:8080/app/session/active" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
- 终止指定会话:
|
||||
```bash
|
||||
curl -sS -X POST "http://localhost:8080/app/session/terminate" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-d '{ "sessionId": "TOKEN_VALUE_TO_TERMINATE" }'
|
||||
```
|
||||
- 会话续期(心跳):
|
||||
```bash
|
||||
curl -sS -X POST "http://localhost:8080/app/session/heartbeat" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-d '{ "sessionId": "CURRENT_TOKEN_VALUE" }'
|
||||
```
|
||||
- 订阅会话事件(SSE):
|
||||
```bash
|
||||
curl -sS "http://localhost:8080/app/session/sse" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
### 6.5 优化建议
|
||||
- 统一设备标识头 `X-Device-Id`,策略层在登录时读取并写入会话,便于同账号多设备管理。
|
||||
- 事件命名与负载标准化:`<TYPE>:<SESSION_ID>`(TYPE ∈ {login, logout, heartbeat, terminate})。
|
||||
- TTL 与 Sa-Token 同步:确保 `APP:SESS:{userId}:{sessionId}` 的过期时间与 Token 过期保持一致,避免“僵尸会话”。
|
||||
|
||||
---
|
||||
|
||||
## 7. Sys 在线用户管理 API
|
||||
|
||||
- 基础路径:`/monitor/online`
|
||||
- 鉴权:登录态(Sa-Token),需要权限 `monitor:online:*`
|
||||
|
||||
### 7.1 接口列表
|
||||
- `GET /monitor/online/list` 获取在线用户监控列表(支持按IP、用户名筛选)
|
||||
- `GET /monitor/online` 获取当前用户登录的在线设备列表
|
||||
- `DELETE /monitor/online/{tokenId}` 强退指定用户(需要权限 `monitor:online:forceLogout`)
|
||||
- `DELETE /monitor/online/myself/{tokenId}` 强退当前用户的指定设备
|
||||
|
||||
### 7.2 在线用户数据结构(SysUserOnline)
|
||||
- `tokenId` 会话编号(Token值)
|
||||
- `userName` 用户名称
|
||||
- `deptName` 部门名称
|
||||
- `clientKey` 客户端标识
|
||||
- `deviceType` 设备类型
|
||||
- `ipaddr` 登录IP地址
|
||||
- `loginLocation` 登录地点
|
||||
- `browser` 浏览器类型
|
||||
- `os` 操作系统
|
||||
- `loginTime` 登录时间(时间戳,毫秒)
|
||||
|
||||
### 7.3 调用示例(curl)
|
||||
- 获取在线用户列表:
|
||||
```bash
|
||||
curl -sS "http://localhost:8080/monitor/online/list?ipaddr=127.0.0.1&userName=admin" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
- 获取当前用户在线设备:
|
||||
```bash
|
||||
curl -sS "http://localhost:8080/monitor/online" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
- 强退指定用户:
|
||||
```bash
|
||||
curl -sS -X DELETE "http://localhost:8080/monitor/online/TOKEN_VALUE" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
- 强退当前用户的指定设备:
|
||||
```bash
|
||||
curl -sS -X DELETE "http://localhost:8080/monitor/online/myself/TOKEN_VALUE" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
- **代码路径**:[SysUserOnlineController](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/monitor/SysUserOnlineController.java)
|
||||
|
||||
---
|
||||
|
||||
## 8. Sys 登录日志管理 API
|
||||
|
||||
- 基础路径:`/monitor/logininfor`
|
||||
- 鉴权:登录态(Sa-Token),需要权限 `monitor:logininfor:*`
|
||||
|
||||
### 8.1 接口列表
|
||||
- `GET /monitor/logininfor/list` 获取系统访问记录列表(分页,支持按IP、状态、用户名、时间范围筛选)
|
||||
- `POST /monitor/logininfor/export` 导出系统访问记录列表(需要权限 `monitor:logininfor:export`)
|
||||
- `DELETE /monitor/logininfor/{infoIds}` 批量删除登录日志(需要权限 `monitor:logininfor:remove`)
|
||||
- `DELETE /monitor/logininfor/clean` 清理系统访问记录(需要权限 `monitor:logininfor:remove`)
|
||||
- `GET /monitor/logininfor/unlock/{userName}` 解锁指定用户的登录锁定(需要权限 `monitor:logininfor:unlock`)
|
||||
|
||||
### 8.2 登录日志数据结构(SysLogininfor)
|
||||
- `infoId` 日志ID
|
||||
- `userName` 用户账号
|
||||
- `clientKey` 客户端标识
|
||||
- `deviceType` 设备类型
|
||||
- `status` 登录状态(0成功 1失败)
|
||||
- `ipaddr` 登录IP地址
|
||||
- `loginLocation` 登录地点
|
||||
- `browser` 浏览器类型
|
||||
- `os` 操作系统
|
||||
- `msg` 提示消息
|
||||
- `loginTime` 访问时间
|
||||
|
||||
### 8.3 调用示例(curl)
|
||||
- 获取登录日志列表:
|
||||
```bash
|
||||
curl -sS "http://localhost:8080/monitor/logininfor/list?ipaddr=127.0.0.1&status=0&userName=admin&beginTime=2024-01-01&endTime=2024-12-31&pageNum=1&pageSize=10" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
- 导出登录日志:
|
||||
```bash
|
||||
curl -sS -X POST "http://localhost:8080/monitor/logininfor/export" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-o logininfor.xlsx
|
||||
```
|
||||
- 批量删除登录日志:
|
||||
```bash
|
||||
curl -sS -X DELETE "http://localhost:8080/monitor/logininfor/1,2,3" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
- 清理登录日志:
|
||||
```bash
|
||||
curl -sS -X DELETE "http://localhost:8080/monitor/logininfor/clean" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
- 解锁用户登录:
|
||||
```bash
|
||||
curl -sS "http://localhost:8080/monitor/logininfor/unlock/admin" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
- **代码路径**:[SysLogininforController](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/monitor/SysLogininforController.java)
|
||||
|
||||
---
|
||||
|
||||
## 9. 相关代码路径参考
|
||||
|
||||
### 9.1 核心控制器
|
||||
- **Sys登录控制器**:[AuthController](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/web/controller/AuthController.java)
|
||||
- **App登录控制器**:[AppAuthController](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/app/controller/AppAuthController.java)
|
||||
- **验证码控制器**:[CaptchaController](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/web/controller/CaptchaController.java)
|
||||
|
||||
### 9.2 核心服务
|
||||
- **Sys登录服务**:[SysLoginService](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/web/service/SysLoginService.java)
|
||||
- **App登录服务**:[AppLoginService](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/app/service/AppLoginService.java)
|
||||
- **Sys注册服务**:[SysRegisterService](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/web/service/SysRegisterService.java)
|
||||
|
||||
### 9.3 登录策略
|
||||
- **Sys策略接口**:[IAuthStrategy](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/web/service/IAuthStrategy.java)
|
||||
- **App策略接口**:[IAppAuthStrategy](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/app/service/IAppAuthStrategy.java)
|
||||
- **密码策略**:[PasswordAuthStrategy](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java), [AppPasswordAuthStrategy](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/app/service/impl/AppPasswordAuthStrategy.java)
|
||||
- **短信策略**:[SmsAuthStrategy](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java), [AppSmsAuthStrategy](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/app/service/impl/AppSmsAuthStrategy.java)
|
||||
- **邮箱策略**:[EmailAuthStrategy](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/web/service/impl/EmailAuthStrategy.java), [AppEmailAuthStrategy](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/app/service/impl/AppEmailAuthStrategy.java)
|
||||
- **第三方策略**:[SocialAuthStrategy](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java), [AppSocialAuthStrategy](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-server/src/main/java/org/dromara/app/service/impl/AppSocialAuthStrategy.java)
|
||||
|
||||
### 9.4 管理接口
|
||||
- **在线用户管理**:[SysUserOnlineController](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/monitor/SysUserOnlineController.java)
|
||||
- **登录日志管理**:[SysLogininforController](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/monitor/SysLogininforController.java)
|
||||
- **登录日志服务**:[SysLogininforService](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/SysLogininforService.java), [SysLogininforServiceImpl](file:///Users/lihaha/Documents/Projecte/PoJie/PoJie/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysLogininforServiceImpl.java)
|
||||
|
||||
---
|
||||
|
||||
所有代码与配置参考项目既有实现与路径,确保可在 IDE 中点击跳转进行联查与验证。
|
||||
@@ -0,0 +1,188 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>PoJie</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<packaging>jar</packaging>
|
||||
<artifactId>ruoyi-server</artifactId>
|
||||
|
||||
<description>
|
||||
服务启动入口
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- Mysql驱动包 -->
|
||||
<!--<dependency>-->
|
||||
<!-- <groupId>com.mysql</groupId>-->
|
||||
<!-- <artifactId>mysql-connector-j</artifactId>-->
|
||||
<!--</dependency>-->
|
||||
|
||||
<!-- <!– mp支持的数据库均支持 只需要增加对应的jdbc依赖即可 –>-->
|
||||
<!-- <!– Oracle –>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.oracle.database.jdbc</groupId>-->
|
||||
<!-- <artifactId>ojdbc8</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- <!– 兼容oracle低版本 –>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.oracle.database.nls</groupId>-->
|
||||
<!-- <artifactId>orai18n</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- PostgreSql -->
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
</dependency>
|
||||
<!-- <!– SqlServer –>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.microsoft.sqlserver</groupId>-->
|
||||
<!-- <artifactId>mssql-jdbc</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-doc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-social</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-ratelimiter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-mail</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 显式引入 mybatis 公共模块,确保接口默认方法可见 -->
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-mybatis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-system</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-member</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-mp</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-job</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 代码生成-->
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-generator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 工作流模块 -->
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-workflow</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 知识库模块 -->
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-knowledge</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- AI中枢模块 -->
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-ai</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 聊天模块 -->
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-chat</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 支付模块 -->
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-pay</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>de.codecentric</groupId>
|
||||
<artifactId>spring-boot-admin-starter-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- skywalking 整合 logback -->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.apache.skywalking</groupId>-->
|
||||
<!-- <artifactId>apm-toolkit-logback-1.x</artifactId>-->
|
||||
<!-- <version>${与你的agent探针版本保持一致}</version>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.apache.skywalking</groupId>-->
|
||||
<!-- <artifactId>apm-toolkit-trace</artifactId>-->
|
||||
<!-- <version>${与你的agent探针版本保持一致}</version>-->
|
||||
<!-- </dependency>-->
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>${project.artifactId}</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>${maven-jar-plugin.version}</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-war-plugin</artifactId>
|
||||
<version>${maven-war-plugin.version}</version>
|
||||
<configuration>
|
||||
<failOnMissingWebXml>false</failOnMissingWebXml>
|
||||
<warName>${project.artifactId}</warName>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.dromara;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
|
||||
|
||||
/**
|
||||
* 启动程序
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class PoJieApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication application = new SpringApplication(PoJieApplication.class);
|
||||
application.setApplicationStartup(new BufferingApplicationStartup(2048));
|
||||
application.run(args);
|
||||
System.out.println("(♥◠‿◠)ノ゙ PoJie启动成功 ლ(´ڡ`ლ)゙");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.dromara;
|
||||
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
|
||||
|
||||
/**
|
||||
* web容器中进行部署
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public class PoJieServletInitializer extends SpringBootServletInitializer {
|
||||
|
||||
@Override
|
||||
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
|
||||
return application.sources(PoJieApplication.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package org.dromara.app.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.hutool.core.codec.Base64;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.zhyd.oauth.model.AuthResponse;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import me.zhyd.oauth.request.AuthRequest;
|
||||
import me.zhyd.oauth.utils.AuthStateUtils;
|
||||
import org.dromara.app.domain.vo.AppConfigVo;
|
||||
import org.dromara.app.service.AppLoginService;
|
||||
import org.dromara.app.service.IAppAuthStrategy;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.domain.model.LoginBody;
|
||||
import org.dromara.common.core.domain.model.RegisterBody;
|
||||
import org.dromara.common.core.domain.model.SocialLoginBody;
|
||||
import org.dromara.common.core.utils.MessageUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.ratelimiter.annotation.RateLimiter;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
|
||||
import org.dromara.common.social.config.properties.SocialProperties;
|
||||
import org.dromara.common.social.utils.SocialUtils;
|
||||
import org.dromara.member.domain.bo.AppUserBo;
|
||||
import org.dromara.member.service.AppSocialAuthService;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.system.service.SysClientService;
|
||||
import org.dromara.system.service.SysConfigService;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* App端认证控制器
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@SaIgnore
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/app/auth")
|
||||
public class AppAuthController {
|
||||
|
||||
private final SocialProperties socialProperties;
|
||||
private final AppLoginService appLoginService;
|
||||
private final org.dromara.app.service.AppLoginRiskService appLoginRiskService;
|
||||
private final SysClientService clientService;
|
||||
private final SysConfigService configService;
|
||||
private final AppSocialAuthService appSocialAuthService;
|
||||
|
||||
/**
|
||||
* App端登录方法
|
||||
*
|
||||
* @param body 登录信息
|
||||
* @return 结果
|
||||
*/
|
||||
// TODO: App端暂时不加密,先完善用户端的加密方法在加密
|
||||
// @ApiEncrypt
|
||||
@PostMapping("/login")
|
||||
public R<LoginVo> login(@RequestBody String body) {
|
||||
LoginBody loginBody = JsonUtils.parseObject(body, LoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
// 授权类型和客户端id
|
||||
String clientId = loginBody.getClientId();
|
||||
String grantType = loginBody.getGrantType();
|
||||
SysClientVo client = clientService.queryByClientId(clientId);
|
||||
// 查询不到 client 或 client 内不包含 grantType
|
||||
if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), grantType)) {
|
||||
log.info("客户端id: {} 认证类型:{} 异常!.", clientId, grantType);
|
||||
return R.fail(MessageUtils.message("auth.grant.type.error"));
|
||||
} else if (!SystemConstants.NORMAL.equals(client.getStatus())) {
|
||||
return R.fail(MessageUtils.message("auth.grant.type.blocked"));
|
||||
}
|
||||
// App端登录
|
||||
LoginVo loginVo = IAppAuthStrategy.authenticate(body, client, grantType);
|
||||
|
||||
Long userId = LoginHelper.getUserId();
|
||||
// 记录登录信息
|
||||
appLoginRiskService.checkRisk(userId, LoginHelper.getLoginUser().getIpaddr());
|
||||
appLoginService.recordLoginInfo(userId, LoginHelper.getLoginUser().getIpaddr());
|
||||
|
||||
return R.ok(loginVo);
|
||||
}
|
||||
|
||||
/**
|
||||
* App端退出登录
|
||||
*
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
public R<Void> logout() {
|
||||
appLoginService.logout();
|
||||
return R.ok("退出成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* App端用户注册
|
||||
*
|
||||
* @param user 用户信息
|
||||
* @return 结果
|
||||
*/
|
||||
// TODO: App端暂时不加密,先完善用户端的加密方法在加密
|
||||
// @ApiEncrypt
|
||||
@RateLimiter(key = "register", time = 60, count = 1)
|
||||
@PostMapping("/register")
|
||||
public R<Void> register(@Validated @RequestBody RegisterBody user) {
|
||||
if (!configService.selectRegisterEnabled()) {
|
||||
return R.fail("当前系统没有开启注册功能!");
|
||||
}
|
||||
AppUserBo bo = new AppUserBo();
|
||||
bo.setUserName(user.getUsername());
|
||||
bo.setPassword(user.getPassword());
|
||||
bo.setNickName(user.getUsername());
|
||||
|
||||
// 校验是否已存在
|
||||
if (!appLoginService.checkUserNameUnique(bo.getUserName())) {
|
||||
return R.fail("注册失败,账号已存在");
|
||||
}
|
||||
|
||||
// 执行注册
|
||||
return appLoginService.register(bo) ? R.ok() : R.fail("注册失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取App端配置信息
|
||||
*
|
||||
* @return 配置信息
|
||||
*/
|
||||
@GetMapping("/config")
|
||||
public R<AppConfigVo> getConfig() {
|
||||
AppConfigVo config = new AppConfigVo();
|
||||
config.setRegisterEnabled(configService.selectRegisterEnabled());
|
||||
// 可以添加更多App端特有的配置
|
||||
return R.ok(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录二维码与票据
|
||||
*/
|
||||
@GetMapping("/qrcode")
|
||||
public R<java.util.Map<String,String>> getQrCode() {
|
||||
String ticket = cn.hutool.core.util.IdUtil.fastSimpleUUID();
|
||||
RedisUtils.setCacheObject("APP_LOGIN_QR:" + ticket, "PENDING", java.time.Duration.ofMinutes(5));
|
||||
String url = "/wx/mp/qrcode/url?ticket=" + ticket;
|
||||
java.util.Map<String,String> res = new java.util.HashMap<>();
|
||||
res.put("ticket", ticket);
|
||||
res.put("url", url);
|
||||
return R.ok(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询二维码登录状态
|
||||
*/
|
||||
@GetMapping("/qrcode/login")
|
||||
public R<String> pollQrLogin(@RequestParam String ticket) {
|
||||
String key = "APP_LOGIN_QR:" + ticket;
|
||||
String status = RedisUtils.getCacheObject(key);
|
||||
if (status == null) {
|
||||
return R.ok("EXPIRED");
|
||||
}
|
||||
return R.ok(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定第三方账号
|
||||
*
|
||||
* @param body 第三方账号信息
|
||||
* @return 结果
|
||||
*/
|
||||
// TODO: App端暂时不加密,先完善前段的加密方法在加密
|
||||
// @ApiEncrypt
|
||||
@PostMapping("/bind/social")
|
||||
public R<Void> bindSocial(@RequestBody String body) {
|
||||
SocialLoginBody socialLoginBody = JsonUtils.parseObject(body, SocialLoginBody.class);
|
||||
ValidatorUtils.validate(socialLoginBody);
|
||||
|
||||
Long userId = LoginHelper.getUserId();
|
||||
if (userId == null) {
|
||||
return R.fail("请先登录");
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取第三方登录信息
|
||||
AuthResponse<AuthUser> response = SocialUtils.loginAuth(
|
||||
socialLoginBody.getSource(), socialLoginBody.getSocialCode(),
|
||||
socialLoginBody.getSocialState(), socialProperties);
|
||||
AuthUser authUserData = response.getData();
|
||||
// 判断授权响应是否成功
|
||||
if (!response.ok()) {
|
||||
return R.fail(response.getMsg());
|
||||
}
|
||||
// 执行绑定逻辑
|
||||
appSocialAuthService.socialRegister(authUserData);
|
||||
log.info("用户 {} 绑定第三方账号成功,平台: {}", userId, socialLoginBody.getSource());
|
||||
return R.ok("绑定成功");
|
||||
} catch (Exception e) {
|
||||
log.error("绑定第三方账号失败", e);
|
||||
return R.fail("绑定失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑第三方账号
|
||||
*
|
||||
* @param platform 第三方平台
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/unbind/social")
|
||||
public R<Void> unbindSocial(@RequestParam String platform) {
|
||||
Long userId = LoginHelper.getUserId();
|
||||
if (userId == null) {
|
||||
return R.fail("请先登录");
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(platform)) {
|
||||
return R.fail("平台参数不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// 查找并删除绑定关系
|
||||
boolean success = appSocialAuthService.deleteByUserIdAndPlatform(userId, platform);
|
||||
if (success) {
|
||||
log.info("用户 {} 解绑第三方账号成功,平台: {}", userId, platform);
|
||||
return R.ok("解绑成功");
|
||||
} else {
|
||||
log.warn("用户 {} 尝试解绑不存在的第三方账号,平台: {}", userId, platform);
|
||||
return R.fail("未找到绑定关系");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("用户 {} 解绑第三方账号失败,平台: {}", userId, platform, e);
|
||||
return R.fail("解绑失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取App端第三方登录跳转URL
|
||||
*
|
||||
* @param source 登录来源
|
||||
* @param domain 域名
|
||||
* @return 结果
|
||||
*/
|
||||
@GetMapping("/binding/{source}")
|
||||
public R<String> authBinding(@PathVariable("source") String source, @RequestParam String domain) {
|
||||
SocialLoginConfigProperties obj = socialProperties.getType().get(source);
|
||||
if (ObjectUtil.isNull(obj)) {
|
||||
return R.fail(source + "平台账号暂不支持");
|
||||
}
|
||||
AuthRequest authRequest = SocialUtils.getAuthRequest(source, socialProperties);
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("domain", domain);
|
||||
map.put("state", AuthStateUtils.createState());
|
||||
String authorizeUrl = authRequest.authorize(Base64.encode(JsonUtils.toJsonString(map), StandardCharsets.UTF_8));
|
||||
return R.ok("操作成功", authorizeUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* App端社交账号绑定回调(需要token)
|
||||
*
|
||||
* @param loginBody 请求体
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/social/binding")
|
||||
public R<Void> socialBinding(@RequestBody SocialLoginBody loginBody) {
|
||||
// 校验token
|
||||
Long userId = LoginHelper.getUserId();
|
||||
if (userId == null) {
|
||||
return R.fail("请先登录");
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取第三方登录信息
|
||||
AuthResponse<AuthUser> response = SocialUtils.loginAuth(
|
||||
loginBody.getSource(), loginBody.getSocialCode(),
|
||||
loginBody.getSocialState(), socialProperties);
|
||||
AuthUser authUserData = response.getData();
|
||||
// 判断授权响应是否成功
|
||||
if (!response.ok()) {
|
||||
return R.fail(response.getMsg());
|
||||
}
|
||||
appSocialAuthService.socialRegister(authUserData);
|
||||
return R.ok("绑定成功");
|
||||
} catch (Exception e) {
|
||||
log.error("社交账号绑定失败", e);
|
||||
return R.fail("绑定失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户绑定的第三方账号列表
|
||||
*
|
||||
* @return 绑定列表
|
||||
*/
|
||||
@GetMapping("/social/list")
|
||||
public R<?> getSocialList() {
|
||||
Long userId = LoginHelper.getUserId();
|
||||
if (userId == null) {
|
||||
return R.fail("请先登录");
|
||||
}
|
||||
|
||||
try {
|
||||
var socialList = appSocialAuthService.queryByUserId(userId);
|
||||
return R.ok(socialList);
|
||||
} catch (Exception e) {
|
||||
log.error("获取第三方账号绑定列表失败", e);
|
||||
return R.fail("获取失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.dromara.app.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.app.session.AppSession;
|
||||
import org.dromara.app.session.AppSessionManager;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/app/session")
|
||||
public class AppSessionController {
|
||||
|
||||
private final AppSessionManager sessionManager;
|
||||
|
||||
@GetMapping("/active")
|
||||
@SaCheckLogin
|
||||
public R<List<AppSession>> active() {
|
||||
Long userId = LoginHelper.getUserId();
|
||||
return R.ok(sessionManager.list(userId));
|
||||
}
|
||||
|
||||
@PostMapping("/terminate")
|
||||
@SaCheckLogin
|
||||
public R<Boolean> terminate(@RequestParam String sessionId) {
|
||||
Long userId = LoginHelper.getUserId();
|
||||
return R.ok(sessionManager.terminate(userId, sessionId));
|
||||
}
|
||||
|
||||
@PostMapping("/heartbeat")
|
||||
@SaCheckLogin
|
||||
public R<Boolean> heartbeat(@RequestParam String sessionId) {
|
||||
Long userId = LoginHelper.getUserId();
|
||||
return R.ok(sessionManager.heartbeat(userId, sessionId, Duration.ofDays(30)));
|
||||
}
|
||||
|
||||
@GetMapping("/sse")
|
||||
@SaCheckLogin
|
||||
public SseEmitter sse() {
|
||||
Long userId = LoginHelper.getUserId();
|
||||
SseEmitter emitter = new SseEmitter(Duration.ofHours(1).toMillis());
|
||||
String ch = "APP:SESS_EVT:" + userId;
|
||||
org.dromara.common.redis.utils.RedisUtils.subscribe(ch, String.class, msg -> {
|
||||
try {
|
||||
emitter.send(SseEmitter.event().name("session").data(msg));
|
||||
} catch (IOException e) {
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
});
|
||||
return emitter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package org.dromara.app.domain;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* App登录用户身份权限
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@NoArgsConstructor
|
||||
public class AppLoginUser extends LoginUser {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 部门名
|
||||
*/
|
||||
private String deptName;
|
||||
|
||||
/**
|
||||
* 用户唯一标识
|
||||
*/
|
||||
private String token;
|
||||
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private String userType;
|
||||
|
||||
/**
|
||||
* 登录时间
|
||||
*/
|
||||
private Long loginTime;
|
||||
|
||||
/**
|
||||
* 过期时间
|
||||
*/
|
||||
private Long expireTime;
|
||||
|
||||
/**
|
||||
* 登录IP地址
|
||||
*/
|
||||
private String ipaddr;
|
||||
|
||||
/**
|
||||
* 登录地点
|
||||
*/
|
||||
private String loginLocation;
|
||||
|
||||
/**
|
||||
* 浏览器类型
|
||||
*/
|
||||
private String browser;
|
||||
|
||||
/**
|
||||
* 操作系统
|
||||
*/
|
||||
private String os;
|
||||
|
||||
/**
|
||||
* 菜单权限
|
||||
*/
|
||||
private Set<String> menuPermission;
|
||||
|
||||
/**
|
||||
* 角色权限
|
||||
*/
|
||||
private Set<String> rolePermission;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 信息是否完善
|
||||
*/
|
||||
private Boolean infoComplete;
|
||||
|
||||
/**
|
||||
* 获取登录id
|
||||
*/
|
||||
@Override
|
||||
public String getLoginId() {
|
||||
if (userType == null) {
|
||||
throw new IllegalArgumentException("用户类型不能为空");
|
||||
}
|
||||
if (userId == null) {
|
||||
throw new IllegalArgumentException("用户ID不能为空");
|
||||
}
|
||||
return userType + ":" + userId;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.dromara.app.domain.vo;
|
||||
|
||||
public class AppConfigVo {
|
||||
private boolean registerEnabled;
|
||||
|
||||
public boolean isRegisterEnabled() {
|
||||
return registerEnabled;
|
||||
}
|
||||
|
||||
public void setRegisterEnabled(boolean registerEnabled) {
|
||||
this.registerEnabled = registerEnabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package org.dromara.app.listener;
|
||||
|
||||
import cn.dev33.satoken.listener.SaTokenListener;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.http.useragent.UserAgent;
|
||||
import cn.hutool.http.useragent.UserAgentUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.app.service.AppLoginService;
|
||||
import org.dromara.common.core.constant.CacheConstants;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.domain.dto.UserOnlineDTO;
|
||||
import org.dromara.common.core.utils.MessageUtils;
|
||||
import org.dromara.common.core.utils.ServletUtils;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.ip.AddressUtils;
|
||||
import org.dromara.common.log.event.LogininforEvent;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* App用户行为 侦听器的实现
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
@Slf4j
|
||||
public class AppUserActionListener implements SaTokenListener {
|
||||
|
||||
private final AppLoginService appLoginService;
|
||||
|
||||
/**
|
||||
* 每次登录时触发
|
||||
*/
|
||||
@Override
|
||||
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {
|
||||
UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
|
||||
String ip = ServletUtils.getClientIP();
|
||||
UserOnlineDTO dto = new UserOnlineDTO();
|
||||
dto.setIpaddr(ip);
|
||||
dto.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
|
||||
dto.setBrowser(userAgent.getBrowser().getName());
|
||||
dto.setOs(userAgent.getOs().getName());
|
||||
dto.setLoginTime(System.currentTimeMillis());
|
||||
dto.setTokenId(tokenValue);
|
||||
String username = (String) loginParameter.getExtra(LoginHelper.USER_NAME_KEY);
|
||||
dto.setUserName(username);
|
||||
dto.setClientKey((String) loginParameter.getExtra(LoginHelper.CLIENT_KEY));
|
||||
dto.setDeviceType(loginParameter.getDeviceType());
|
||||
dto.setDeptName((String) loginParameter.getExtra(LoginHelper.DEPT_NAME_KEY));
|
||||
if (loginParameter.getTimeout() == -1) {
|
||||
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto);
|
||||
} else {
|
||||
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto, Duration.ofSeconds(loginParameter.getTimeout()));
|
||||
}
|
||||
// 记录登录日志
|
||||
LogininforEvent logininforEvent = new LogininforEvent();
|
||||
logininforEvent.setUsername(username);
|
||||
logininforEvent.setStatus(Constants.LOGIN_SUCCESS);
|
||||
logininforEvent.setMessage(MessageUtils.message("user.login.success"));
|
||||
logininforEvent.setRequest(ServletUtils.getRequest());
|
||||
SpringUtils.context().publishEvent(logininforEvent);
|
||||
// 更新App用户登录信息
|
||||
appLoginService.recordLoginInfo((Long) loginParameter.getExtra(LoginHelper.USER_KEY), ip);
|
||||
log.info("app user doLogin, userId:{}, token:{}", loginId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次注销时触发
|
||||
*/
|
||||
@Override
|
||||
public void doLogout(String loginType, Object loginId, String tokenValue) {
|
||||
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
||||
log.info("app user doLogout, userId:{}, token:{}", loginId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次被踢下线时触发
|
||||
*/
|
||||
@Override
|
||||
public void doKickout(String loginType, Object loginId, String tokenValue) {
|
||||
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
||||
log.info("app user doKickout, userId:{}, token:{}", loginId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次被顶下线时触发
|
||||
*/
|
||||
@Override
|
||||
public void doReplaced(String loginType, Object loginId, String tokenValue) {
|
||||
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
||||
log.info("app user doReplaced, userId:{}, token:{}", loginId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次被封禁时触发
|
||||
*/
|
||||
@Override
|
||||
public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {
|
||||
log.info("App用户被禁用: loginType={}, loginId={}, service={}, level={}, disableTime={}",
|
||||
loginType, loginId, service, level, disableTime);
|
||||
|
||||
// 删除在线用户信息
|
||||
String tokenValue = StpUtil.getTokenValueByLoginId(loginId);
|
||||
if (StringUtils.isNotBlank(tokenValue)) {
|
||||
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次被解封时触发
|
||||
*/
|
||||
@Override
|
||||
public void doUntieDisable(String loginType, Object loginId, String service) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次打开二级认证时触发
|
||||
*/
|
||||
@Override
|
||||
public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次关闭二级认证时触发
|
||||
*/
|
||||
@Override
|
||||
public void doCloseSafe(String loginType, String tokenValue, String service) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次创建Session时触发
|
||||
*/
|
||||
@Override
|
||||
public void doCreateSession(String id) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次注销Session时触发
|
||||
*/
|
||||
@Override
|
||||
public void doLogoutSession(String id) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次Token续期时触发
|
||||
*/
|
||||
@Override
|
||||
public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.dromara.app.service;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.member.domain.vo.AppUserVo;
|
||||
import org.dromara.common.core.utils.ip.AddressUtils;
|
||||
import org.dromara.common.mail.utils.MailUtils;
|
||||
import org.dromara.member.service.AppUserService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* App登录异地风险检测服务
|
||||
* 比对用户上次登录IP归属地与本次登录IP归属地,若发生变化则进行日志与邮件告警。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AppLoginRiskService {
|
||||
|
||||
private final AppUserService appUserService;
|
||||
|
||||
/**
|
||||
* 执行异地登录风险检测
|
||||
* @param userId 当前登录用户ID
|
||||
* @param currentIp 本次登录IP
|
||||
*/
|
||||
public void checkRisk(Long userId, String currentIp) {
|
||||
AppUserVo user = appUserService.queryById(userId);
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
String lastIp = StrUtil.blankToDefault(user.getLoginIp(), "");
|
||||
String lastRegion = AddressUtils.getRealAddressByIP(lastIp);
|
||||
String currentRegion = AddressUtils.getRealAddressByIP(currentIp);
|
||||
boolean validLast = !AddressUtils.UNKNOWN_IP.equals(lastRegion) && !AddressUtils.UNKNOWN_ADDRESS.equals(lastRegion);
|
||||
boolean validCurrent = !AddressUtils.UNKNOWN_IP.equals(currentRegion) && !AddressUtils.UNKNOWN_ADDRESS.equals(currentRegion);
|
||||
boolean isLocal = AddressUtils.LOCAL_ADDRESS.equals(lastRegion) || AddressUtils.LOCAL_ADDRESS.equals(currentRegion);
|
||||
if (validLast && validCurrent && !isLocal && !StrUtil.equals(lastRegion, currentRegion)) {
|
||||
log.warn("App用户 {} 异地登录风险, 上次:{}({}), 本次:{}({})", userId, lastRegion, lastIp, currentRegion, currentIp);
|
||||
String email = user.getEmail();
|
||||
if (StrUtil.isNotBlank(email)) {
|
||||
String subject = "异地登录告警";
|
||||
String content = "您的账号检测到异地登录风险\n上次登录: " + lastRegion + " (" + lastIp + ")\n本次登录: " + currentRegion + " (" + currentIp + ")";
|
||||
try {
|
||||
MailUtils.sendText(email, subject, content);
|
||||
} catch (Exception e) {
|
||||
log.error("发送异地登录告警邮件失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package org.dromara.app.service;
|
||||
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.app.domain.AppLoginUser;
|
||||
import org.dromara.member.domain.AppUser;
|
||||
import org.dromara.member.domain.bo.AppUserBo;
|
||||
import org.dromara.common.core.constant.CacheConstants;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.enums.LoginType;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.MessageUtils;
|
||||
import org.dromara.common.core.utils.ServletUtils;
|
||||
import org.dromara.common.log.event.LogininforEvent;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.member.service.AppUserService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* App登录校验方法
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AppLoginService {
|
||||
|
||||
private final AppUserService appUserService;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
@Value("${user.password.maxRetryCount}")
|
||||
private Integer maxRetryCount;
|
||||
@Value("${user.password.lockTime}")
|
||||
private Integer lockTime;
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
public void logout() {
|
||||
try {
|
||||
AppLoginUser loginUser = LoginHelper.getLoginUser();
|
||||
if (ObjectUtil.isNotNull(loginUser)) {
|
||||
StpUtil.logout();
|
||||
recordLogininfor(loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success"));
|
||||
}
|
||||
} catch (NotLoginException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册
|
||||
*/
|
||||
public Boolean register(AppUserBo bo) {
|
||||
return appUserService.register(bo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验用户名唯一
|
||||
*/
|
||||
public boolean checkUserNameUnique(String userName) {
|
||||
return appUserService.checkUserNameUnique(userName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录信息
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param status 状态
|
||||
* @param message 消息内容
|
||||
*/
|
||||
public void recordLogininfor(String username, String status, String message) {
|
||||
LogininforEvent logininforEvent = new LogininforEvent();
|
||||
logininforEvent.setUsername(username);
|
||||
logininforEvent.setStatus(status);
|
||||
logininforEvent.setMessage(message);
|
||||
logininforEvent.setRequest(ServletUtils.getRequest());
|
||||
eventPublisher.publishEvent(logininforEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建登录用户
|
||||
*/
|
||||
public AppLoginUser buildLoginUser(AppUser appUser) {
|
||||
AppLoginUser loginUser = new AppLoginUser();
|
||||
loginUser.setUserId(appUser.getUserId());
|
||||
loginUser.setUsername(appUser.getUserName());
|
||||
loginUser.setUserType("app");
|
||||
loginUser.setInfoComplete(StringUtils.isNotBlank(appUser.getPhone()));
|
||||
// 可以根据需要添加更多字段
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param ip IP地址
|
||||
*/
|
||||
public void recordLoginInfo(Long userId, String ip) {
|
||||
AppUser appUser = new AppUser();
|
||||
appUser.setUserId(userId);
|
||||
appUser.setLoginIp(ip);
|
||||
appUser.setLoginDate(LocalDateTime.now());
|
||||
appUserService.updateById(appUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录校验
|
||||
*/
|
||||
public void checkLogin(LoginType loginType, String username, Supplier<Boolean> supplier) {
|
||||
String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
|
||||
String loginFail = Constants.LOGIN_FAIL;
|
||||
|
||||
// 获取用户登录错误次数,默认为0 (可自定义限制策略 例如: key + username + ip)
|
||||
int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
|
||||
// 锁定时间内登录 则踢出
|
||||
if (errorNumber >= maxRetryCount) {
|
||||
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
|
||||
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
|
||||
}
|
||||
|
||||
if (supplier.get()) {
|
||||
// 是否第一次
|
||||
errorNumber++;
|
||||
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
|
||||
// 达到规定错误次数 则锁定登录
|
||||
if (errorNumber >= maxRetryCount) {
|
||||
RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
|
||||
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
|
||||
} else {
|
||||
RedisUtils.setCacheObject(errorKey, errorNumber);
|
||||
throw new UserException(loginType.getRetryLimitCount(), errorNumber);
|
||||
}
|
||||
}
|
||||
|
||||
// 登录成功 清空错误次数
|
||||
RedisUtils.deleteObject(errorKey);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.dromara.app.service;
|
||||
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
|
||||
/**
|
||||
* App认证策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface IAppAuthStrategy {
|
||||
|
||||
String BASE_NAME = "AppAuthStrategy";
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*
|
||||
* @param body 登录对象
|
||||
* @param client 授权管理视图对象
|
||||
* @param grantType 授权类型
|
||||
* @return 登录验证信息
|
||||
*/
|
||||
static LoginVo authenticate(String body, SysClientVo client, String grantType) {
|
||||
// 授权类型和客户端id
|
||||
String beanName = grantType + BASE_NAME;
|
||||
if (!SpringUtils.containsBean(beanName)) {
|
||||
throw new ServiceException("授权类型不正确!");
|
||||
}
|
||||
IAppAuthStrategy instance = SpringUtils.getBean(beanName);
|
||||
return instance.login(body, client);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*
|
||||
* @param body 登录对象
|
||||
* @param client 授权管理视图对象
|
||||
* @return 登录验证信息
|
||||
*/
|
||||
LoginVo login(String body, SysClientVo client);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package org.dromara.app.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.app.domain.AppLoginUser;
|
||||
import org.dromara.member.domain.vo.AppUserVo;
|
||||
import org.dromara.app.service.AppLoginService;
|
||||
import org.dromara.member.service.AppUserService;
|
||||
import org.dromara.app.service.IAppAuthStrategy;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.domain.model.EmailLoginBody;
|
||||
import org.dromara.common.core.enums.LoginType;
|
||||
import org.dromara.common.core.exception.user.CaptchaExpireException;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.MessageUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* App邮箱认证策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("app_email" + IAppAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class AppEmailAuthStrategy implements IAppAuthStrategy {
|
||||
|
||||
private final AppLoginService appLoginService;
|
||||
private final AppUserService appUserService;
|
||||
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
EmailLoginBody loginBody = JsonUtils.parseObject(body, EmailLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
String email = loginBody.getEmail();
|
||||
String emailCode = loginBody.getEmailCode();
|
||||
|
||||
AppUserVo user = loadUserByEmail(email);
|
||||
appLoginService.checkLogin(LoginType.EMAIL, user.getUserName(), () -> !validateEmailCode(email, emailCode));
|
||||
// 构建App登录用户
|
||||
AppLoginUser loginUser = new AppLoginUser();
|
||||
loginUser.setUserId(user.getUserId());
|
||||
loginUser.setUsername(user.getUserName());
|
||||
loginUser.setUserType("app");
|
||||
|
||||
loginUser.setClientKey(client.getClientKey());
|
||||
loginUser.setDeviceType(client.getDeviceType());
|
||||
SaLoginParameter model = new SaLoginParameter();
|
||||
model.setDeviceType(client.getDeviceType());
|
||||
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
|
||||
// 例如: 后台用户30分钟过期 app用户1天过期
|
||||
model.setTimeout(client.getTimeout());
|
||||
model.setActiveTimeout(client.getActiveTimeout());
|
||||
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
|
||||
// 生成token
|
||||
LoginHelper.login(loginUser, model);
|
||||
|
||||
LoginVo loginVo = new LoginVo();
|
||||
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||
loginVo.setClientId(client.getClientId());
|
||||
return loginVo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验邮箱验证码
|
||||
*/
|
||||
private boolean validateEmailCode(String email, String emailCode) {
|
||||
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + email);
|
||||
if (StringUtils.isBlank(code)) {
|
||||
appLoginService.recordLogininfor(email, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
return code.equals(emailCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过邮箱加载用户信息
|
||||
*/
|
||||
private AppUserVo loadUserByEmail(String email) {
|
||||
AppUserVo appUser = appUserService.selectUserByEmail(email);
|
||||
if (ObjectUtil.isNull(appUser)) {
|
||||
log.info("登录用户:{} 不存在.", email);
|
||||
throw new UserException("user.not.exists", email);
|
||||
} else if ("1".equals(appUser.getStatus())) {
|
||||
log.info("登录用户:{} 已被停用.", email);
|
||||
throw new UserException("user.blocked", email);
|
||||
}
|
||||
// 将 AppUser 转换为 AppUserVo
|
||||
AppUserVo user = new AppUserVo();
|
||||
BeanUtil.copyProperties(appUser, user);
|
||||
return user;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package org.dromara.app.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.app.domain.AppLoginUser;
|
||||
import org.dromara.member.domain.AppUser;
|
||||
import org.dromara.member.domain.vo.AppUserVo;
|
||||
import org.dromara.app.service.AppLoginService;
|
||||
import org.dromara.member.service.AppUserService;
|
||||
import org.dromara.app.service.IAppAuthStrategy;
|
||||
import org.dromara.app.session.AppSession;
|
||||
import org.dromara.app.session.AppSessionManager;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.domain.model.PasswordLoginBody;
|
||||
import org.dromara.common.core.enums.LoginType;
|
||||
import org.dromara.common.core.exception.user.CaptchaException;
|
||||
import org.dromara.common.core.exception.user.CaptchaExpireException;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.MessageUtils;
|
||||
import org.dromara.common.core.utils.ServletUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* App密码认证策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("app_password" + IAppAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class AppPasswordAuthStrategy implements IAppAuthStrategy {
|
||||
|
||||
private final CaptchaProperties captchaProperties;
|
||||
private final AppLoginService appLoginService;
|
||||
private final AppUserService appUserService;
|
||||
private final AppSessionManager appSessionManager;
|
||||
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
PasswordLoginBody loginBody = JsonUtils.parseObject(body, PasswordLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
String username = loginBody.getUsername();
|
||||
String password = loginBody.getPassword();
|
||||
String code = loginBody.getCode();
|
||||
String uuid = loginBody.getUuid();
|
||||
|
||||
boolean captchaEnabled = captchaProperties.getEnable();
|
||||
// 验证码开关
|
||||
// if (captchaEnabled) {
|
||||
// validateCaptcha(username, code, uuid);
|
||||
// }
|
||||
|
||||
AppUser appUser = loadUserByUsername(username);
|
||||
appLoginService.checkLogin(LoginType.PASSWORD, username, () -> !BCrypt.checkpw(password, appUser.getPassword()));
|
||||
|
||||
// 创建App登录用户
|
||||
AppLoginUser loginUser = buildAppLoginUser(appUser);
|
||||
|
||||
loginUser.setClientKey(client.getClientKey());
|
||||
loginUser.setDeviceType(client.getDeviceType());
|
||||
SaLoginParameter model = new SaLoginParameter();
|
||||
model.setDeviceType(client.getDeviceType());
|
||||
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
|
||||
// 例如: 后台用户30分钟过期 app用户1天过期
|
||||
model.setTimeout(client.getTimeout());
|
||||
model.setActiveTimeout(client.getActiveTimeout());
|
||||
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
|
||||
// 生成token
|
||||
LoginHelper.login(loginUser, model);
|
||||
|
||||
LoginVo loginVo = new LoginVo();
|
||||
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||
loginVo.setClientId(client.getClientId());
|
||||
|
||||
// 注册会话
|
||||
try {
|
||||
String ua = ServletUtils.getRequest().getHeader("User-Agent");
|
||||
String deviceId = ServletUtils.getRequest().getHeader("X-Device-Id");
|
||||
String ip = LoginHelper.getLoginUser().getIpaddr();
|
||||
AppSession s = new AppSession();
|
||||
s.setSessionId(StpUtil.getTokenValue());
|
||||
s.setUserId(loginUser.getUserId());
|
||||
s.setDeviceType(client.getDeviceType());
|
||||
s.setDeviceId(deviceId != null ? deviceId : UUID.randomUUID().toString());
|
||||
s.setUserAgent(ua);
|
||||
s.setIp(ip);
|
||||
s.setLocation("");
|
||||
long now = Instant.now().toEpochMilli();
|
||||
s.setLoginAt(now);
|
||||
Long ttlSec = StpUtil.getTokenTimeout();
|
||||
s.setExpireAt(now + (ttlSec != null ? ttlSec * 1000 : 24 * 3600 * 1000));
|
||||
s.setActive(true);
|
||||
s.setToken(StpUtil.getTokenValue());
|
||||
appSessionManager.register(s, Duration.ofSeconds(ttlSec != null ? ttlSec : 86400));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return loginVo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验验证码
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param code 验证码
|
||||
* @param uuid 唯一标识
|
||||
*/
|
||||
private void validateCaptcha(String username, String code, String uuid) {
|
||||
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, "");
|
||||
String captcha = RedisUtils.getCacheObject(verifyKey);
|
||||
RedisUtils.deleteObject(verifyKey);
|
||||
if (captcha == null) {
|
||||
appLoginService.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
if (!code.equalsIgnoreCase(captcha)) {
|
||||
appLoginService.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
|
||||
throw new CaptchaException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名加载App用户
|
||||
*/
|
||||
private AppUser loadUserByUsername(String username) {
|
||||
AppUserVo user = appUserService.selectUserByUserName(username);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
log.info("登录用户:{} 不存在.", username);
|
||||
throw new UserException("user.not.exists", username);
|
||||
} else if ("1".equals(user.getStatus())) {
|
||||
log.info("登录用户:{} 已被停用.", username);
|
||||
throw new UserException("user.blocked", username);
|
||||
}
|
||||
return JsonUtils.parseObject(JsonUtils.toJsonString(user), AppUser.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建App登录用户
|
||||
*/
|
||||
private AppLoginUser buildAppLoginUser(AppUser appUser) {
|
||||
AppLoginUser loginUser = new AppLoginUser();
|
||||
loginUser.setUserId(appUser.getUserId());
|
||||
loginUser.setUsername(appUser.getUserName());
|
||||
loginUser.setUserType("app");
|
||||
// 设置其他必要字段
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package org.dromara.app.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.app.domain.AppLoginUser;
|
||||
import org.dromara.member.domain.vo.AppUserVo;
|
||||
import org.dromara.app.service.AppLoginService;
|
||||
import org.dromara.member.service.AppUserService;
|
||||
import org.dromara.app.service.IAppAuthStrategy;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.domain.model.SmsLoginBody;
|
||||
import org.dromara.common.core.enums.LoginType;
|
||||
import org.dromara.common.core.exception.user.CaptchaExpireException;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.MessageUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* App短信认证策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("app_sms" + IAppAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class AppSmsAuthStrategy implements IAppAuthStrategy {
|
||||
|
||||
private final AppLoginService appLoginService;
|
||||
private final AppUserService appUserService;
|
||||
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
SmsLoginBody loginBody = JsonUtils.parseObject(body, SmsLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
String phonenumber = loginBody.getPhonenumber();
|
||||
String smsCode = loginBody.getSmsCode();
|
||||
|
||||
AppUserVo user = loadUserByPhonenumber(phonenumber);
|
||||
appLoginService.checkLogin(LoginType.SMS, user.getUserName(), () -> !validateSmsCode(phonenumber, smsCode));
|
||||
// 构建App登录用户
|
||||
AppLoginUser loginUser = new AppLoginUser();
|
||||
loginUser.setUserId(user.getUserId());
|
||||
loginUser.setUsername(user.getUserName());
|
||||
loginUser.setUserType("app");
|
||||
|
||||
loginUser.setClientKey(client.getClientKey());
|
||||
loginUser.setDeviceType(client.getDeviceType());
|
||||
SaLoginParameter model = new SaLoginParameter();
|
||||
model.setDeviceType(client.getDeviceType());
|
||||
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
|
||||
// 例如: 后台用户30分钟过期 app用户1天过期
|
||||
model.setTimeout(client.getTimeout());
|
||||
model.setActiveTimeout(client.getActiveTimeout());
|
||||
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
|
||||
// 生成token
|
||||
LoginHelper.login(loginUser, model);
|
||||
|
||||
LoginVo loginVo = new LoginVo();
|
||||
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||
loginVo.setClientId(client.getClientId());
|
||||
return loginVo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验短信验证码
|
||||
*/
|
||||
private boolean validateSmsCode(String phonenumber, String smsCode) {
|
||||
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + phonenumber);
|
||||
if (StringUtils.isBlank(code)) {
|
||||
appLoginService.recordLogininfor(phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
return code.equals(smsCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过手机号加载用户信息
|
||||
*/
|
||||
private AppUserVo loadUserByPhonenumber(String phonenumber) {
|
||||
AppUserVo appUser = appUserService.selectUserByPhone(phonenumber);
|
||||
if (ObjectUtil.isNull(appUser)) {
|
||||
log.info("登录用户:{} 不存在.", phonenumber);
|
||||
throw new UserException("user.not.exists", phonenumber);
|
||||
} else if ("1".equals(appUser.getStatus())) {
|
||||
log.info("登录用户:{} 已被停用.", phonenumber);
|
||||
throw new UserException("user.blocked", phonenumber);
|
||||
}
|
||||
// 将 AppUser 转换为 AppUserVo
|
||||
AppUserVo user = new AppUserVo();
|
||||
BeanUtil.copyProperties(appUser, user);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
log.info("登录用户:{} 不存在.", phonenumber);
|
||||
throw new UserException("user.not.exists", phonenumber);
|
||||
} else if ("1".equals(user.getStatus())) {
|
||||
log.info("登录用户:{} 已被停用.", phonenumber);
|
||||
throw new UserException("user.blocked", phonenumber);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package org.dromara.app.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.zhyd.oauth.model.AuthCallback;
|
||||
import me.zhyd.oauth.model.AuthResponse;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import me.zhyd.oauth.request.AuthRequest;
|
||||
import org.dromara.app.domain.AppLoginUser;
|
||||
import org.dromara.member.domain.AppSocialAuth;
|
||||
import org.dromara.member.domain.vo.AppUserVo;
|
||||
import org.dromara.app.service.AppLoginService;
|
||||
import org.dromara.member.service.AppSocialAuthService;
|
||||
import org.dromara.member.service.AppUserService;
|
||||
import org.dromara.app.service.IAppAuthStrategy;
|
||||
import org.dromara.common.core.domain.model.SocialLoginBody;
|
||||
import org.dromara.common.core.enums.LoginType;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.social.config.properties.SocialProperties;
|
||||
import org.dromara.common.social.utils.SocialUtils;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* App第三方登录认证策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("app_social" + IAppAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class AppSocialAuthStrategy implements IAppAuthStrategy {
|
||||
|
||||
private final SocialProperties socialProperties;
|
||||
private final AppUserService appUserService;
|
||||
private final AppLoginService appLoginService;
|
||||
private final AppSocialAuthService appSocialAuthService;
|
||||
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
SocialLoginBody loginBody = JsonUtils.parseObject(body, SocialLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
String source = loginBody.getSource();
|
||||
String socialCode = loginBody.getSocialCode();
|
||||
String socialState = loginBody.getSocialState();
|
||||
// 获取第三方登录信息
|
||||
AuthRequest authRequest = SocialUtils.getAuthRequest(source, socialProperties);
|
||||
AuthCallback callback = AuthCallback.builder()
|
||||
.code(socialCode)
|
||||
.state(socialState)
|
||||
.build();
|
||||
AuthResponse<AuthUser> response = authRequest.login(callback);
|
||||
if (!response.ok()) {
|
||||
throw new ServiceException(response.getMsg());
|
||||
}
|
||||
AuthUser authUserData = response.getData();
|
||||
|
||||
AppUserVo user = loadUserBySocial(authUserData.getSource(), authUserData.getUuid());
|
||||
assert user != null;
|
||||
appLoginService.checkLogin(LoginType.PASSWORD, user.getUserName(), () -> ObjectUtil.isNull(user));
|
||||
// 构建App登录用户
|
||||
AppLoginUser loginUser = new AppLoginUser();
|
||||
loginUser.setUserId(user.getUserId());
|
||||
loginUser.setUsername(user.getUserName());
|
||||
loginUser.setUserType("app");
|
||||
|
||||
loginUser.setClientKey(client.getClientKey());
|
||||
loginUser.setDeviceType(client.getDeviceType());
|
||||
SaLoginParameter model = new SaLoginParameter();
|
||||
model.setDeviceType(client.getDeviceType());
|
||||
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
|
||||
// 例如: 后台用户30分钟过期 app用户1天过期
|
||||
model.setTimeout(client.getTimeout());
|
||||
model.setActiveTimeout(client.getActiveTimeout());
|
||||
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
|
||||
// 生成token
|
||||
LoginHelper.login(loginUser, model);
|
||||
|
||||
LoginVo loginVo = new LoginVo();
|
||||
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||
loginVo.setClientId(client.getClientId());
|
||||
return loginVo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过第三方信息查找用户
|
||||
*/
|
||||
private AppUserVo loadUserBySocial(String source, String openId) {
|
||||
// 根据第三方平台信息查找绑定的App用户
|
||||
log.info("App端第三方登录查找用户,source: {}, openId: {}", source, openId);
|
||||
|
||||
// 首先通过openId查找
|
||||
AppSocialAuth socialAuth = appSocialAuthService.queryByPlatformAndOpenId(source, openId);
|
||||
|
||||
if (socialAuth != null && socialAuth.getUserId() != null) {
|
||||
// 找到绑定的用户,返回用户信息
|
||||
AppUserVo user = appUserService.queryById(socialAuth.getUserId());
|
||||
if (user != null) {
|
||||
log.info("通过openId找到绑定用户,userId: {}, userName: {}", user.getUserId(), user.getUserName());
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
log.warn("未找到与第三方账号绑定的App用户,source: {}, openId: {}", source, openId);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package org.dromara.app.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.zhyd.oauth.model.AuthCallback;
|
||||
import me.zhyd.oauth.model.AuthResponse;
|
||||
import me.zhyd.oauth.model.AuthToken;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import me.zhyd.oauth.request.AuthRequest;
|
||||
import org.dromara.app.domain.AppLoginUser;
|
||||
import org.dromara.member.domain.vo.AppUserVo;
|
||||
import org.dromara.member.service.AppSocialAuthService;
|
||||
import org.dromara.app.service.IAppAuthStrategy;
|
||||
import org.dromara.common.core.domain.model.XcxLoginBody;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.social.config.properties.SocialProperties;
|
||||
import org.dromara.common.social.utils.SocialUtils;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* App端微信小程序认证策略
|
||||
* 支持微信小程序登录,通过微信小程序授权码获取用户信息并完成登录
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("app_wechat_miniprogram" + IAppAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class AppWechatMiniprogramAuthStrategy implements IAppAuthStrategy {
|
||||
|
||||
private final SocialProperties socialProperties;
|
||||
private final AppSocialAuthService appSocialAuthService;
|
||||
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
XcxLoginBody loginBody = JsonUtils.parseObject(body, XcxLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
|
||||
// xcxCode 为小程序调用 wx.login 授权后获取
|
||||
String xcxCode = loginBody.getCode();
|
||||
// 多个小程序识别使用
|
||||
String appid = loginBody.getAppid();
|
||||
|
||||
// 校验 appid + appsecret + xcxCode 调用登录凭证校验接口 获取 session_key 与 openid
|
||||
// 使用SocialUtils获取微信小程序的AuthRequest
|
||||
AuthRequest authRequest = SocialUtils.getAuthRequest("wechat_miniprogram", socialProperties);
|
||||
if (authRequest == null) {
|
||||
throw new UserException("未配置微信小程序登录");
|
||||
}
|
||||
AuthCallback authCallback = new AuthCallback();
|
||||
authCallback.setCode(xcxCode);
|
||||
AuthResponse<AuthUser> response = authRequest.login(authCallback);
|
||||
|
||||
String openid = null;
|
||||
String unionId = null;
|
||||
if (response.ok()) {
|
||||
AuthToken token = response.getData().getToken();
|
||||
openid = token.getOpenId();
|
||||
// 微信小程序只有关联到微信开放平台下之后才能获取到 unionId,因此unionId不一定能返回。
|
||||
unionId = token.getUnionId();
|
||||
} else {
|
||||
throw new UserException("第三方登录失败,原因:" + response.getMsg());
|
||||
}
|
||||
|
||||
AppUserVo user = loadUserByWxId("wechat_miniprogram", openid, unionId);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
throw new UserException("登录失败,请先绑定用户账号");
|
||||
}
|
||||
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
AppLoginUser loginUser = new AppLoginUser();
|
||||
loginUser.setUserId(user.getUserId());
|
||||
loginUser.setUsername(user.getUserName());
|
||||
loginUser.setNickname(user.getNickName());
|
||||
loginUser.setUserType("app");
|
||||
loginUser.setClientKey(client.getClientKey());
|
||||
loginUser.setDeviceType(client.getDeviceType());
|
||||
|
||||
SaLoginParameter model = new SaLoginParameter();
|
||||
model.setDeviceType(client.getDeviceType());
|
||||
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
|
||||
// 例如: 后台用户30分钟过期 app用户1天过期
|
||||
model.setTimeout(client.getTimeout());
|
||||
model.setActiveTimeout(client.getActiveTimeout());
|
||||
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
|
||||
// 生成token
|
||||
LoginHelper.login(loginUser, model);
|
||||
|
||||
LoginVo loginVo = new LoginVo();
|
||||
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||
loginVo.setClientId(client.getClientId());
|
||||
loginVo.setOpenid(openid);
|
||||
return loginVo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过微信标识加载用户信息,优先 unionId,回退 openId
|
||||
*/
|
||||
private AppUserVo loadUserByWxId(String platform, String openid, String unionId) {
|
||||
if (ObjectUtil.isNotEmpty(unionId)) {
|
||||
AppUserVo byUnion = appSocialAuthService.selectUserByPlatformAndUnionId(platform, unionId);
|
||||
if (ObjectUtil.isNotNull(byUnion)) {
|
||||
return byUnion;
|
||||
}
|
||||
}
|
||||
return appSocialAuthService.selectUserByPlatformAndOpenId(platform, openid);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package org.dromara.app.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.zhyd.oauth.model.AuthCallback;
|
||||
import me.zhyd.oauth.model.AuthResponse;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import me.zhyd.oauth.request.AuthRequest;
|
||||
import org.dromara.app.domain.AppLoginUser;
|
||||
import org.dromara.member.domain.vo.AppUserVo;
|
||||
import org.dromara.member.service.AppSocialAuthService;
|
||||
import org.dromara.app.service.IAppAuthStrategy;
|
||||
import org.dromara.common.core.domain.model.SocialLoginBody;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.social.config.properties.SocialProperties;
|
||||
import org.dromara.common.social.utils.SocialUtils;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* App端微信公众号认证策略
|
||||
* 处理微信公众号环境下的OAuth2登录逻辑
|
||||
* 依赖 justauth.type.wechat_mp 配置项
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("app_wechat_mp" + IAppAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class AppWechatMpAuthStrategy implements IAppAuthStrategy {
|
||||
|
||||
private final SocialProperties socialProperties;
|
||||
private final AppSocialAuthService appSocialAuthService;
|
||||
|
||||
/**
|
||||
* 微信公众号登录核心逻辑
|
||||
*
|
||||
* @param body 登录请求体,包含 socialCode 和 socialState
|
||||
* @param client 客户端配置
|
||||
* @return 登录结果 LoginVo
|
||||
*/
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
SocialLoginBody loginBody = JsonUtils.parseObject(body, SocialLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
|
||||
String code = loginBody.getSocialCode();
|
||||
String state = loginBody.getSocialState();
|
||||
|
||||
// 使用SocialUtils获取微信公众号的AuthRequest
|
||||
// 对应 application.yml 中 justauth.type.wechat_mp 的配置
|
||||
AuthRequest authRequest = SocialUtils.getAuthRequest("wechat_mp", socialProperties);
|
||||
if (authRequest == null) {
|
||||
throw new UserException("未配置微信公众号登录");
|
||||
}
|
||||
|
||||
// 构造回调参数
|
||||
AuthCallback authCallback = new AuthCallback();
|
||||
authCallback.setCode(code);
|
||||
authCallback.setState(state);
|
||||
// 执行第三方登录请求,换取 Access Token 和 User Info
|
||||
AuthResponse<AuthUser> response = authRequest.login(authCallback);
|
||||
|
||||
String openid;
|
||||
String unionId = null;
|
||||
if (response.ok()) {
|
||||
AuthUser authUser = response.getData();
|
||||
openid = authUser.getUuid();
|
||||
if (authUser.getToken() != null) {
|
||||
unionId = authUser.getToken().getUnionId();
|
||||
}
|
||||
} else {
|
||||
throw new UserException("第三方登录失败,原因:" + response.getMsg());
|
||||
}
|
||||
|
||||
// 根据 OpenId 或 UnionId 查找系统用户
|
||||
AppUserVo user = loadUserByWxId("wechat_mp", openid, unionId);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
throw new UserException("登录失败,请先绑定用户账号");
|
||||
}
|
||||
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
AppLoginUser loginUser = new AppLoginUser();
|
||||
loginUser.setUserId(user.getUserId());
|
||||
loginUser.setUsername(user.getUserName());
|
||||
loginUser.setNickname(user.getNickName());
|
||||
loginUser.setUserType("app");
|
||||
loginUser.setClientKey(client.getClientKey());
|
||||
loginUser.setDeviceType(client.getDeviceType());
|
||||
SaLoginParameter model = new SaLoginParameter();
|
||||
model.setDeviceType(client.getDeviceType());
|
||||
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
|
||||
// 例如: 后台用户30分钟过期 app用户1天过期
|
||||
model.setTimeout(client.getTimeout());
|
||||
model.setActiveTimeout(client.getActiveTimeout());
|
||||
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
|
||||
// 生成token
|
||||
LoginHelper.login(loginUser, model);
|
||||
|
||||
LoginVo loginVo = new LoginVo();
|
||||
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||
loginVo.setClientId(client.getClientId());
|
||||
loginVo.setOpenid(openid);
|
||||
return loginVo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过微信标识加载用户信息,优先使用 unionId(跨应用统一标识),缺失时回退使用 openId
|
||||
* @param platform 平台标识(wechat_mp / wechat_open / wechat_miniprogram)
|
||||
* @param openid 微信 openId
|
||||
* @param unionId 微信 unionId(可能为空)
|
||||
*/
|
||||
private AppUserVo loadUserByWxId(String platform, String openid, String unionId) {
|
||||
if (ObjectUtil.isNotEmpty(unionId)) {
|
||||
AppUserVo byUnion = appSocialAuthService.selectUserByPlatformAndUnionId(platform, unionId);
|
||||
if (ObjectUtil.isNotNull(byUnion)) {
|
||||
return byUnion;
|
||||
}
|
||||
}
|
||||
return appSocialAuthService.selectUserByPlatformAndOpenId(platform, openid);
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package org.dromara.app.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.zhyd.oauth.model.AuthCallback;
|
||||
import me.zhyd.oauth.model.AuthResponse;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import me.zhyd.oauth.request.AuthRequest;
|
||||
import org.dromara.app.domain.AppLoginUser;
|
||||
import org.dromara.member.domain.vo.AppUserVo;
|
||||
import org.dromara.member.service.AppSocialAuthService;
|
||||
import org.dromara.app.service.IAppAuthStrategy;
|
||||
import org.dromara.common.core.domain.model.SocialLoginBody;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.social.config.properties.SocialProperties;
|
||||
import org.dromara.common.social.utils.SocialUtils;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* App端微信开放平台认证策略
|
||||
* 处理微信开放平台(APP端/网站应用)环境下的OAuth2登录逻辑
|
||||
* 依赖 justauth.type.wechat_open 配置项
|
||||
* 通常用于APP原生微信登录
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("app_wechat_open" + IAppAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class AppWechatOpenAuthStrategy implements IAppAuthStrategy {
|
||||
|
||||
private final SocialProperties socialProperties;
|
||||
private final AppSocialAuthService appSocialAuthService;
|
||||
|
||||
/**
|
||||
* 微信开放平台登录核心逻辑
|
||||
*
|
||||
* @param body 登录请求体,包含 socialCode 和 socialState
|
||||
* @param client 客户端配置
|
||||
* @return 登录结果 LoginVo
|
||||
*/
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
SocialLoginBody loginBody = JsonUtils.parseObject(body, SocialLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
|
||||
String code = loginBody.getSocialCode();
|
||||
String state = loginBody.getSocialState();
|
||||
|
||||
// 使用SocialUtils获取微信开放平台的AuthRequest
|
||||
// 对应 application.yml 中 justauth.type.wechat_open 的配置
|
||||
AuthRequest authRequest = SocialUtils.getAuthRequest("wechat_open", socialProperties);
|
||||
if (authRequest == null) {
|
||||
throw new UserException("未配置微信开放平台登录");
|
||||
}
|
||||
|
||||
// 构造回调参数
|
||||
AuthCallback authCallback = new AuthCallback();
|
||||
authCallback.setCode(code);
|
||||
authCallback.setState(state);
|
||||
// 执行第三方登录请求,换取 Access Token 和 User Info
|
||||
AuthResponse<AuthUser> response = authRequest.login(authCallback);
|
||||
|
||||
String openid;
|
||||
String unionId = null;
|
||||
if (response.ok()) {
|
||||
AuthUser authUser = response.getData();
|
||||
openid = authUser.getUuid();
|
||||
if (authUser.getToken() != null) {
|
||||
unionId = authUser.getToken().getUnionId();
|
||||
}
|
||||
} else {
|
||||
throw new UserException("第三方登录失败,原因:" + response.getMsg());
|
||||
}
|
||||
|
||||
// 根据 OpenId 或 UnionId 查找系统用户
|
||||
AppUserVo user = loadUserByWxId("wechat_open", openid, unionId);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
throw new UserException("登录失败,请先绑定用户账号");
|
||||
}
|
||||
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
AppLoginUser loginUser = new AppLoginUser();
|
||||
loginUser.setUserId(user.getUserId());
|
||||
loginUser.setUsername(user.getUserName());
|
||||
loginUser.setNickname(user.getNickName());
|
||||
loginUser.setUserType("app");
|
||||
loginUser.setClientKey(client.getClientKey());
|
||||
loginUser.setDeviceType(client.getDeviceType());
|
||||
SaLoginParameter model = new SaLoginParameter();
|
||||
model.setDeviceType(client.getDeviceType());
|
||||
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
|
||||
// 例如: 后台用户30分钟过期 app用户1天过期
|
||||
model.setTimeout(client.getTimeout());
|
||||
model.setActiveTimeout(client.getActiveTimeout());
|
||||
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
|
||||
// 生成token
|
||||
LoginHelper.login(loginUser, model);
|
||||
|
||||
LoginVo loginVo = new LoginVo();
|
||||
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||
loginVo.setClientId(client.getClientId());
|
||||
loginVo.setOpenid(openid);
|
||||
return loginVo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过微信标识加载用户信息,优先 unionId,回退 openId
|
||||
*/
|
||||
private AppUserVo loadUserByWxId(String platform, String openid, String unionId) {
|
||||
if (ObjectUtil.isNotEmpty(unionId)) {
|
||||
AppUserVo byUnion = appSocialAuthService.selectUserByPlatformAndUnionId(platform, unionId);
|
||||
if (ObjectUtil.isNotNull(byUnion)) {
|
||||
return byUnion;
|
||||
}
|
||||
}
|
||||
return appSocialAuthService.selectUserByPlatformAndOpenId(platform, openid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.dromara.app.session;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AppSession {
|
||||
private String sessionId;
|
||||
private Long userId;
|
||||
private String deviceType;
|
||||
private String deviceId;
|
||||
private String userAgent;
|
||||
private String ip;
|
||||
private String location;
|
||||
private long loginAt;
|
||||
private long expireAt;
|
||||
private boolean active;
|
||||
private String token;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.dromara.app.session;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AppSessionManager {
|
||||
private static final String KEY_INDEX_FMT = "APP:SESS_INDEX:%s";
|
||||
private static final String KEY_SESS_FMT = "APP:SESS:%s:%s";
|
||||
private static final String CH_EVT_FMT = "APP:SESS_EVT:%s";
|
||||
|
||||
public void register(AppSession s, Duration ttl) {
|
||||
String indexKey = indexKey(s.getUserId());
|
||||
String sessKey = sessKey(s.getUserId(), s.getSessionId());
|
||||
RedisUtils.addCacheSet(indexKey, sessKey);
|
||||
RedisUtils.setCacheObject(sessKey, JsonUtils.toJsonString(s), ttl);
|
||||
publishEvent(s.getUserId(), "REGISTER", s.getSessionId());
|
||||
}
|
||||
|
||||
public List<AppSession> list(Long userId) {
|
||||
String indexKey = indexKey(userId);
|
||||
Set<String> keys = RedisUtils.getCacheSet(indexKey);
|
||||
List<AppSession> out = new ArrayList<>();
|
||||
if (keys != null) {
|
||||
for (String k : keys) {
|
||||
String val = RedisUtils.getCacheObject(k);
|
||||
if (val != null) {
|
||||
out.add(JsonUtils.parseObject(val, AppSession.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public boolean terminate(Long userId, String sessionId) {
|
||||
String indexKey = indexKey(userId);
|
||||
String sessKey = sessKey(userId, sessionId);
|
||||
RedisUtils.deleteObject(sessKey);
|
||||
try {
|
||||
org.redisson.api.RSet<String> set = org.dromara.common.redis.utils.RedisUtils.getClient().getSet(indexKey);
|
||||
set.remove(sessKey);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
publishEvent(userId, "TERMINATE", sessionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean heartbeat(Long userId, String sessionId, Duration ttlExtend) {
|
||||
String sessKey = sessKey(userId, sessionId);
|
||||
String val = RedisUtils.getCacheObject(sessKey);
|
||||
if (val == null) return false;
|
||||
AppSession s = JsonUtils.parseObject(val, AppSession.class);
|
||||
RedisUtils.setCacheObject(sessKey, JsonUtils.toJsonString(s), ttlExtend);
|
||||
publishEvent(userId, "HEARTBEAT", sessionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void publishEvent(Long userId, String type, String sessionId) {
|
||||
String ch = String.format(CH_EVT_FMT, userId);
|
||||
String payload = type + ":" + sessionId;
|
||||
RedisUtils.publish(ch, payload);
|
||||
}
|
||||
|
||||
private String indexKey(Long userId) {
|
||||
return String.format(KEY_INDEX_FMT, userId);
|
||||
}
|
||||
private String sessKey(Long userId, String sessionId) {
|
||||
return String.format(KEY_SESS_FMT, userId, sessionId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.dromara.web.config;
|
||||
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import org.dromara.app.domain.AppLoginUser;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.core.utils.ServletUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* App用户信息补全过滤器
|
||||
*
|
||||
* @author Pair-Programmer
|
||||
*/
|
||||
public class AppInfoCompletionFilter implements Filter {
|
||||
|
||||
@Value("#{'${app.auth.whitelist:/app/user/profile,/app/user/bind,/app/auth/logout}'.split(',')}")
|
||||
private List<String> whitelist;
|
||||
|
||||
private final AntPathMatcher pathMatcher = new AntPathMatcher();
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||
String uri = httpRequest.getRequestURI();
|
||||
|
||||
// 仅处理App端请求
|
||||
if (StringUtils.startsWith(uri, "/app")) {
|
||||
try {
|
||||
// 尝试获取登录用户,若未登录会抛出异常或返回null
|
||||
LoginUser loginUser = LoginHelper.getLoginUser();
|
||||
|
||||
// 仅针对App登录用户进行检查
|
||||
if (loginUser instanceof AppLoginUser appUser) {
|
||||
// 如果信息不完善 (infoComplete 为 false 或 null)
|
||||
if (Boolean.FALSE.equals(appUser.getInfoComplete())) {
|
||||
// 检查白名单
|
||||
boolean isAllowed = false;
|
||||
if (whitelist != null) {
|
||||
for (String pattern : whitelist) {
|
||||
if (pathMatcher.match(pattern.trim(), uri)) {
|
||||
isAllowed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAllowed) {
|
||||
ServletUtils.renderString((HttpServletResponse) response,
|
||||
JsonUtils.toJsonString(R.fail(403, "用户信息未补全,请先完善信息")));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 未登录或其他异常(如Token无效),放行交由后续认证过滤器或拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.dromara.web.config;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.core.utils.ServletUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
/**
|
||||
* 风险控制拦截器
|
||||
*
|
||||
* @author Pair-Programmer
|
||||
*/
|
||||
public class RiskInterceptor implements HandlerInterceptor {
|
||||
|
||||
public static final String RISK_LOCKED_KEY = "RISK_LOCKED";
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
// 仅处理登录用户
|
||||
if (!StpUtil.isLogin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查 Session 是否有风险锁
|
||||
Object isLocked = StpUtil.getSession().get(RISK_LOCKED_KEY);
|
||||
if (ObjectUtil.isNotNull(isLocked) && (Boolean) isLocked) {
|
||||
String uri = request.getRequestURI();
|
||||
|
||||
// 白名单:改密接口、登出接口、获取个人信息(用于页面展示)
|
||||
// 注意:具体路径需根据实际 Controller 调整
|
||||
if (StringUtils.equalsAnyIgnoreCase(uri,
|
||||
"/system/user/profile/updatePwd",
|
||||
"/auth/logout",
|
||||
"/system/user/profile"
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 拦截并返回 403
|
||||
ServletUtils.renderString(response,
|
||||
JsonUtils.toJsonString(R.fail(403, "检测到异常登录,请强制修改密码后继续操作")));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.dromara.web.config;
|
||||
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web MVC 扩展配置
|
||||
*
|
||||
* @author Pair-Programmer
|
||||
*/
|
||||
@Configuration
|
||||
public class WebMvcPlusConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 注册风险控制拦截器
|
||||
registry.addInterceptor(new RiskInterceptor())
|
||||
.addPathPatterns("/system/**", "/monitor/**", "/tool/**");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AppInfoCompletionFilter appInfoCompletionFilter() {
|
||||
return new AppInfoCompletionFilter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean<AppInfoCompletionFilter> appInfoCompletionFilterRegistration(AppInfoCompletionFilter filter) {
|
||||
FilterRegistrationBean<AppInfoCompletionFilter> registration = new FilterRegistrationBean<>(filter);
|
||||
registration.addUrlPatterns("/app/*");
|
||||
registration.setName("appInfoCompletionFilter");
|
||||
// 顺序需在 Sa-Token 认证过滤器之后,但在业务逻辑之前
|
||||
// Sa-Token 拦截器在 Interceptor 层,Filter 在 Servlet 层。
|
||||
// 但 LoginHelper 需要 Token,Token 在 Header 里,随时可取。
|
||||
// 关键是 Filter 需要在请求处理前执行。
|
||||
registration.setOrder(100);
|
||||
return registration;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package org.dromara.web.config.properties;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 风险控制配置
|
||||
*
|
||||
* @author Pair-Programmer
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "sys.risk")
|
||||
public class RiskControlProperties {
|
||||
|
||||
/**
|
||||
* 是否开启风险检测
|
||||
*/
|
||||
private Boolean enabled = true;
|
||||
|
||||
/**
|
||||
* 风险阈值 (0-100)
|
||||
*/
|
||||
private Integer threshold = 60;
|
||||
|
||||
/**
|
||||
* 异地登录权重
|
||||
*/
|
||||
private Integer geoWeight = 50;
|
||||
|
||||
/**
|
||||
* 新设备权重
|
||||
*/
|
||||
private Integer deviceWeight = 30;
|
||||
|
||||
/**
|
||||
* 非工作时间权重 (00:00-06:00)
|
||||
*/
|
||||
private Integer timeWeight = 10;
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package org.dromara.web.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.codec.Base64;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.zhyd.oauth.model.AuthResponse;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import me.zhyd.oauth.request.AuthRequest;
|
||||
import me.zhyd.oauth.utils.AuthStateUtils;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.domain.model.LoginBody;
|
||||
import org.dromara.common.core.domain.model.RegisterBody;
|
||||
import org.dromara.common.core.domain.model.SocialLoginBody;
|
||||
import org.dromara.common.core.utils.MessageUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.encrypt.annotation.ApiEncrypt;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
|
||||
import org.dromara.common.social.config.properties.SocialProperties;
|
||||
import org.dromara.common.social.utils.SocialUtils;
|
||||
import org.dromara.common.sse.dto.SseMessageDto;
|
||||
import org.dromara.common.sse.utils.SseMessageUtils;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.system.service.SysClientService;
|
||||
import org.dromara.system.service.SysConfigService;
|
||||
import org.dromara.system.service.SysSocialService;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.dromara.web.service.IAuthStrategy;
|
||||
import org.dromara.web.service.SysLoginService;
|
||||
import org.dromara.web.service.SysRegisterService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 认证
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@SaIgnore
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/auth")
|
||||
public class AuthController {
|
||||
|
||||
private final SocialProperties socialProperties;
|
||||
private final SysLoginService loginService;
|
||||
private final SysRegisterService registerService;
|
||||
private final SysConfigService configService;
|
||||
private final SysSocialService socialUserService;
|
||||
private final SysClientService clientService;
|
||||
private final ScheduledExecutorService scheduledExecutorService;
|
||||
|
||||
|
||||
/**
|
||||
* 登录方法
|
||||
*
|
||||
* @param body 登录信息
|
||||
* @return 结果
|
||||
*/
|
||||
@ApiEncrypt
|
||||
@PostMapping("/login")
|
||||
public R<LoginVo> login(@RequestBody String body) {
|
||||
LoginBody loginBody = JsonUtils.parseObject(body, LoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
// 授权类型和客户端id
|
||||
String clientId = loginBody.getClientId();
|
||||
String grantType = loginBody.getGrantType();
|
||||
SysClientVo client = clientService.queryByClientId(clientId);
|
||||
// 查询不到 client 或 client 内不包含 grantType
|
||||
if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), grantType)) {
|
||||
log.info("客户端id: {} 认证类型:{} 异常!.", clientId, grantType);
|
||||
return R.fail(MessageUtils.message("auth.grant.type.error"));
|
||||
} else if (!SystemConstants.NORMAL.equals(client.getStatus())) {
|
||||
return R.fail(MessageUtils.message("auth.grant.type.blocked"));
|
||||
}
|
||||
// 登录
|
||||
LoginVo loginVo = IAuthStrategy.login(body, client, grantType);
|
||||
|
||||
Long userId = LoginHelper.getUserId();
|
||||
scheduledExecutorService.schedule(() -> {
|
||||
SseMessageDto dto = new SseMessageDto();
|
||||
dto.setMessage("欢迎登录RuoYi-Vue-Plus后台管理系统");
|
||||
dto.setUserIds(List.of(userId));
|
||||
SseMessageUtils.publishMessage(dto);
|
||||
}, 5, TimeUnit.SECONDS);
|
||||
return R.ok(loginVo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取跳转URL
|
||||
*
|
||||
* @param source 登录来源
|
||||
* @return 结果
|
||||
*/
|
||||
@GetMapping("/binding/{source}")
|
||||
public R<String> authBinding(@PathVariable("source") String source, @RequestParam String domain) {
|
||||
SocialLoginConfigProperties obj = socialProperties.getType().get(source);
|
||||
if (ObjectUtil.isNull(obj)) {
|
||||
return R.fail(source + "平台账号暂不支持");
|
||||
}
|
||||
AuthRequest authRequest = SocialUtils.getAuthRequest(source, socialProperties);
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("domain", domain);
|
||||
map.put("state", AuthStateUtils.createState());
|
||||
String authorizeUrl = authRequest.authorize(Base64.encode(JsonUtils.toJsonString(map), StandardCharsets.UTF_8));
|
||||
return R.ok("操作成功", authorizeUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端回调绑定授权(需要token)
|
||||
*
|
||||
* @param loginBody 请求体
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/social/callback")
|
||||
public R<Void> socialCallback(@RequestBody SocialLoginBody loginBody) {
|
||||
// 校验token
|
||||
StpUtil.checkLogin();
|
||||
// 获取第三方登录信息
|
||||
AuthResponse<AuthUser> response = SocialUtils.loginAuth(
|
||||
loginBody.getSource(), loginBody.getSocialCode(),
|
||||
loginBody.getSocialState(), socialProperties);
|
||||
AuthUser authUserData = response.getData();
|
||||
// 判断授权响应是否成功
|
||||
if (!response.ok()) {
|
||||
return R.fail(response.getMsg());
|
||||
}
|
||||
loginService.socialRegister(authUserData);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 取消授权(需要token)
|
||||
*
|
||||
* @param socialId socialId
|
||||
*/
|
||||
@DeleteMapping(value = "/unlock/{socialId}")
|
||||
public R<Void> unlockSocial(@PathVariable Long socialId) {
|
||||
// 校验token
|
||||
StpUtil.checkLogin();
|
||||
Boolean rows = socialUserService.deleteWithValidById(socialId);
|
||||
return rows ? R.ok() : R.fail("取消授权失败");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
public R<Void> logout() {
|
||||
loginService.logout();
|
||||
return R.ok("退出成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
@ApiEncrypt
|
||||
@PostMapping("/register")
|
||||
public R<Void> register(@Validated @RequestBody RegisterBody user) {
|
||||
if (!configService.selectRegisterEnabled()) {
|
||||
return R.fail("当前系统没有开启注册功能!");
|
||||
}
|
||||
registerService.register(user);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录页面租户下拉框(兼容多租户前端)
|
||||
*/
|
||||
@GetMapping("/tenant/list")
|
||||
public R<TenantListVo> tenantList() {
|
||||
return R.ok(TenantListVo.DISABLED_TENANT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户下拉列表
|
||||
*
|
||||
* @param tenantEnabled 租户开关
|
||||
*/
|
||||
public record TenantListVo(boolean tenantEnabled) {
|
||||
|
||||
/**
|
||||
* 禁用租户,租户开关响应false(即关闭),让前端关闭租户功能
|
||||
*/
|
||||
public static final TenantListVo DISABLED_TENANT = TenantListVo.of(false);
|
||||
|
||||
public static TenantListVo of(boolean tenantEnabled) {
|
||||
return new TenantListVo(tenantEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package org.dromara.web.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.hutool.captcha.AbstractCaptcha;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.reflect.ReflectUtils;
|
||||
import org.dromara.common.mail.config.properties.MailProperties;
|
||||
import org.dromara.common.mail.utils.MailUtils;
|
||||
import org.dromara.common.ratelimiter.annotation.RateLimiter;
|
||||
import org.dromara.common.ratelimiter.enums.LimitType;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
||||
import org.dromara.common.web.enums.CaptchaType;
|
||||
import org.dromara.sms4j.api.SmsBlend;
|
||||
import org.dromara.sms4j.api.entity.SmsResponse;
|
||||
import org.dromara.sms4j.core.factory.SmsFactory;
|
||||
import org.dromara.web.domain.vo.CaptchaVo;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
/**
|
||||
* 验证码操作处理
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@SaIgnore
|
||||
@Slf4j
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
public class CaptchaController {
|
||||
|
||||
private final CaptchaProperties captchaProperties;
|
||||
private final MailProperties mailProperties;
|
||||
|
||||
/**
|
||||
* 短信验证码
|
||||
*
|
||||
* @param phonenumber 用户手机号
|
||||
*/
|
||||
@RateLimiter(key = "#phonenumber", time = 60, count = 1)
|
||||
@GetMapping("/resource/sms/code")
|
||||
public R<Void> smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) {
|
||||
String key = GlobalConstants.CAPTCHA_CODE_KEY + phonenumber;
|
||||
String code = RandomUtil.randomNumbers(4);
|
||||
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||
// 验证码模板id 自行处理 (查数据库或写死均可)
|
||||
String templateId = "";
|
||||
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
|
||||
map.put("code", code);
|
||||
SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
|
||||
SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, templateId, map);
|
||||
if (!smsResponse.isSuccess()) {
|
||||
log.error("验证码短信发送异常 => {}", smsResponse);
|
||||
return R.fail(smsResponse.getData().toString());
|
||||
}
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮箱验证码
|
||||
*
|
||||
* @param email 邮箱
|
||||
*/
|
||||
@GetMapping("/resource/email/code")
|
||||
public R<Void> emailCode(@NotBlank(message = "{user.email.not.blank}") String email) {
|
||||
if (!mailProperties.getEnabled()) {
|
||||
return R.fail("当前系统没有开启邮箱功能!");
|
||||
}
|
||||
SpringUtils.getAopProxy(this).emailCodeImpl(email);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮箱验证码
|
||||
* 独立方法避免验证码关闭之后仍然走限流
|
||||
*/
|
||||
@RateLimiter(key = "#email", time = 60, count = 1)
|
||||
public void emailCodeImpl(String email) {
|
||||
String key = GlobalConstants.CAPTCHA_CODE_KEY + email;
|
||||
String code = RandomUtil.randomNumbers(4);
|
||||
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||
try {
|
||||
MailUtils.sendText(email, "登录验证码", "您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。");
|
||||
} catch (Exception e) {
|
||||
log.error("验证码短信发送异常 => {}", e.getMessage());
|
||||
throw new ServiceException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*/
|
||||
@GetMapping("/auth/code")
|
||||
public R<CaptchaVo> getCode() {
|
||||
boolean captchaEnabled = captchaProperties.getEnable();
|
||||
if (!captchaEnabled) {
|
||||
CaptchaVo captchaVo = new CaptchaVo();
|
||||
captchaVo.setCaptchaEnabled(false);
|
||||
return R.ok(captchaVo);
|
||||
}
|
||||
return R.ok(SpringUtils.getAopProxy(this).getCodeImpl());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
* 独立方法避免验证码关闭之后仍然走限流
|
||||
*/
|
||||
@RateLimiter(time = 60, count = 10, limitType = LimitType.IP)
|
||||
public CaptchaVo getCodeImpl() {
|
||||
// 保存验证码信息
|
||||
String uuid = IdUtil.simpleUUID();
|
||||
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
|
||||
// 生成验证码
|
||||
CaptchaType captchaType = captchaProperties.getType();
|
||||
boolean isMath = CaptchaType.MATH == captchaType;
|
||||
Integer length = isMath ? captchaProperties.getNumberLength() : captchaProperties.getCharLength();
|
||||
CodeGenerator codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), length);
|
||||
AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz());
|
||||
captcha.setGenerator(codeGenerator);
|
||||
captcha.createCode();
|
||||
// 如果是数学验证码,使用SpEL表达式处理验证码结果
|
||||
String code = captcha.getCode();
|
||||
if (isMath) {
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
Expression exp = parser.parseExpression(StringUtils.remove(code, "="));
|
||||
code = exp.getValue(String.class);
|
||||
}
|
||||
RedisUtils.setCacheObject(verifyKey, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||
CaptchaVo captchaVo = new CaptchaVo();
|
||||
captchaVo.setUuid(uuid);
|
||||
captchaVo.setImg(captcha.getImageBase64());
|
||||
return captchaVo;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.dromara.web.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 首页
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@SaIgnore
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
public class IndexController {
|
||||
|
||||
/**
|
||||
* 访问首页,提示语
|
||||
*/
|
||||
@GetMapping("/")
|
||||
public String index() {
|
||||
return StringUtils.format("欢迎使用{}后台管理框架,请通过前端地址访问。", SpringUtils.getApplicationName());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.dromara.web.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 验证码信息
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
public class CaptchaVo {
|
||||
|
||||
/**
|
||||
* 是否开启验证码
|
||||
*/
|
||||
private Boolean captchaEnabled = true;
|
||||
|
||||
/**
|
||||
* 验证码唯一标识
|
||||
*/
|
||||
private String uuid;
|
||||
|
||||
/**
|
||||
* 验证码图片
|
||||
*/
|
||||
private String img;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.dromara.web.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 登录验证信息
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
public class LoginVo {
|
||||
|
||||
/**
|
||||
* 授权令牌
|
||||
*/
|
||||
@JsonProperty("access_token")
|
||||
private String accessToken;
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*/
|
||||
@JsonProperty("refresh_token")
|
||||
private String refreshToken;
|
||||
|
||||
/**
|
||||
* 授权令牌 access_token 的有效期
|
||||
*/
|
||||
@JsonProperty("expire_in")
|
||||
private Long expireIn;
|
||||
|
||||
/**
|
||||
* 刷新令牌 refresh_token 的有效期
|
||||
*/
|
||||
@JsonProperty("refresh_expire_in")
|
||||
private Long refreshExpireIn;
|
||||
|
||||
/**
|
||||
* 应用id
|
||||
*/
|
||||
@JsonProperty("client_id")
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* 令牌权限
|
||||
*/
|
||||
private String scope;
|
||||
|
||||
/**
|
||||
* 用户 openid
|
||||
*/
|
||||
private String openid;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package org.dromara.web.listener;
|
||||
|
||||
import cn.dev33.satoken.listener.SaTokenListener;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.http.useragent.UserAgent;
|
||||
import cn.hutool.http.useragent.UserAgentUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.CacheConstants;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.domain.dto.UserOnlineDTO;
|
||||
import org.dromara.common.core.utils.MessageUtils;
|
||||
import org.dromara.common.core.utils.ServletUtils;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.ip.AddressUtils;
|
||||
import org.dromara.common.log.event.LogininforEvent;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.web.config.properties.RiskControlProperties;
|
||||
import org.dromara.web.service.RiskControlService;
|
||||
import org.dromara.web.service.SysLoginService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 用户行为 侦听器的实现
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
@Slf4j
|
||||
public class UserActionListener implements SaTokenListener {
|
||||
|
||||
private final SysLoginService loginService;
|
||||
private final RiskControlService riskControlService;
|
||||
private final RiskControlProperties riskProperties;
|
||||
|
||||
/**
|
||||
* 每次登录时触发
|
||||
*/
|
||||
@Override
|
||||
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {
|
||||
UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
|
||||
String ip = ServletUtils.getClientIP();
|
||||
UserOnlineDTO dto = new UserOnlineDTO();
|
||||
dto.setIpaddr(ip);
|
||||
dto.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
|
||||
dto.setBrowser(userAgent.getBrowser().getName());
|
||||
dto.setOs(userAgent.getOs().getName());
|
||||
dto.setLoginTime(System.currentTimeMillis());
|
||||
dto.setTokenId(tokenValue);
|
||||
String username = (String) loginParameter.getExtra(LoginHelper.USER_NAME_KEY);
|
||||
dto.setUserName(username);
|
||||
dto.setClientKey((String) loginParameter.getExtra(LoginHelper.CLIENT_KEY));
|
||||
dto.setDeviceType(loginParameter.getDeviceType());
|
||||
dto.setDeptName((String) loginParameter.getExtra(LoginHelper.DEPT_NAME_KEY));
|
||||
if (loginParameter.getTimeout() == -1) {
|
||||
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto);
|
||||
} else {
|
||||
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto, Duration.ofSeconds(loginParameter.getTimeout()));
|
||||
}
|
||||
|
||||
// 风险检测 (仅针对 sys 用户)
|
||||
// 假设 "login" 是 sys 用户的 loginType (SaToken 默认配置)
|
||||
// if ("login".equals(loginType)) {
|
||||
// int score = riskControlService.assessRisk(username, ip, ServletUtils.getRequest().getHeader("User-Agent"));
|
||||
// if (score > riskProperties.getThreshold()) {
|
||||
// log.warn("用户 {} 登录风险过高 (score={}),已锁定会话", username, score);
|
||||
// StpUtil.getSession().set(RiskInterceptor.RISK_LOCKED_KEY, true);
|
||||
// // 可以在此触发邮件/短信告警
|
||||
// }
|
||||
// }
|
||||
|
||||
// 记录登录日志
|
||||
LogininforEvent logininforEvent = new LogininforEvent();
|
||||
logininforEvent.setUsername(username);
|
||||
logininforEvent.setStatus(Constants.LOGIN_SUCCESS);
|
||||
logininforEvent.setMessage(MessageUtils.message("user.login.success"));
|
||||
logininforEvent.setRequest(ServletUtils.getRequest());
|
||||
SpringUtils.context().publishEvent(logininforEvent);
|
||||
// 更新登录信息
|
||||
loginService.recordLoginInfo((Long) loginParameter.getExtra(LoginHelper.USER_KEY), ip);
|
||||
log.info("user doLogin, userId:{}, token:{}", loginId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次注销时触发
|
||||
*/
|
||||
@Override
|
||||
public void doLogout(String loginType, Object loginId, String tokenValue) {
|
||||
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
||||
log.info("user doLogout, userId:{}, token:{}", loginId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次被踢下线时触发
|
||||
*/
|
||||
@Override
|
||||
public void doKickout(String loginType, Object loginId, String tokenValue) {
|
||||
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
||||
log.info("user doKickout, userId:{}, token:{}", loginId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次被顶下线时触发
|
||||
*/
|
||||
@Override
|
||||
public void doReplaced(String loginType, Object loginId, String tokenValue) {
|
||||
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
||||
log.info("user doReplaced, userId:{}, token:{}", loginId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次被封禁时触发
|
||||
*/
|
||||
@Override
|
||||
public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {
|
||||
log.info("用户被禁用: loginType={}, loginId={}, service={}, level={}, disableTime={}",
|
||||
loginType, loginId, service, level, disableTime);
|
||||
|
||||
// 删除在线用户信息
|
||||
String tokenValue = StpUtil.getTokenValueByLoginId(loginId);
|
||||
if (StringUtils.isNotBlank(tokenValue)) {
|
||||
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次被解封时触发
|
||||
*/
|
||||
@Override
|
||||
public void doUntieDisable(String loginType, Object loginId, String service) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次打开二级认证时触发
|
||||
*/
|
||||
@Override
|
||||
public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次创建Session时触发
|
||||
*/
|
||||
@Override
|
||||
public void doCloseSafe(String loginType, String tokenValue, String service) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次创建Session时触发
|
||||
*/
|
||||
@Override
|
||||
public void doCreateSession(String id) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次注销Session时触发
|
||||
*/
|
||||
@Override
|
||||
public void doLogoutSession(String id) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 每次Token续期时触发
|
||||
*/
|
||||
@Override
|
||||
public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.dromara.web.service;
|
||||
|
||||
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
|
||||
/**
|
||||
* 授权策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface IAuthStrategy {
|
||||
|
||||
String BASE_NAME = "AuthStrategy";
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*
|
||||
* @param body 登录对象
|
||||
* @param client 授权管理视图对象
|
||||
* @param grantType 授权类型
|
||||
* @return 登录验证信息
|
||||
*/
|
||||
static LoginVo login(String body, SysClientVo client, String grantType) {
|
||||
// 授权类型和客户端id
|
||||
String beanName = grantType + BASE_NAME;
|
||||
if (!SpringUtils.containsBean(beanName)) {
|
||||
throw new ServiceException("授权类型不正确!");
|
||||
}
|
||||
IAuthStrategy instance = SpringUtils.getBean(beanName);
|
||||
return instance.login(body, client);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*
|
||||
* @param body 登录对象
|
||||
* @param client 授权管理视图对象
|
||||
* @return 登录验证信息
|
||||
*/
|
||||
LoginVo login(String body, SysClientVo client);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package org.dromara.web.service;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.useragent.UserAgent;
|
||||
import cn.hutool.http.useragent.UserAgentUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.ip.AddressUtils;
|
||||
import org.dromara.system.domain.bo.SysLogininforBo;
|
||||
import org.dromara.system.domain.vo.SysLogininforVo;
|
||||
import org.dromara.system.service.SysLogininforService;
|
||||
import org.dromara.web.config.properties.RiskControlProperties;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 风险控制服务
|
||||
*
|
||||
* @author Pair-Programmer
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RiskControlService {
|
||||
|
||||
private final SysLogininforService logininforService;
|
||||
private final RiskControlProperties properties;
|
||||
|
||||
/**
|
||||
* 评估登录风险
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param currentIp 当前IP
|
||||
* @param uaStr User-Agent字符串
|
||||
* @return 风险分数 (0-100)
|
||||
*/
|
||||
public int assessRisk(String username, String currentIp, String uaStr) {
|
||||
if (!properties.getEnabled()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int score = 0;
|
||||
String currentAddress = AddressUtils.getRealAddressByIP(currentIp);
|
||||
UserAgent currentUserAgent = UserAgentUtil.parse(uaStr);
|
||||
|
||||
// 1. 获取最近成功的登录记录 (取最近10条)
|
||||
SysLogininforBo query = new SysLogininforBo();
|
||||
query.setUserName(username);
|
||||
query.setStatus(Constants.SUCCESS);
|
||||
// 这里假设 selectLogininforList 是按时间倒序的,且我们可以通过PageHelper或流截取
|
||||
// 由于是 Service 调用,我们先获取列表然后手动截取,或者依赖 Mapper 的分页(这里简化处理)
|
||||
List<SysLogininforVo> historyList = logininforService.selectLogininforList(query);
|
||||
|
||||
// 如果是首次登录或无历史记录,视为低风险
|
||||
if (CollUtil.isEmpty(historyList)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 截取最近 10 条作为基线
|
||||
List<SysLogininforVo> baseline = historyList.stream().limit(10).collect(Collectors.toList());
|
||||
|
||||
// 2. 检查异地登录 (省份是否变化)
|
||||
boolean isGeoAnomaly = true;
|
||||
Set<String> validLocations = baseline.stream()
|
||||
.map(SysLogininforVo::getLoginLocation)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.map(this::extractProvince)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
String currentProvince = extractProvince(currentAddress);
|
||||
if (validLocations.contains(currentProvince)) {
|
||||
isGeoAnomaly = false;
|
||||
} else {
|
||||
// 特殊处理:如果历史记录都是“未知”或“内网IP”,且当前是公网IP,可能误报,暂且认为是异地
|
||||
log.info("风险检测-异地登录: 用户={}, 历史={}, 当前={}", username, validLocations, currentProvince);
|
||||
}
|
||||
|
||||
if (isGeoAnomaly) {
|
||||
score += properties.getGeoWeight();
|
||||
}
|
||||
|
||||
// 3. 检查新设备 (Browser + OS)
|
||||
boolean isNewDevice = true;
|
||||
Set<String> knownDevices = baseline.stream()
|
||||
.map(log -> log.getBrowser() + " on " + log.getOs())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
String currentDevice = currentUserAgent.getBrowser().getName() + " on " + currentUserAgent.getOs().getName();
|
||||
if (knownDevices.contains(currentDevice)) {
|
||||
isNewDevice = false;
|
||||
} else {
|
||||
log.info("风险检测-新设备: 用户={}, 历史={}, 当前={}", username, knownDevices, currentDevice);
|
||||
}
|
||||
|
||||
if (isNewDevice) {
|
||||
score += properties.getDeviceWeight();
|
||||
}
|
||||
|
||||
// 4. 检查非工作时间 (00:00 - 06:00)
|
||||
int hour = DateUtil.hour(new Date(), true);
|
||||
if (hour >= 0 && hour < 6) {
|
||||
score += properties.getTimeWeight();
|
||||
}
|
||||
|
||||
log.info("风险检测完成: 用户={}, 总分={}, 详情(Geo={}, Device={}, Time={})",
|
||||
username, score, isGeoAnomaly, isNewDevice, (hour >= 0 && hour < 6));
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取省份 (简单截取)
|
||||
*/
|
||||
private String extractProvince(String location) {
|
||||
if (StrUtil.isBlank(location)) {
|
||||
return "Unknown";
|
||||
}
|
||||
// 假设 location 格式为 "XX省 XX市" 或 "XX省"
|
||||
// 简单处理:取前2-3个字符,或者根据空格分割
|
||||
if (location.contains("省")) {
|
||||
return location.substring(0, location.indexOf("省") + 1);
|
||||
}
|
||||
// 直辖市
|
||||
if (location.startsWith("北京") || location.startsWith("上海") || location.startsWith("天津") || location.startsWith("重庆")) {
|
||||
return location.substring(0, 2);
|
||||
}
|
||||
return location; // 无法识别则返回原值
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package org.dromara.web.service;
|
||||
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.Opt;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.lock.annotation.Lock4j;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import org.dromara.common.core.constant.CacheConstants;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.domain.dto.PostDTO;
|
||||
import org.dromara.common.core.domain.dto.RoleDTO;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.core.enums.LoginType;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.MessageUtils;
|
||||
import org.dromara.common.core.utils.ServletUtils;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.log.event.LogininforEvent;
|
||||
import org.dromara.common.mybatis.helper.DataPermissionHelper;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.system.domain.bo.SysSocialBo;
|
||||
import org.dromara.system.domain.vo.*;
|
||||
import org.dromara.system.service.*;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 登录校验方法
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SysLoginService {
|
||||
|
||||
@Value("${user.password.maxRetryCount}")
|
||||
private Integer maxRetryCount;
|
||||
|
||||
@Value("${user.password.lockTime}")
|
||||
private Integer lockTime;
|
||||
|
||||
private final SysPermissionService permissionService;
|
||||
private final SysSocialService sysSocialService;
|
||||
private final SysRoleService roleService;
|
||||
private final SysDeptService deptService;
|
||||
private final SysPostService postService;
|
||||
private final SysUserService userService;
|
||||
|
||||
|
||||
/**
|
||||
* 绑定第三方用户
|
||||
*
|
||||
* @param authUserData 授权响应实体
|
||||
*/
|
||||
@Lock4j
|
||||
public void socialRegister(AuthUser authUserData) {
|
||||
String authId = authUserData.getSource() + authUserData.getUuid();
|
||||
// 第三方用户信息
|
||||
SysSocialBo bo = BeanUtil.toBean(authUserData, SysSocialBo.class);
|
||||
BeanUtil.copyProperties(authUserData.getToken(), bo);
|
||||
Long userId = LoginHelper.getUserId();
|
||||
bo.setUserId(userId);
|
||||
bo.setAuthId(authId);
|
||||
bo.setOpenId(authUserData.getUuid());
|
||||
bo.setUserName(authUserData.getUsername());
|
||||
bo.setNickName(authUserData.getNickname());
|
||||
List<SysSocialVo> checkList = sysSocialService.selectByAuthId(authId);
|
||||
if (CollUtil.isNotEmpty(checkList)) {
|
||||
throw new ServiceException("此三方账号已经被绑定!");
|
||||
}
|
||||
// 查询是否已经绑定用户
|
||||
SysSocialBo params = new SysSocialBo();
|
||||
params.setUserId(userId);
|
||||
params.setSource(bo.getSource());
|
||||
List<SysSocialVo> list = sysSocialService.queryList(params);
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
// 没有绑定用户, 新增用户信息
|
||||
sysSocialService.insertByBo(bo);
|
||||
} else {
|
||||
// 更新用户信息
|
||||
bo.setId(list.get(0).getId());
|
||||
sysSocialService.updateByBo(bo);
|
||||
// 如果要绑定的平台账号已经被绑定过了 是否抛异常自行决断
|
||||
// throw new ServiceException("此平台账号已经被绑定!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
public void logout() {
|
||||
try {
|
||||
LoginUser loginUser = LoginHelper.getLoginUser();
|
||||
if (ObjectUtil.isNull(loginUser)) {
|
||||
return;
|
||||
}
|
||||
recordLogininfor(loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success"));
|
||||
} catch (NotLoginException ignored) {
|
||||
} finally {
|
||||
try {
|
||||
StpUtil.logout();
|
||||
} catch (NotLoginException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录信息
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param status 状态
|
||||
* @param message 消息内容
|
||||
*/
|
||||
public void recordLogininfor(String username, String status, String message) {
|
||||
LogininforEvent logininforEvent = new LogininforEvent();
|
||||
logininforEvent.setUsername(username);
|
||||
logininforEvent.setStatus(status);
|
||||
logininforEvent.setMessage(message);
|
||||
logininforEvent.setRequest(ServletUtils.getRequest());
|
||||
SpringUtils.context().publishEvent(logininforEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建登录用户
|
||||
*/
|
||||
public LoginUser buildLoginUser(SysUserVo user) {
|
||||
LoginUser loginUser = new LoginUser();
|
||||
Long userId = user.getUserId();
|
||||
loginUser.setUserId(userId);
|
||||
loginUser.setDeptId(user.getDeptId());
|
||||
loginUser.setUsername(user.getUserName());
|
||||
loginUser.setNickname(user.getNickName());
|
||||
loginUser.setUserType(user.getUserType());
|
||||
loginUser.setMenuPermission(permissionService.getMenuPermission(userId));
|
||||
loginUser.setRolePermission(permissionService.getRolePermission(userId));
|
||||
if (ObjectUtil.isNotNull(user.getDeptId())) {
|
||||
Opt<SysDeptVo> deptOpt = Opt.of(user.getDeptId()).map(deptService::selectDeptById);
|
||||
loginUser.setDeptName(deptOpt.map(SysDeptVo::getDeptName).orElse(StringUtils.EMPTY));
|
||||
loginUser.setDeptCategory(deptOpt.map(SysDeptVo::getDeptCategory).orElse(StringUtils.EMPTY));
|
||||
}
|
||||
List<SysRoleVo> roles = roleService.selectRolesByUserId(userId);
|
||||
List<SysPostVo> posts = postService.selectPostsByUserId(userId);
|
||||
loginUser.setRoles(BeanUtil.copyToList(roles, RoleDTO.class));
|
||||
loginUser.setPosts(BeanUtil.copyToList(posts, PostDTO.class));
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
public void recordLoginInfo(Long userId, String ip) {
|
||||
DataPermissionHelper.ignore(() -> userService.updateLoginInfo(userId, ip));
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录校验
|
||||
*/
|
||||
public void checkLogin(LoginType loginType, String username, Supplier<Boolean> supplier) {
|
||||
String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
|
||||
String loginFail = Constants.LOGIN_FAIL;
|
||||
|
||||
// 获取用户登录错误次数,默认为0 (可自定义限制策略 例如: key + username + ip)
|
||||
int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
|
||||
// 锁定时间内登录 则踢出
|
||||
if (errorNumber >= maxRetryCount) {
|
||||
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
|
||||
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
|
||||
}
|
||||
|
||||
if (supplier.get()) {
|
||||
// 错误次数递增
|
||||
errorNumber++;
|
||||
RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
|
||||
// 达到规定错误次数 则锁定登录
|
||||
if (errorNumber >= maxRetryCount) {
|
||||
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
|
||||
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
|
||||
} else {
|
||||
// 未达到规定错误次数
|
||||
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
|
||||
throw new UserException(loginType.getRetryLimitCount(), errorNumber);
|
||||
}
|
||||
}
|
||||
|
||||
// 登录成功 清空错误次数
|
||||
RedisUtils.deleteObject(errorKey);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.dromara.web.service;
|
||||
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.domain.model.RegisterBody;
|
||||
import org.dromara.common.core.enums.UserType;
|
||||
import org.dromara.common.core.exception.user.CaptchaException;
|
||||
import org.dromara.common.core.exception.user.CaptchaExpireException;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.MessageUtils;
|
||||
import org.dromara.common.core.utils.ServletUtils;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.log.event.LogininforEvent;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
||||
import org.dromara.system.domain.SysUser;
|
||||
import org.dromara.system.domain.bo.SysUserBo;
|
||||
import org.dromara.system.mapper.SysUserMapper;
|
||||
import org.dromara.system.service.SysUserService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 注册校验方法
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class SysRegisterService {
|
||||
|
||||
private final SysUserService userService;
|
||||
private final SysUserMapper userMapper;
|
||||
private final CaptchaProperties captchaProperties;
|
||||
|
||||
/**
|
||||
* 注册
|
||||
*/
|
||||
public void register(RegisterBody registerBody) {
|
||||
String username = registerBody.getUsername();
|
||||
String password = registerBody.getPassword();
|
||||
// 校验用户类型是否存在
|
||||
String userType = UserType.getUserType(registerBody.getUserType()).getUserType();
|
||||
|
||||
boolean captchaEnabled = captchaProperties.getEnable();
|
||||
// 验证码开关
|
||||
if (captchaEnabled) {
|
||||
validateCaptcha(username, registerBody.getCode(), registerBody.getUuid());
|
||||
}
|
||||
SysUserBo sysUser = new SysUserBo();
|
||||
sysUser.setUserName(username);
|
||||
sysUser.setNickName(username);
|
||||
sysUser.setPassword(BCrypt.hashpw(password));
|
||||
sysUser.setUserType(userType);
|
||||
|
||||
boolean exist = !userService.checkUserNameUnique(sysUser);
|
||||
if (exist) {
|
||||
throw new UserException("user.register.save.error", username);
|
||||
}
|
||||
boolean regFlag = userService.registerUser(sysUser);
|
||||
if (!regFlag) {
|
||||
throw new UserException("user.register.error");
|
||||
}
|
||||
recordLogininfor(username, Constants.REGISTER, MessageUtils.message("user.register.success"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验验证码
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param code 验证码
|
||||
* @param uuid 唯一标识
|
||||
*/
|
||||
public void validateCaptcha(String username, String code, String uuid) {
|
||||
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, "");
|
||||
String captcha = RedisUtils.getCacheObject(verifyKey);
|
||||
RedisUtils.deleteObject(verifyKey);
|
||||
if (captcha == null) {
|
||||
recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
if (!code.equalsIgnoreCase(captcha)) {
|
||||
recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
|
||||
throw new CaptchaException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录信息
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param status 状态
|
||||
* @param message 消息内容
|
||||
* @return
|
||||
*/
|
||||
private void recordLogininfor(String username, String status, String message) {
|
||||
LogininforEvent logininforEvent = new LogininforEvent();
|
||||
logininforEvent.setUsername(username);
|
||||
logininforEvent.setStatus(status);
|
||||
logininforEvent.setMessage(message);
|
||||
logininforEvent.setRequest(ServletUtils.getRequest());
|
||||
SpringUtils.context().publishEvent(logininforEvent);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package org.dromara.web.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
import org.dromara.common.core.domain.model.EmailLoginBody;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.core.enums.LoginType;
|
||||
import org.dromara.common.core.exception.user.CaptchaExpireException;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.MessageUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.system.domain.vo.SysUserVo;
|
||||
import org.dromara.system.service.SysUserService;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.dromara.web.service.IAuthStrategy;
|
||||
import org.dromara.web.service.SysLoginService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 邮件认证策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("email" + IAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class EmailAuthStrategy implements IAuthStrategy {
|
||||
|
||||
private final SysLoginService loginService;
|
||||
private final SysUserService userService;
|
||||
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
EmailLoginBody loginBody = JsonUtils.parseObject(body, EmailLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
String email = loginBody.getEmail();
|
||||
String emailCode = loginBody.getEmailCode();
|
||||
|
||||
SysUserVo user = loadUserByEmail(email);
|
||||
loginService.checkLogin(LoginType.EMAIL, user.getUserName(), () -> !validateEmailCode(email, emailCode));
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
LoginUser loginUser = loginService.buildLoginUser(user);
|
||||
|
||||
loginUser.setClientKey(client.getClientKey());
|
||||
loginUser.setDeviceType(client.getDeviceType());
|
||||
SaLoginParameter model = new SaLoginParameter();
|
||||
model.setDeviceType(client.getDeviceType());
|
||||
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
|
||||
// 例如: 后台用户30分钟过期 app用户1天过期
|
||||
model.setTimeout(client.getTimeout());
|
||||
model.setActiveTimeout(client.getActiveTimeout());
|
||||
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
|
||||
// 生成token
|
||||
LoginHelper.login(loginUser, model);
|
||||
|
||||
LoginVo loginVo = new LoginVo();
|
||||
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||
loginVo.setClientId(client.getClientId());
|
||||
return loginVo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验邮箱验证码
|
||||
*/
|
||||
private boolean validateEmailCode(String email, String emailCode) {
|
||||
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + email);
|
||||
if (StringUtils.isBlank(code)) {
|
||||
loginService.recordLogininfor(email, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
return code.equals(emailCode);
|
||||
}
|
||||
|
||||
private SysUserVo loadUserByEmail(String email) {
|
||||
SysUserVo user = userService.selectUserByEmail(email);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
log.info("登录用户:{} 不存在.", email);
|
||||
throw new UserException("user.not.exists", email);
|
||||
} else if (SystemConstants.DISABLE.equals(user.getStatus())) {
|
||||
log.info("登录用户:{} 已被停用.", email);
|
||||
throw new UserException("user.blocked", email);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package org.dromara.web.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.core.domain.model.PasswordLoginBody;
|
||||
import org.dromara.common.core.enums.LoginType;
|
||||
import org.dromara.common.core.exception.user.CaptchaException;
|
||||
import org.dromara.common.core.exception.user.CaptchaExpireException;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.MessageUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.system.domain.vo.SysUserVo;
|
||||
import org.dromara.system.service.SysUserService;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.dromara.web.service.IAuthStrategy;
|
||||
import org.dromara.web.service.SysLoginService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 密码认证策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("password" + IAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class PasswordAuthStrategy implements IAuthStrategy {
|
||||
|
||||
private final CaptchaProperties captchaProperties;
|
||||
private final SysLoginService loginService;
|
||||
private final SysUserService userService;
|
||||
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
PasswordLoginBody loginBody = JsonUtils.parseObject(body, PasswordLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
String username = loginBody.getUsername();
|
||||
String password = loginBody.getPassword();
|
||||
String code = loginBody.getCode();
|
||||
String uuid = loginBody.getUuid();
|
||||
|
||||
boolean captchaEnabled = captchaProperties.getEnable();
|
||||
// 验证码开关
|
||||
if (captchaEnabled) {
|
||||
validateCaptcha(username, code, uuid);
|
||||
}
|
||||
|
||||
SysUserVo user = loadUserByUsername(username);
|
||||
loginService.checkLogin(LoginType.PASSWORD, username, () -> !BCrypt.checkpw(password, user.getPassword()));
|
||||
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser
|
||||
LoginUser loginUser = loginService.buildLoginUser(user);
|
||||
|
||||
loginUser.setClientKey(client.getClientKey());
|
||||
loginUser.setDeviceType(client.getDeviceType());
|
||||
SaLoginParameter model = new SaLoginParameter();
|
||||
model.setDeviceType(client.getDeviceType());
|
||||
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
|
||||
// 例如: 后台用户30分钟过期 app用户1天过期
|
||||
model.setTimeout(client.getTimeout());
|
||||
model.setActiveTimeout(client.getActiveTimeout());
|
||||
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
|
||||
// 生成token
|
||||
LoginHelper.login(loginUser, model);
|
||||
|
||||
LoginVo loginVo = new LoginVo();
|
||||
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||
loginVo.setClientId(client.getClientId());
|
||||
return loginVo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验验证码
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param code 验证码
|
||||
* @param uuid 唯一标识
|
||||
*/
|
||||
private void validateCaptcha(String username, String code, String uuid) {
|
||||
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.blankToDefault(uuid, "");
|
||||
String captcha = RedisUtils.getCacheObject(verifyKey);
|
||||
RedisUtils.deleteObject(verifyKey);
|
||||
if (captcha == null) {
|
||||
loginService.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
if (!code.equalsIgnoreCase(captcha)) {
|
||||
loginService.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
|
||||
throw new CaptchaException();
|
||||
}
|
||||
}
|
||||
|
||||
private SysUserVo loadUserByUsername(String username) {
|
||||
SysUserVo user = userService.selectUserByUserName(username);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
log.info("登录用户:{} 不存在.", username);
|
||||
throw new UserException("user.not.exists", username);
|
||||
} else if (SystemConstants.DISABLE.equals(user.getStatus())) {
|
||||
log.info("登录用户:{} 已被停用.", username);
|
||||
throw new UserException("user.blocked", username);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package org.dromara.web.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.core.domain.model.SmsLoginBody;
|
||||
import org.dromara.common.core.enums.LoginType;
|
||||
import org.dromara.common.core.exception.user.CaptchaExpireException;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.MessageUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.system.domain.vo.SysUserVo;
|
||||
import org.dromara.system.service.SysUserService;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.dromara.web.service.IAuthStrategy;
|
||||
import org.dromara.web.service.SysLoginService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 短信认证策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("sms" + IAuthStrategy.BASE_NAME)
|
||||
public class SmsAuthStrategy implements IAuthStrategy {
|
||||
|
||||
@Autowired
|
||||
private SysLoginService loginService;
|
||||
@Autowired
|
||||
private SysUserService userService;
|
||||
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
SmsLoginBody loginBody = JsonUtils.parseObject(body, SmsLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
String phonenumber = loginBody.getPhonenumber();
|
||||
String smsCode = loginBody.getSmsCode();
|
||||
|
||||
SysUserVo user = loadUserByPhonenumber(phonenumber);
|
||||
loginService.checkLogin(LoginType.SMS, user.getUserName(), () -> !validateSmsCode(phonenumber, smsCode));
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
LoginUser loginUser = loginService.buildLoginUser(user);
|
||||
|
||||
loginUser.setClientKey(client.getClientKey());
|
||||
loginUser.setDeviceType(client.getDeviceType());
|
||||
SaLoginParameter model = new SaLoginParameter();
|
||||
model.setDeviceType(client.getDeviceType());
|
||||
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
|
||||
// 例如: 后台用户30分钟过期 app用户1天过期
|
||||
model.setTimeout(client.getTimeout());
|
||||
model.setActiveTimeout(client.getActiveTimeout());
|
||||
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
|
||||
// 生成token
|
||||
LoginHelper.login(loginUser, model);
|
||||
|
||||
LoginVo loginVo = new LoginVo();
|
||||
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||
loginVo.setClientId(client.getClientId());
|
||||
return loginVo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验短信验证码
|
||||
*/
|
||||
private boolean validateSmsCode(String phonenumber, String smsCode) {
|
||||
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + phonenumber);
|
||||
if (StringUtils.isBlank(code)) {
|
||||
loginService.recordLogininfor(phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
return code.equals(smsCode);
|
||||
}
|
||||
|
||||
private SysUserVo loadUserByPhonenumber(String phonenumber) {
|
||||
SysUserVo user = userService.selectUserByPhonenumber(phonenumber);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
log.info("登录用户:{} 不存在.", phonenumber);
|
||||
throw new UserException("user.not.exists", phonenumber);
|
||||
} else if (SystemConstants.DISABLE.equals(user.getStatus())) {
|
||||
log.info("登录用户:{} 已被停用.", phonenumber);
|
||||
throw new UserException("user.blocked", phonenumber);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package org.dromara.web.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import me.zhyd.oauth.model.AuthResponse;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.core.domain.model.SocialLoginBody;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.ValidatorUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.common.social.config.properties.SocialProperties;
|
||||
import org.dromara.common.social.utils.SocialUtils;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.system.domain.vo.SysSocialVo;
|
||||
import org.dromara.system.domain.vo.SysUserVo;
|
||||
import org.dromara.system.service.SysUserService;
|
||||
import org.dromara.system.service.SysSocialService;
|
||||
import org.dromara.web.domain.vo.LoginVo;
|
||||
import org.dromara.web.service.IAuthStrategy;
|
||||
import org.dromara.web.service.SysLoginService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 第三方授权策略
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("social" + IAuthStrategy.BASE_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class SocialAuthStrategy implements IAuthStrategy {
|
||||
|
||||
private final SocialProperties socialProperties;
|
||||
private final SysSocialService sysSocialService;
|
||||
private final SysUserService userService;
|
||||
private final SysLoginService loginService;
|
||||
|
||||
/**
|
||||
* 登录-第三方授权登录
|
||||
*
|
||||
* @param body 登录信息
|
||||
* @param client 客户端信息
|
||||
*/
|
||||
@Override
|
||||
public LoginVo login(String body, SysClientVo client) {
|
||||
SocialLoginBody loginBody = JsonUtils.parseObject(body, SocialLoginBody.class);
|
||||
ValidatorUtils.validate(loginBody);
|
||||
AuthResponse<AuthUser> response = SocialUtils.loginAuth(
|
||||
loginBody.getSource(), loginBody.getSocialCode(),
|
||||
loginBody.getSocialState(), socialProperties);
|
||||
if (!response.ok()) {
|
||||
throw new ServiceException(response.getMsg());
|
||||
}
|
||||
AuthUser authUserData = response.getData();
|
||||
|
||||
List<SysSocialVo> list = sysSocialService.selectByAuthId(authUserData.getSource() + authUserData.getUuid());
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
throw new ServiceException("你还没有绑定第三方账号,绑定后才可以登录!");
|
||||
}
|
||||
SysSocialVo social = list.get(0);
|
||||
|
||||
SysUserVo user = loadUser(social.getUserId());
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
LoginUser loginUser = loginService.buildLoginUser(user);
|
||||
|
||||
loginUser.setClientKey(client.getClientKey());
|
||||
loginUser.setDeviceType(client.getDeviceType());
|
||||
SaLoginParameter model = new SaLoginParameter();
|
||||
model.setDeviceType(client.getDeviceType());
|
||||
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
|
||||
// 例如: 后台用户30分钟过期 app用户1天过期
|
||||
model.setTimeout(client.getTimeout());
|
||||
model.setActiveTimeout(client.getActiveTimeout());
|
||||
model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
|
||||
// 生成token
|
||||
LoginHelper.login(loginUser, model);
|
||||
|
||||
LoginVo loginVo = new LoginVo();
|
||||
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||
loginVo.setClientId(client.getClientId());
|
||||
return loginVo;
|
||||
}
|
||||
|
||||
private SysUserVo loadUser(Long userId) {
|
||||
SysUserVo user = userService.selectUserById(userId);
|
||||
if (ObjectUtil.isNull(user)) {
|
||||
log.info("登录用户:{} 不存在.", "");
|
||||
throw new UserException("user.not.exists", "");
|
||||
} else if (SystemConstants.DISABLE.equals(user.getStatus())) {
|
||||
log.info("登录用户:{} 已被停用.", "");
|
||||
throw new UserException("user.blocked", "");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,274 @@
|
||||
# AI模块配置
|
||||
ruoyi:
|
||||
ai:
|
||||
# 是否启用AI模块
|
||||
enabled: true
|
||||
|
||||
# 默认配置
|
||||
default:
|
||||
# 默认超时时间(秒)
|
||||
timeout: 30
|
||||
# 默认最大重试次数
|
||||
max-retries: 3
|
||||
# 默认最大并发数
|
||||
max-concurrency: 10
|
||||
# 默认权重
|
||||
weight: 1
|
||||
# 默认优先级
|
||||
priority: 0
|
||||
|
||||
# 缓存配置
|
||||
cache:
|
||||
# 是否启用缓存
|
||||
enabled: true
|
||||
# 缓存过期时间(分钟)
|
||||
expire-minutes: 60
|
||||
# 缓存键前缀
|
||||
key-prefix: "ai:chat:cache:"
|
||||
|
||||
# 健康检查配置
|
||||
health:
|
||||
# 健康检查间隔(秒)
|
||||
check-interval: 60
|
||||
# 健康检查超时时间(秒)
|
||||
check-timeout: 10
|
||||
# 缓存键前缀
|
||||
cache-prefix: "ai:health:"
|
||||
|
||||
# 流式响应配置
|
||||
stream:
|
||||
# 是否启用流式响应
|
||||
enabled: true
|
||||
# 流式响应缓冲区大小
|
||||
buffer-size: 1024
|
||||
# 流式响应超时时间(秒)
|
||||
timeout: 60
|
||||
|
||||
# 负载均衡配置
|
||||
load-balance:
|
||||
# 负载均衡策略(ROUND_ROBIN, WEIGHTED_ROUND_ROBIN, RANDOM, LEAST_CONNECTIONS)
|
||||
strategy: WEIGHTED_ROUND_ROBIN
|
||||
# 是否启用健康检查
|
||||
health-check: true
|
||||
|
||||
# 熔断器配置
|
||||
circuit-breaker:
|
||||
# 是否启用熔断器
|
||||
enabled: true
|
||||
# 失败阈值
|
||||
failure-threshold: 5
|
||||
# 恢复时间(秒)
|
||||
recovery-timeout: 30
|
||||
# 半开状态最大请求数
|
||||
half-open-max-calls: 3
|
||||
|
||||
# 限流配置
|
||||
rate-limit:
|
||||
# 是否启用限流
|
||||
enabled: true
|
||||
# 每秒最大请求数
|
||||
max-requests-per-second: 100
|
||||
# 突发请求数
|
||||
burst-capacity: 200
|
||||
|
||||
# 监控配置
|
||||
monitoring:
|
||||
# 是否启用监控
|
||||
enabled: true
|
||||
# 统计数据保留天数
|
||||
stats-retention-days: 30
|
||||
# 是否记录详细日志
|
||||
detailed-logging: false
|
||||
|
||||
# 故障转移配置
|
||||
failover:
|
||||
enabled: true
|
||||
check-interval-seconds: 30
|
||||
failure-threshold: 3
|
||||
recovery-threshold: 2
|
||||
|
||||
# 知识库配置
|
||||
knowledge:
|
||||
enabled: true
|
||||
default-search-count: 5
|
||||
similarity-threshold: 0.7
|
||||
max-context-length: 4000
|
||||
reranker-enabled: false
|
||||
embedding:
|
||||
base-url:
|
||||
default-model: bge-large-zh
|
||||
vector-store:
|
||||
type: db
|
||||
qdrant:
|
||||
base-url: http://localhost:6333
|
||||
api-key:
|
||||
collection: knowledge_vectors
|
||||
distance: Cosine
|
||||
weaviate:
|
||||
base-url: http://localhost:8080
|
||||
api-key:
|
||||
class-name: KnowledgeVector
|
||||
chunk:
|
||||
size: 1000
|
||||
overlap: 100
|
||||
|
||||
# 外部AI配置
|
||||
external-ai:
|
||||
# OpenAI配置
|
||||
openai:
|
||||
enabled: false
|
||||
api-key: ${OPENAI_API_KEY:}
|
||||
base-url: https://api.openai.com
|
||||
default-model: gpt-3.5-turbo
|
||||
timeout: 60s
|
||||
|
||||
# Azure配置
|
||||
azure:
|
||||
enabled: false
|
||||
api-key: ${AZURE_API_KEY:}
|
||||
endpoint: ${AZURE_ENDPOINT:}
|
||||
deployment-name: ${AZURE_DEPLOYMENT_NAME:}
|
||||
api-version: 2023-12-01-preview
|
||||
|
||||
# ChatGLM配置
|
||||
chatglm:
|
||||
enabled: false
|
||||
api-key: ${CHATGLM_API_KEY:}
|
||||
base-url: https://open.bigmodel.cn
|
||||
default-model: glm-4
|
||||
|
||||
# 通义千问配置
|
||||
qwen:
|
||||
enabled: false
|
||||
api-key: ${QWEN_API_KEY:}
|
||||
base-url: https://dashscope.aliyuncs.com
|
||||
default-model: qwen-turbo
|
||||
|
||||
# 智谱AI配置
|
||||
zhipu:
|
||||
enabled: false
|
||||
api-key: ${ZHIPU_API_KEY:}
|
||||
base-url: https://open.bigmodel.cn
|
||||
default-model: glm-4
|
||||
|
||||
# AI平台配置
|
||||
ai-platform:
|
||||
# FastGPT配置
|
||||
fastgpt:
|
||||
enabled: false
|
||||
base-url: ${FASTGPT_BASE_URL:}
|
||||
api-key: ${FASTGPT_API_KEY:}
|
||||
default-app-id: ${FASTGPT_DEFAULT_APP_ID:}
|
||||
|
||||
# Coze配置
|
||||
coze:
|
||||
enabled: false
|
||||
base-url: https://www.coze.com
|
||||
api-key: ${COZE_API_KEY:}
|
||||
default-bot-id: ${COZE_DEFAULT_BOT_ID:}
|
||||
|
||||
# DIFY配置
|
||||
dify:
|
||||
enabled: false
|
||||
base-url: ${DIFY_BASE_URL:}
|
||||
api-key: ${DIFY_API_KEY:}
|
||||
default-app-id: ${DIFY_DEFAULT_APP_ID:}
|
||||
|
||||
# Provider特定配置
|
||||
providers:
|
||||
# OpenAI配置
|
||||
openai:
|
||||
# API基础URL
|
||||
base-url: "https://api.openai.com/v1"
|
||||
# 默认模型
|
||||
default-model: "gpt-3.5-turbo"
|
||||
# 支持的模型列表
|
||||
supported-models:
|
||||
- "gpt-3.5-turbo"
|
||||
- "gpt-4"
|
||||
- "gpt-4-turbo"
|
||||
|
||||
# Azure OpenAI配置
|
||||
azure-openai:
|
||||
# API版本
|
||||
api-version: "2023-12-01-preview"
|
||||
# 默认模型
|
||||
default-model: "gpt-35-turbo"
|
||||
# 支持的模型列表
|
||||
supported-models:
|
||||
- "gpt-35-turbo"
|
||||
- "gpt-4"
|
||||
|
||||
# ChatGLM配置
|
||||
chatglm:
|
||||
# API基础URL
|
||||
base-url: "https://open.bigmodel.cn/api/paas/v4"
|
||||
# 默认模型
|
||||
default-model: "glm-4"
|
||||
# 支持的模型列表
|
||||
supported-models:
|
||||
- "glm-4"
|
||||
- "glm-3-turbo"
|
||||
|
||||
# 通义千问配置
|
||||
qwen:
|
||||
# API基础URL
|
||||
base-url: "https://dashscope.aliyuncs.com/api/v1"
|
||||
# 默认模型
|
||||
default-model: "qwen-turbo"
|
||||
# 支持的模型列表
|
||||
supported-models:
|
||||
- "qwen-turbo"
|
||||
- "qwen-plus"
|
||||
- "qwen-max"
|
||||
|
||||
# 智谱AI配置
|
||||
zhipu:
|
||||
# API基础URL
|
||||
base-url: "https://open.bigmodel.cn/api/paas/v4"
|
||||
# 默认模型
|
||||
default-model: "glm-4"
|
||||
# 支持的模型列表
|
||||
supported-models:
|
||||
- "glm-4"
|
||||
- "glm-3-turbo"
|
||||
|
||||
# FastGPT配置
|
||||
fastgpt:
|
||||
# 默认模型
|
||||
default-model: "fastgpt"
|
||||
# 支持的模型列表
|
||||
supported-models:
|
||||
- "fastgpt"
|
||||
|
||||
# Coze配置
|
||||
coze:
|
||||
# API基础URL
|
||||
base-url: "https://www.coze.cn/api"
|
||||
# 默认模型
|
||||
default-model: "coze"
|
||||
# 支持的模型列表
|
||||
supported-models:
|
||||
- "coze"
|
||||
|
||||
# Dify配置
|
||||
dify:
|
||||
# 默认模型
|
||||
default-model: "dify"
|
||||
# 支持的模型列表
|
||||
supported-models:
|
||||
- "dify"
|
||||
|
||||
# 知识库配置
|
||||
knowledge:
|
||||
# 是否启用
|
||||
enabled: true
|
||||
# 默认模型
|
||||
default-model: "knowledge"
|
||||
# 支持的模型列表
|
||||
supported-models:
|
||||
- "knowledge"
|
||||
# 相似度阈值
|
||||
similarity-threshold: 0.7
|
||||
# 最大返回结果数
|
||||
max-results: 10
|
||||
@@ -0,0 +1,337 @@
|
||||
--- # 监控中心配置
|
||||
spring.boot.admin.client:
|
||||
# 增加客户端开关
|
||||
enabled: false
|
||||
url: http://localhost:9090/admin
|
||||
instance:
|
||||
service-host-type: IP
|
||||
metadata:
|
||||
username: ${spring.boot.admin.client.username}
|
||||
userpassword: ${spring.boot.admin.client.password}
|
||||
username: @monitor.username@
|
||||
password: @monitor.password@
|
||||
|
||||
--- # snail-job 配置
|
||||
snail-job:
|
||||
enabled: false
|
||||
# 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
|
||||
group: "ruoyi_group"
|
||||
# SnailJob 接入验证令牌 详见 script/sql/ry_job.sql `sj_group_config` 表
|
||||
token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
|
||||
server:
|
||||
host: 127.0.0.1
|
||||
port: 17888
|
||||
# 命名空间UUID 详见 script/sql/ry_job.sql `sj_namespace`表`unique_id`字段
|
||||
namespace: ${spring.profiles.active}
|
||||
# 随主应用端口漂移
|
||||
port: 2${server.port}
|
||||
# 客户端ip指定
|
||||
host:
|
||||
# RPC类型: netty, grpc
|
||||
rpc-type: grpc
|
||||
|
||||
--- # 数据源配置
|
||||
spring:
|
||||
datasource:
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
# 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
|
||||
dynamic:
|
||||
# 性能分析插件(有性能损耗 不建议生产环境使用)
|
||||
p6spy: true
|
||||
# 设置默认的数据源或者数据源组,默认值即为 master
|
||||
primary: master
|
||||
# 严格模式 匹配不到数据源则报错
|
||||
strict: true
|
||||
datasource:
|
||||
# 主库数据源
|
||||
master:
|
||||
type: ${spring.datasource.type}
|
||||
driverClassName: org.postgresql.Driver
|
||||
url: jdbc:postgresql://localhost:5432/postgres?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
|
||||
username: postgres
|
||||
password: 123456
|
||||
# # 从库数据源
|
||||
# slave:
|
||||
# lazy: true
|
||||
# type: ${spring.datasource.type}
|
||||
# driverClassName: com.mysql.cj.jdbc.Driver
|
||||
# url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
# username:
|
||||
# password:
|
||||
# oracle:
|
||||
# type: ${spring.datasource.type}
|
||||
# driverClassName: oracle.jdbc.OracleDriver
|
||||
# url: jdbc:oracle:thin:@//localhost:1521/XE
|
||||
# username: ROOT
|
||||
# password: root
|
||||
# postgres:
|
||||
# type: ${spring.datasource.type}
|
||||
# driverClassName: org.postgresql.Driver
|
||||
# url: jdbc:postgresql://localhost:5432/postgres?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
|
||||
# username: root
|
||||
# password: root
|
||||
# sqlserver:
|
||||
# type: ${spring.datasource.type}
|
||||
# driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
|
||||
# url: jdbc:sqlserver://localhost:1433;DatabaseName=tempdb;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true
|
||||
# username: SA
|
||||
# password: root
|
||||
hikari:
|
||||
# 最大连接池数量
|
||||
maxPoolSize: 20
|
||||
# 最小空闲线程数量
|
||||
minIdle: 10
|
||||
# 配置获取连接等待超时的时间
|
||||
connectionTimeout: 30000
|
||||
# 校验超时时间
|
||||
validationTimeout: 5000
|
||||
# 空闲连接存活最大时间,默认10分钟
|
||||
idleTimeout: 600000
|
||||
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认30分钟
|
||||
maxLifetime: 1800000
|
||||
# 多久检查一次连接的活性
|
||||
keepaliveTime: 30000
|
||||
|
||||
--- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉)
|
||||
spring.data:
|
||||
redis:
|
||||
# 地址
|
||||
host: localhost
|
||||
# 端口,默认为6379
|
||||
port: 6379
|
||||
# 数据库索引
|
||||
database: 0
|
||||
# redis 密码必须配置
|
||||
password: 123456
|
||||
# 连接超时时间
|
||||
timeout: 10s
|
||||
# 是否开启ssl连接
|
||||
ssl.enabled: false
|
||||
|
||||
# redisson 配置
|
||||
redisson:
|
||||
# redis key前缀
|
||||
keyPrefix:
|
||||
# 线程池数量
|
||||
threads: 4
|
||||
# Netty线程池数量
|
||||
nettyThreads: 8
|
||||
# 单节点配置
|
||||
singleServerConfig:
|
||||
# 客户端名称 不能用中文
|
||||
clientName: RuoYi-Vue-Plus
|
||||
# 最小空闲连接数
|
||||
connectionMinimumIdleSize: 8
|
||||
# 连接池大小
|
||||
connectionPoolSize: 32
|
||||
# 连接空闲超时,单位:毫秒
|
||||
idleConnectionTimeout: 10000
|
||||
# 命令等待超时,单位:毫秒
|
||||
timeout: 3000
|
||||
# 发布和订阅连接池大小
|
||||
subscriptionConnectionPoolSize: 50
|
||||
|
||||
--- # mail 邮件发送
|
||||
mail:
|
||||
enabled: false
|
||||
host: smtp.163.com
|
||||
port: 465
|
||||
# 是否需要用户名密码验证
|
||||
auth: true
|
||||
# 发送方,遵循RFC-822标准
|
||||
from: xxx@163.com
|
||||
# 用户名(注意:如果使用foxmail邮箱,此处user为qq号)
|
||||
user: xxx@163.com
|
||||
# 密码(注意,某些邮箱需要为SMTP服务单独设置密码,详情查看相关帮助)
|
||||
pass: xxxxxxxxxx
|
||||
# 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。
|
||||
starttlsEnable: true
|
||||
# 使用SSL安全连接
|
||||
sslEnable: true
|
||||
# SMTP超时时长,单位毫秒,缺省值不超时
|
||||
timeout: 0
|
||||
# Socket连接超时值,单位毫秒,缺省值不超时
|
||||
connectionTimeout: 0
|
||||
|
||||
--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
|
||||
# https://sms4j.com/doc3/ 差异配置文档地址 支持单厂商多配置,可以配置多个同时使用
|
||||
sms:
|
||||
# 配置源类型用于标定配置来源(interface,yaml)
|
||||
config-type: yaml
|
||||
# 用于标定yml中的配置是否开启短信拦截,接口配置不受此限制
|
||||
restricted: true
|
||||
# 短信拦截限制单手机号每分钟最大发送,只对开启了拦截的配置有效
|
||||
minute-max: 1
|
||||
# 短信拦截限制单手机号每日最大发送量,只对开启了拦截的配置有效
|
||||
account-max: 30
|
||||
# 以下配置来自于 org.dromara.sms4j.provider.config.BaseConfig类中
|
||||
blends:
|
||||
# 唯一ID 用于发送短信寻找具体配置 随便定义别用中文即可
|
||||
# 可以同时存在两个相同厂商 例如: ali1 ali2 两个不同的阿里短信账号
|
||||
config1:
|
||||
# 框架定义的厂商名称标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
|
||||
supplier: alibaba
|
||||
# 有些称为accessKey有些称之为apiKey,也有称为sdkKey或者appId。
|
||||
access-key-id: 您的accessKey
|
||||
# 称为accessSecret有些称之为apiSecret
|
||||
access-key-secret: 您的accessKeySecret
|
||||
signature: 您的短信签名
|
||||
sdk-app-id: 您的sdkAppId
|
||||
config2:
|
||||
# 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
|
||||
supplier: tencent
|
||||
access-key-id: 您的accessKey
|
||||
access-key-secret: 您的accessKeySecret
|
||||
signature: 您的短信签名
|
||||
sdk-app-id: 您的sdkAppId
|
||||
|
||||
|
||||
--- # 三方授权
|
||||
justauth:
|
||||
# 前端外网访问地址
|
||||
address: http://localhost:80
|
||||
type:
|
||||
maxkey:
|
||||
# maxkey 服务器地址
|
||||
# 注意 如下均配置均不需要修改 maxkey 已经内置好了数据
|
||||
server-url: http://sso.maxkey.top
|
||||
client-id: 876892492581044224
|
||||
client-secret: x1Y5MTMwNzIwMjMxNTM4NDc3Mzche8
|
||||
redirect-uri: ${justauth.address}/social-callback?source=maxkey
|
||||
topiam:
|
||||
# topiam 服务器地址
|
||||
server-url: http://127.0.0.1:1898/api/v1/authorize/y0q************spq***********8ol
|
||||
client-id: 449c4*********937************759
|
||||
client-secret: ac7***********1e0************28d
|
||||
redirect-uri: ${justauth.address}/social-callback?source=topiam
|
||||
scopes: [ openid, email, phone, profile ]
|
||||
qq:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=qq
|
||||
union-id: false
|
||||
weibo:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=weibo
|
||||
gitee:
|
||||
client-id: 9ceb2ba3150369c0278f69f7eab35ba41dd011a568368c65bb132125a4d95821
|
||||
client-secret: 9801fe69875da3b56f0773ae6737d416b6888f3f4af04d6bca5b63055728859c
|
||||
redirect-uri: ${justauth.address}/social-callback?source=gitee
|
||||
dingtalk:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=dingtalk
|
||||
baidu:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=baidu
|
||||
csdn:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=csdn
|
||||
coding:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=coding
|
||||
coding-group-name: xx
|
||||
oschina:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=oschina
|
||||
alipay_wallet:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=alipay_wallet
|
||||
alipay-public-key: MIIB**************DAQAB
|
||||
wechat_open:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=wechat_open
|
||||
wechat_mp:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=wechat_mp
|
||||
wechat_miniprogram:
|
||||
client-id: wx329e1cc66e69f88a
|
||||
client-secret: 181c3b076969a5cb935fb88f5fe6347b
|
||||
redirect-uri: ${justauth.address}/social-callback?source=wechat_miniprogram
|
||||
wechat_enterprise:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=wechat_enterprise
|
||||
agent-id: 1000002
|
||||
gitlab:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=gitlab
|
||||
gitea:
|
||||
# 前端改动 https://gitee.com/JavaLionLi/plus-ui/pulls/204
|
||||
# gitea 服务器地址
|
||||
server-url: https://demo.gitea.com
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=gitea
|
||||
|
||||
|
||||
--- # 微信平台
|
||||
wx:
|
||||
miniapp:
|
||||
configs:
|
||||
# 测试号
|
||||
- appid: wxb1e1ffda7cb636e9
|
||||
secret: 80bae6f0fd391a95a89f5ad867bd6991
|
||||
token: mvlcmI66IVHDLZP8ZFdCKy6okr2thHgr
|
||||
aesKey: #微信小程序消息服务器配置的EncodingAESKey
|
||||
msgDataFormat: JSON
|
||||
# 测试号
|
||||
- appid: wxb1e1ffda7cb636e1
|
||||
secret: 083ae010dae63a0e2efa851245581802
|
||||
token: mvlcmI66IVHDLZP8ZFdCKy6okr2thHgr
|
||||
aesKey: Jv70essZrhV4cguBvUh4nwu3bjVTQcRJeAUzqBBbdqz
|
||||
msgDataFormat: JSON
|
||||
mp:
|
||||
useRedis: false
|
||||
redisConfig:
|
||||
host:
|
||||
port:
|
||||
database:
|
||||
password:
|
||||
timeout:
|
||||
configs:
|
||||
# 测试号
|
||||
- appId: wxb1e1ffda7cb636e9
|
||||
secret: aab69b83d10226bd8608ec4cf6c6ec0c
|
||||
token: mvlcmI66IVHDLZP8ZFdCKy6okr2thHgr
|
||||
aesKey:
|
||||
oauth2RedirectUrl: http://192.168.0.3:8080/pages/home/main
|
||||
qrConnectRedirectUrl: http://192.168.0.3:8080/pages/home/main
|
||||
# 测试号
|
||||
- appId: wxb1e1ffda7cb636e1
|
||||
secret: aab69b83d10226bd8608ec4cf6c6ec0c
|
||||
token: mvlcmI66IVHDLZP8ZFdCKy6okr2thHgr
|
||||
aesKey: WCnqUoRueykM2E3oDsFAacXBUjioC10EfxwO2TMhYhT
|
||||
oauth2RedirectUrl: http://192.168.0.22:9090/#/pages/index
|
||||
qrConnectRedirectUrl: http://192.168.0.22:8080/#/pages/index
|
||||
|
||||
pay:
|
||||
alipay:
|
||||
appId: 2021003164606727
|
||||
mchId: 2088541440500474
|
||||
serverUrl: https://openapi.alipay.com/gateway.do
|
||||
alipayPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmDCp3S+3WiWZsZPrTZU40wgnb2LlGPxlUnPrlGGZipKtSx+qNrquh0gfWFFXx4PxArkJN2bh39eeUHlNb+WUTQJLq8eMa7jLw8WRpfi5e0O0zzwv8XGGqc3Rt97RAQqOTXTPUJ0hVMtr6DLX/0aE9p3kTh4+BK9pGo/o9HK/NDG5zKP6DDuAfAOpY48ktFmhZOHd799Ki8p7w2CRLxAmuicC8RaQsR7jv/AJLEIwF7WIAmJmviDDhjuHOyUaqoHEC14EHmVw68IBK5xcFct7QvRjJoi703zc7yXlzClRdkjw06PC8SQ267TRg0z1jYRPyDBzDRhjvv1rZPijtwC7JQIDAQAB
|
||||
privateKey: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCYMKndL7daJZmxk+tNlTjTCCdvYuUY/GVSc+uUYZmKkq1LH6o2uq6HSB9YUVfHg/ECuQk3ZuHf155QeU1v5ZRNAkurx4xruMvDxZGl+Ll7Q7TPPC/xcYapzdG33tEBCo5NdM9QnSFUy2voMtf/RoT2neROHj4Er2kaj+j0cr80MbnMo/oMO4B8A6ljjyS0WaFk4d3v30qLynvDYJEvECa6JwLxFpCxHuO/8AksQjAXtYgCYma+IMOGO4c7JRqqgcQLXgQeZXDrwgErnFwVy3tC9GMmiLvTfNzvJeXMKVF2SPDTo8LxJDbrtNGDTPWNhE/IMHMNGGO+/Wtk+KO3ALslAgMBAAECggEAUBx9rgLqHzffuYxFtqcwLFYEfCuwsQBbTZXbklX1/u3K7tqSPRDzwh6C8XiQHmQjv+0rHtC3YZ8cpPvVeWt+LzNgJeJ61lGGYV1kl9tft7UiPlxOWGgMHOJM3N8bYdLuqXMtlh/AZeRWvvnUnXm/kBn5De35c3nqd6L9W3/zikM5y9MuMQR23iSaNtoeBVGLkX/i9otf12+UrWv/loLJpqLCw8C5MT+zbXZgFZG/zBHjk6oUoiYB9NLUj0C+V9w6/ROGXoBAfBYEbxVgiQKIlSySdozYOlzt3Mp0FSS/RevjNoVHj4eDiOQZDxGkpzEg8LRSwhUn254/echQSyuimQKBgQDafIXftqpt9ts27S/q4GSHMB1qN9QiKJ6iZAAel98bEe42Zvum0nEUxxGQ2tADBtz+lnHBi4VvSrid9O1fsmih0FtXMZcG6hMBY49G1fXpbhGI6kSZBxKPeDgiLb0rgmvAdCBtgUx6ML50lW0dVwEXbobQ2LKVqOE/LNlnPU5QnwKBgQCyUh0q8Lqk4cXDg/qnYHbtv43CBOend/frBsSxrYKkf6CiV2kbKJFeBSjBOD8YtGad1Pm2EhloAotJUAh2nqybV4CQkBZUEYQDtM2b5yJF8xCn5lbf+S7rmij1zP0dyOtLdKiOMwtRdBFJJthw1gt6eFhpD+c5CvpqSTD00yHJuwKBgQCb98iZqwx+83oJ+8f5I7afyvk0miYVPGoCAuES9deOu34R1/JNZGzVKEah4ZIclwmrtDoAsFjQ2cZw/Cd36SRIXzTVSdFGXlKy6x+csaCawrhBxPqzQxk80dVAkOY56SCCgmOjyGmP0Lwk/YanKzTcRUp4TDkwHR6uupV33YvKgQKBgDqNKdSirLZdB8G2AUSaMRLJtfNCBwp/IuGCHG226lG3Mnh7uSBYxrqXeRVQsa8b9SHX/5JgCQWU6EVPSSgh2806AxX0qdA63B0XbffGAgPz1sE6qcXrHRPxT4e+IlJ4WYIyMPJYIlxBfzeE0MbkEWrKP0VoGmUpjKX/mFqbRNnnAoGAf3YEakCzCyd6PylB+gWsajoQfiZOaOkwReX8/5P7atVmpNSNPOaehvtQE8Bd8lu/9Bh3LYfpb2+giVVsdujqPDShez+q6hSMyrW1TSw8we7iY1J0J0r9RwxAdokdLH2J8JX6LuWP7NY9VHDNXbERcDVjat15gukJmuK85KqMFSc=
|
||||
notifyUrl: http://bdhdm3.natappfree.cc/no-auth/ali/notify/trade
|
||||
returnUrl: xxxxpay/notify
|
||||
signType: RSA2
|
||||
charset: UTF-8
|
||||
wechat:
|
||||
appMp1Id: wxb7250f9a052e9015
|
||||
appMp2Id: wx11fde1504adee855
|
||||
mchId: 1635571318
|
||||
mchSerialNo: 3177DDC954F864ADE851086DB3E24A7AB932A711
|
||||
privateKey: -----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDG0SuHJE8dXLNk
|
||||
-----END PRIVATE KEY-----
|
||||
apiV3Key: geneface27bdaf31d54447d8b7b7c7d7
|
||||
notifyUrl: https://ppm-stage.geneface.com/gf-api
|
||||
@@ -0,0 +1,276 @@
|
||||
--- # 临时文件存储位置 避免临时文件被系统清理报错
|
||||
spring.servlet.multipart.location: /ruoyi/server/temp
|
||||
|
||||
--- # 监控中心配置
|
||||
spring.boot.admin.client:
|
||||
# 增加客户端开关
|
||||
enabled: true
|
||||
url: http://localhost:9090/admin
|
||||
instance:
|
||||
service-host-type: IP
|
||||
metadata:
|
||||
username: ${spring.boot.admin.client.username}
|
||||
userpassword: ${spring.boot.admin.client.password}
|
||||
username: @monitor.username@
|
||||
password: @monitor.password@
|
||||
|
||||
--- # snail-job 配置
|
||||
snail-job:
|
||||
enabled: true
|
||||
# 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
|
||||
group: "ruoyi_group"
|
||||
# SnailJob 接入验证令牌 详见 script/sql/ry_job.sql `sj_group_config`表
|
||||
token: "SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT"
|
||||
server:
|
||||
host: 127.0.0.1
|
||||
port: 17888
|
||||
# 命名空间UUID 详见 script/sql/ry_job.sql `sj_namespace`表`unique_id`字段
|
||||
namespace: ${spring.profiles.active}
|
||||
# 随主应用端口漂移
|
||||
port: 2${server.port}
|
||||
# 客户端ip指定
|
||||
host:
|
||||
# RPC类型: netty, grpc
|
||||
rpc-type: grpc
|
||||
|
||||
--- # 数据源配置
|
||||
spring:
|
||||
datasource:
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
# 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
|
||||
dynamic:
|
||||
# 性能分析插件(有性能损耗 不建议生产环境使用)
|
||||
p6spy: false
|
||||
# 设置默认的数据源或者数据源组,默认值即为 master
|
||||
primary: master
|
||||
# 严格模式 匹配不到数据源则报错
|
||||
strict: true
|
||||
datasource:
|
||||
# 主库数据源
|
||||
master:
|
||||
type: ${spring.datasource.type}
|
||||
driverClassName: org.postgresql.Driver
|
||||
url: jdbc:postgresql://localhost:5432/postgres?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
|
||||
username: postgres
|
||||
password: 123456
|
||||
# # 从库数据源
|
||||
# slave:
|
||||
# lazy: true
|
||||
# type: ${spring.datasource.type}
|
||||
# driverClassName: com.mysql.cj.jdbc.Driver
|
||||
# url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
# username:
|
||||
# password:
|
||||
# oracle:
|
||||
# type: ${spring.datasource.type}
|
||||
# driverClassName: oracle.jdbc.OracleDriver
|
||||
# url: jdbc:oracle:thin:@//localhost:1521/XE
|
||||
# username: ROOT
|
||||
# password: root
|
||||
# postgres:
|
||||
# type: ${spring.datasource.type}
|
||||
# driverClassName: org.postgresql.Driver
|
||||
# url: jdbc:postgresql://localhost:5432/postgres?useUnicode=true&characterEncoding=utf8&useSSL=true&autoReconnect=true&reWriteBatchedInserts=true
|
||||
# username: root
|
||||
# password: root
|
||||
# sqlserver:
|
||||
# type: ${spring.datasource.type}
|
||||
# driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
|
||||
# url: jdbc:sqlserver://localhost:1433;DatabaseName=tempdb;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true
|
||||
# username: SA
|
||||
# password: root
|
||||
hikari:
|
||||
# 最大连接池数量
|
||||
maxPoolSize: 20
|
||||
# 最小空闲线程数量
|
||||
minIdle: 10
|
||||
# 配置获取连接等待超时的时间
|
||||
connectionTimeout: 30000
|
||||
# 校验超时时间
|
||||
validationTimeout: 5000
|
||||
# 空闲连接存活最大时间,默认10分钟
|
||||
idleTimeout: 600000
|
||||
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认30分钟
|
||||
maxLifetime: 1800000
|
||||
# 多久检查一次连接的活性
|
||||
keepaliveTime: 30000
|
||||
|
||||
--- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉)
|
||||
spring.data:
|
||||
redis:
|
||||
# 地址
|
||||
host: localhost
|
||||
# 端口,默认为6379
|
||||
port: 6379
|
||||
# 数据库索引
|
||||
database: 0
|
||||
# redis 密码必须配置
|
||||
password: ruoyi123
|
||||
# 连接超时时间
|
||||
timeout: 10s
|
||||
# 是否开启ssl
|
||||
ssl.enabled: false
|
||||
|
||||
# redisson 配置
|
||||
redisson:
|
||||
# redis key前缀
|
||||
keyPrefix:
|
||||
# 线程池数量
|
||||
threads: 16
|
||||
# Netty线程池数量
|
||||
nettyThreads: 32
|
||||
# 单节点配置
|
||||
singleServerConfig:
|
||||
# 客户端名称 不能用中文
|
||||
clientName: RuoYi-Vue-Plus
|
||||
# 最小空闲连接数
|
||||
connectionMinimumIdleSize: 32
|
||||
# 连接池大小
|
||||
connectionPoolSize: 64
|
||||
# 连接空闲超时,单位:毫秒
|
||||
idleConnectionTimeout: 10000
|
||||
# 命令等待超时,单位:毫秒
|
||||
timeout: 3000
|
||||
# 发布和订阅连接池大小
|
||||
subscriptionConnectionPoolSize: 50
|
||||
|
||||
--- # mail 邮件发送
|
||||
mail:
|
||||
enabled: false
|
||||
host: smtp.163.com
|
||||
port: 465
|
||||
# 是否需要用户名密码验证
|
||||
auth: true
|
||||
# 发送方,遵循RFC-822标准
|
||||
from: xxx@163.com
|
||||
# 用户名(注意:如果使用foxmail邮箱,此处user为qq号)
|
||||
user: xxx@163.com
|
||||
# 密码(注意,某些邮箱需要为SMTP服务单独设置密码,详情查看相关帮助)
|
||||
pass: xxxxxxxxxx
|
||||
# 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。
|
||||
starttlsEnable: true
|
||||
# 使用SSL安全连接
|
||||
sslEnable: true
|
||||
# SMTP超时时长,单位毫秒,缺省值不超时
|
||||
timeout: 0
|
||||
# Socket连接超时值,单位毫秒,缺省值不超时
|
||||
connectionTimeout: 0
|
||||
|
||||
--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
|
||||
# https://sms4j.com/doc3/ 差异配置文档地址 支持单厂商多配置,可以配置多个同时使用
|
||||
sms:
|
||||
# 配置源类型用于标定配置来源(interface,yaml)
|
||||
config-type: yaml
|
||||
# 用于标定yml中的配置是否开启短信拦截,接口配置不受此限制
|
||||
restricted: true
|
||||
# 短信拦截限制单手机号每分钟最大发送,只对开启了拦截的配置有效
|
||||
minute-max: 1
|
||||
# 短信拦截限制单手机号每日最大发送量,只对开启了拦截的配置有效
|
||||
account-max: 30
|
||||
# 以下配置来自于 org.dromara.sms4j.provider.config.BaseConfig类中
|
||||
blends:
|
||||
# 唯一ID 用于发送短信寻找具体配置 随便定义别用中文即可
|
||||
# 可以同时存在两个相同厂商 例如: ali1 ali2 两个不同的阿里短信账号
|
||||
config1:
|
||||
# 框架定义的厂商名称标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
|
||||
supplier: alibaba
|
||||
# 有些称为accessKey有些称之为apiKey,也有称为sdkKey或者appId。
|
||||
access-key-id: 您的accessKey
|
||||
# 称为accessSecret有些称之为apiSecret
|
||||
access-key-secret: 您的accessKeySecret
|
||||
signature: 您的短信签名
|
||||
sdk-app-id: 您的sdkAppId
|
||||
config2:
|
||||
# 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分
|
||||
supplier: tencent
|
||||
access-key-id: 您的accessKey
|
||||
access-key-secret: 您的accessKeySecret
|
||||
signature: 您的短信签名
|
||||
sdk-app-id: 您的sdkAppId
|
||||
|
||||
# 三方授权
|
||||
justauth:
|
||||
# 前端外网访问地址
|
||||
address: http://localhost:80
|
||||
type:
|
||||
maxkey:
|
||||
# maxkey 服务器地址
|
||||
# 注意 如下均配置均不需要修改 maxkey 已经内置好了数据
|
||||
server-url: http://sso.maxkey.top
|
||||
client-id: 876892492581044224
|
||||
client-secret: x1Y5MTMwNzIwMjMxNTM4NDc3Mzche8
|
||||
redirect-uri: ${justauth.address}/social-callback?source=maxkey
|
||||
topiam:
|
||||
# topiam 服务器地址
|
||||
server-url: http://127.0.0.1:1989/api/v1/authorize/y0q************spq***********8ol
|
||||
client-id: 449c4*********937************759
|
||||
client-secret: ac7***********1e0************28d
|
||||
redirect-uri: ${justauth.address}/social-callback?source=topiam
|
||||
scopes: [ openid, email, phone, profile ]
|
||||
qq:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=qq
|
||||
union-id: false
|
||||
weibo:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=weibo
|
||||
gitee:
|
||||
client-id: 91436b7940090d09c72c7daf85b959cfd5f215d67eea73acbf61b6b590751a98
|
||||
client-secret: 02c6fcfd70342980cd8dd2f2c06c1a350645d76c754d7a264c4e125f9ba915ac
|
||||
redirect-uri: ${justauth.address}/social-callback?source=gitee
|
||||
dingtalk:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=dingtalk
|
||||
baidu:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=baidu
|
||||
csdn:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=csdn
|
||||
coding:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=coding
|
||||
coding-group-name: xx
|
||||
oschina:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=oschina
|
||||
alipay_wallet:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=alipay_wallet
|
||||
alipay-public-key: MIIB**************DAQAB
|
||||
wechat_open:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=wechat_open
|
||||
wechat_mp:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=wechat_mp
|
||||
wechat_miniprogram:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=wechat_miniprogram
|
||||
wechat_enterprise:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=wechat_enterprise
|
||||
agent-id: 1000002
|
||||
gitlab:
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=gitlab
|
||||
gitea:
|
||||
# 前端改动 https://gitee.com/JavaLionLi/plus-ui/pulls/204
|
||||
# gitea 服务器地址
|
||||
server-url: https://demo.gitea.com
|
||||
client-id: 10**********6
|
||||
client-secret: 1f7d08**********5b7**********29e
|
||||
redirect-uri: ${justauth.address}/social-callback?source=gitea
|
||||
@@ -0,0 +1,274 @@
|
||||
# 开发环境配置
|
||||
server:
|
||||
# 服务器的HTTP端口,默认为8080
|
||||
port: 8080
|
||||
servlet:
|
||||
# 应用的访问路径
|
||||
context-path: /
|
||||
# undertow 配置
|
||||
undertow:
|
||||
# HTTP post内容的最大大小。当值为-1时,默认值为大小是无限的
|
||||
max-http-post-size: -1
|
||||
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
|
||||
# 每块buffer的空间大小,越小的空间被利用越充分
|
||||
buffer-size: 512
|
||||
# 是否分配的直接内存
|
||||
direct-buffers: true
|
||||
threads:
|
||||
# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
|
||||
io: 8
|
||||
# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
|
||||
worker: 256
|
||||
|
||||
captcha:
|
||||
# 是否启用验证码校验
|
||||
enable: true
|
||||
# 验证码类型 math 数组计算 char 字符验证
|
||||
type: MATH
|
||||
# line 线段干扰 circle 圆圈干扰 shear 扭曲干扰
|
||||
category: CIRCLE
|
||||
# 数字验证码位数
|
||||
numberLength: 1
|
||||
# 字符验证码长度
|
||||
charLength: 4
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
org.dromara: @logging.level@
|
||||
org.springframework: warn
|
||||
org.mybatis.spring.mapper: error
|
||||
org.apache.fury: warn
|
||||
config: classpath:logback-plus.xml
|
||||
|
||||
# 用户配置
|
||||
user:
|
||||
password:
|
||||
# 密码最大错误次数
|
||||
maxRetryCount: 5
|
||||
# 密码锁定时间(默认10分钟)
|
||||
lockTime: 10
|
||||
auth:
|
||||
# 验证码配置
|
||||
captcha:
|
||||
# App端验证码开关
|
||||
enabled: false
|
||||
# 验证码类型 math 数组计算 char 字符验证
|
||||
type: math
|
||||
# 白名单配置(多个路径用逗号分隔)
|
||||
whitelist: /app/user/profile,/app/user/bind,/app/auth/logout
|
||||
|
||||
# Spring配置
|
||||
spring:
|
||||
application:
|
||||
name: RuoYi-Vue-Plus
|
||||
threads:
|
||||
# 开启虚拟线程 仅jdk21可用
|
||||
virtual:
|
||||
enabled: false
|
||||
# 资源信息
|
||||
messages:
|
||||
# 国际化资源文件路径
|
||||
basename: i18n/messages
|
||||
profiles:
|
||||
active: @profiles.active@
|
||||
# 文件上传
|
||||
servlet:
|
||||
multipart:
|
||||
# 单个文件大小
|
||||
max-file-size: 10MB
|
||||
# 设置总上传的文件大小
|
||||
max-request-size: 20MB
|
||||
mvc:
|
||||
# 设置静态资源路径 防止所有请求都去查静态资源
|
||||
static-path-pattern: /static/**
|
||||
format:
|
||||
date-time: yyyy-MM-dd HH:mm:ss
|
||||
jackson:
|
||||
# 日期格式化
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
serialization:
|
||||
# 格式化输出
|
||||
indent_output: false
|
||||
# 忽略无法转换的对象
|
||||
fail_on_empty_beans: false
|
||||
deserialization:
|
||||
# 允许对象忽略json中不存在的属性
|
||||
fail_on_unknown_properties: false
|
||||
|
||||
# Sa-Token配置
|
||||
sa-token:
|
||||
# token名称 (同时也是cookie名称)
|
||||
token-name: Authorization
|
||||
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
|
||||
is-concurrent: true
|
||||
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
|
||||
is-share: false
|
||||
# jwt秘钥
|
||||
jwt-secret-key: abcdefghijklmnopqrstuvwxyz
|
||||
|
||||
# security配置
|
||||
security:
|
||||
# 排除路径
|
||||
excludes:
|
||||
- /*.html
|
||||
- /**/*.html
|
||||
- /**/*.css
|
||||
- /**/*.js
|
||||
- /favicon.ico
|
||||
- /error
|
||||
- /*/api-docs
|
||||
- /*/api-docs/**
|
||||
- /warm-flow-ui/config
|
||||
- /chat/**
|
||||
|
||||
# MyBatisPlus配置
|
||||
# https://baomidou.com/config/
|
||||
mybatis-plus:
|
||||
# 自定义配置 是否全局开启逻辑删除 关闭后 所有逻辑删除功能将失效
|
||||
enableLogicDelete: true
|
||||
# 多包名使用 例如 org.dromara.**.mapper,org.xxx.**.mapper
|
||||
mapperPackage: org.dromara.**.mapper
|
||||
# 对应的 XML 文件位置
|
||||
mapperLocations: classpath*:mapper/**/*Mapper.xml
|
||||
# 实体扫描,多个package用逗号或者分号分隔
|
||||
typeAliasesPackage: org.dromara.**.domain
|
||||
global-config:
|
||||
dbConfig:
|
||||
# 主键类型
|
||||
# AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUID
|
||||
# 如需改为自增 需要将数据库表全部设置为自增
|
||||
idType: ASSIGN_ID
|
||||
|
||||
# 数据加密
|
||||
mybatis-encryptor:
|
||||
# 是否开启加密
|
||||
enable: false
|
||||
# 默认加密算法
|
||||
algorithm: BASE64
|
||||
# 编码方式 BASE64/HEX。默认BASE64
|
||||
encode: BASE64
|
||||
# 安全秘钥 对称算法的秘钥 如:AES,SM4
|
||||
password:
|
||||
# 公私钥 非对称算法的公私钥 如:SM2,RSA
|
||||
publicKey:
|
||||
privateKey:
|
||||
|
||||
# api接口加密
|
||||
api-decrypt:
|
||||
# 是否开启全局接口加密
|
||||
enabled: true
|
||||
# AES 加密头标识
|
||||
headerFlag: encrypt-key
|
||||
# 响应加密公钥 非对称算法的公私钥 如:SM2,RSA 使用者请自行更换
|
||||
# 对应前端解密私钥 MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
|
||||
publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJnNwrj4hi/y3CCJu868ghCG5dUj8wZK++RNlTLcXoMmdZWEQ/u02RgD5LyLAXGjLOjbMtC+/J9qofpSGTKSx/MCAwEAAQ==
|
||||
# 请求解密私钥 非对称算法的公私钥 如:SM2,RSA 使用者请自行更换
|
||||
# 对应前端加密公钥 MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
|
||||
privateKey: MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKNPuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gAkM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWowcSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99EcvDQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthhYhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3UP8iWi1Qw0Y=
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
# 是否开启接口文档
|
||||
enabled: true
|
||||
info:
|
||||
# 标题
|
||||
title: '标题:RuoYi-Vue-Plus管理系统_接口文档'
|
||||
# 描述
|
||||
description: '描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...'
|
||||
# 版本
|
||||
version: '版本号: ${project.version}'
|
||||
# 作者信息
|
||||
contact:
|
||||
name: Lion Li
|
||||
email: crazylionli@163.com
|
||||
url: https://gitee.com/dromara/RuoYi-Vue-Plus
|
||||
components:
|
||||
# 鉴权方式配置
|
||||
security-schemes:
|
||||
apiKey:
|
||||
type: APIKEY
|
||||
in: HEADER
|
||||
name: ${sa-token.token-name}
|
||||
#这里定义了两个分组,可定义多个,也可以不定义
|
||||
group-configs:
|
||||
- group: 1.演示模块
|
||||
packages-to-scan: org.dromara.demo
|
||||
- group: 2.通用模块
|
||||
packages-to-scan: org.dromara.web
|
||||
- group: 3.系统模块
|
||||
packages-to-scan: org.dromara.system
|
||||
- group: 4.代码生成模块
|
||||
packages-to-scan: org.dromara.generator
|
||||
- group: 5.工作流模块
|
||||
packages-to-scan: org.dromara.workflow
|
||||
- group: 6.App端模块
|
||||
packages-to-scan: org.dromara.app
|
||||
|
||||
# 防止XSS攻击
|
||||
xss:
|
||||
# 过滤开关
|
||||
enabled: true
|
||||
# 排除链接(多个用逗号分隔)
|
||||
excludeUrls:
|
||||
- /system/notice
|
||||
|
||||
# 全局线程池相关配置
|
||||
# 如使用JDK21请直接使用虚拟线程 不要开启此配置
|
||||
thread-pool:
|
||||
# 是否开启线程池
|
||||
enabled: false
|
||||
# 队列最大长度
|
||||
queueCapacity: 128
|
||||
# 线程池维护线程所允许的空闲时间
|
||||
keepAliveSeconds: 300
|
||||
|
||||
--- # 分布式锁 lock4j 全局配置
|
||||
lock4j:
|
||||
# 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
acquire-timeout: 3000
|
||||
# 分布式锁的超时时间,默认为 30 秒
|
||||
expire: 30000
|
||||
|
||||
--- # Actuator 监控端点的配置项
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: '*'
|
||||
endpoint:
|
||||
health:
|
||||
show-details: ALWAYS
|
||||
logfile:
|
||||
external-file: ./logs/sys-console.log
|
||||
|
||||
--- # 默认/推荐使用sse推送
|
||||
sse:
|
||||
enabled: true
|
||||
path: /resource/sse
|
||||
|
||||
--- # websocket
|
||||
websocket:
|
||||
# 如果关闭 需要和前端开关一起关闭
|
||||
enabled: false
|
||||
# 路径
|
||||
path: /resource/websocket
|
||||
# 设置访问源地址
|
||||
allowedOrigins: '*'
|
||||
|
||||
--- # warm-flow工作流配置
|
||||
warm-flow:
|
||||
# 是否开启工作流,默认true
|
||||
enabled: false
|
||||
# 是否开启设计器ui
|
||||
ui: true
|
||||
# 默认Authorization,如果有多个token,用逗号分隔
|
||||
token-name: ${sa-token.token-name},clientid
|
||||
# 流程状态对应的三元色
|
||||
chart-status-color:
|
||||
## 未办理
|
||||
- 62,62,62
|
||||
## 待办理
|
||||
- 255,205,23
|
||||
## 已办理
|
||||
- 157,255,0
|
||||
@@ -0,0 +1,8 @@
|
||||
Application Version: ${revision}
|
||||
Spring Boot Version: ${spring-boot.version}
|
||||
__________ _____.___.__ ____ ____ __________.__
|
||||
\______ \__ __ ____\__ | |__| \ \ / /_ __ ____ \______ \ | __ __ ______
|
||||
| _/ | \/ _ \/ | | | ______ \ Y / | \_/ __ \ ______ | ___/ | | | \/ ___/
|
||||
| | \ | ( <_> )____ | | /_____/ \ /| | /\ ___/ /_____/ | | | |_| | /\___ \
|
||||
|____|_ /____/ \____// ______|__| \___/ |____/ \___ > |____| |____/____//____ >
|
||||
\/ \/ \/ \/
|
||||
@@ -0,0 +1,56 @@
|
||||
#错误消息
|
||||
not.null=* 必须填写
|
||||
user.jcaptcha.error=验证码错误
|
||||
user.jcaptcha.expire=验证码已失效
|
||||
user.not.exists=对不起, 您的账号:{0} 不存在.
|
||||
user.password.not.match=用户不存在/密码错误
|
||||
user.password.retry.limit.count=密码输入错误{0}次
|
||||
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
|
||||
user.password.delete=对不起,您的账号:{0} 已被删除
|
||||
user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员
|
||||
role.blocked=角色已封禁,请联系管理员
|
||||
user.logout.success=退出成功
|
||||
length.not.valid=长度必须在{min}到{max}个字符之间
|
||||
user.username.not.blank=用户名不能为空
|
||||
user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头
|
||||
user.username.length.valid=账户长度必须在{min}到{max}个字符之间
|
||||
user.password.not.blank=用户密码不能为空
|
||||
user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
|
||||
user.password.not.valid=* 5-50个字符
|
||||
user.email.not.valid=邮箱格式错误
|
||||
user.email.not.blank=邮箱不能为空
|
||||
user.phonenumber.not.blank=用户手机号不能为空
|
||||
user.mobile.phone.number.not.valid=手机号格式错误
|
||||
user.login.success=登录成功
|
||||
user.register.success=注册成功
|
||||
user.register.save.error=保存用户 {0} 失败,注册账号已存在
|
||||
user.register.error=注册失败,请联系系统管理人员
|
||||
user.notfound=请重新登录
|
||||
user.forcelogout=管理员强制退出,请重新登录
|
||||
user.unknown.error=未知错误,请重新登录
|
||||
auth.grant.type.error=认证权限类型错误
|
||||
auth.grant.type.blocked=认证权限类型已禁用
|
||||
auth.grant.type.not.blank=认证权限类型不能为空
|
||||
auth.clientid.not.blank=认证客户端id不能为空
|
||||
##文件上传消息
|
||||
upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
|
||||
upload.filename.exceed.length=上传的文件名最长{0}个字符
|
||||
##权限
|
||||
no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]
|
||||
repeat.submit.message=不允许重复提交,请稍候再试
|
||||
rate.limiter.message=访问过于频繁,请稍候再试
|
||||
sms.code.not.blank=短信验证码不能为空
|
||||
sms.code.retry.limit.count=短信验证码输入错误{0}次
|
||||
sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟
|
||||
email.code.not.blank=邮箱验证码不能为空
|
||||
email.code.retry.limit.count=邮箱验证码输入错误{0}次
|
||||
email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,帐户锁定{1}分钟
|
||||
xcx.code.not.blank=小程序[code]不能为空
|
||||
social.source.not.blank=第三方登录平台[source]不能为空
|
||||
social.code.not.blank=第三方登录平台[code]不能为空
|
||||
social.state.not.blank=第三方登录平台[state]不能为空
|
||||
@@ -0,0 +1,56 @@
|
||||
#错误消息
|
||||
not.null=* Required fill in
|
||||
user.jcaptcha.error=Captcha error
|
||||
user.jcaptcha.expire=Captcha invalid
|
||||
user.not.exists=Sorry, your account: {0} does not exist
|
||||
user.password.not.match=User does not exist/Password error
|
||||
user.password.retry.limit.count=Password input error {0} times
|
||||
user.password.retry.limit.exceed=Password input error {0} times, account locked for {1} minutes
|
||||
user.password.delete=Sorry, your account:{0} has been deleted
|
||||
user.blocked=Sorry, your account: {0} has been disabled. Please contact the administrator
|
||||
role.blocked=Role disabled,please contact administrators
|
||||
user.logout.success=Exit successful
|
||||
length.not.valid=The length must be between {min} and {max} characters
|
||||
user.username.not.blank=Username cannot be blank
|
||||
user.username.not.valid=* 2 to 20 chinese characters, letters, numbers or underscores, and must start with a non number
|
||||
user.username.length.valid=Account length must be between {min} and {max} characters
|
||||
user.password.not.blank=Password cannot be empty
|
||||
user.password.length.valid=Password length must be between {min} and {max} characters
|
||||
user.password.not.valid=* 5-50 characters
|
||||
user.email.not.valid=Mailbox format error
|
||||
user.email.not.blank=Mailbox cannot be blank
|
||||
user.phonenumber.not.blank=Phone number cannot be blank
|
||||
user.mobile.phone.number.not.valid=Phone number format error
|
||||
user.login.success=Login successful
|
||||
user.register.success=Register successful
|
||||
user.register.save.error=Failed to save user {0}, The registered account already exists
|
||||
user.register.error=Register failed, please contact system administrator
|
||||
user.notfound=Please login again
|
||||
user.forcelogout=The administrator is forced to exit,please login again
|
||||
user.unknown.error=Unknown error, please login again
|
||||
auth.grant.type.error=Auth grant type error
|
||||
auth.grant.type.blocked=Auth grant type disabled
|
||||
auth.grant.type.not.blank=Auth grant type cannot be blank
|
||||
auth.clientid.not.blank=Auth clientid cannot be blank
|
||||
##文件上传消息
|
||||
upload.exceed.maxSize=The uploaded file size exceeds the limit file size!<br/>the maximum allowed file size is:{0}MB!
|
||||
upload.filename.exceed.length=The maximum length of uploaded file name is {0} characters
|
||||
##权限
|
||||
no.permission=You do not have permission to the data,please contact your administrator to add permissions [{0}]
|
||||
no.create.permission=You do not have permission to create data,please contact your administrator to add permissions [{0}]
|
||||
no.update.permission=You do not have permission to modify data,please contact your administrator to add permissions [{0}]
|
||||
no.delete.permission=You do not have permission to delete data,please contact your administrator to add permissions [{0}]
|
||||
no.export.permission=You do not have permission to export data,please contact your administrator to add permissions [{0}]
|
||||
no.view.permission=You do not have permission to view data,please contact your administrator to add permissions [{0}]
|
||||
repeat.submit.message=Repeat submit is not allowed, please try again later
|
||||
rate.limiter.message=Visit too frequently, please try again later
|
||||
sms.code.not.blank=Sms code cannot be blank
|
||||
sms.code.retry.limit.count=Sms code input error {0} times
|
||||
sms.code.retry.limit.exceed=Sms code input error {0} times, account locked for {1} minutes
|
||||
email.code.not.blank=Email code cannot be blank
|
||||
email.code.retry.limit.count=Email code input error {0} times
|
||||
email.code.retry.limit.exceed=Email code input error {0} times, account locked for {1} minutes
|
||||
xcx.code.not.blank=Mini program [code] cannot be blank
|
||||
social.source.not.blank=Social login platform [source] cannot be blank
|
||||
social.code.not.blank=Social login platform [code] cannot be blank
|
||||
social.state.not.blank=Social login platform [state] cannot be blank
|
||||
@@ -0,0 +1,56 @@
|
||||
#错误消息
|
||||
not.null=* 必须填写
|
||||
user.jcaptcha.error=验证码错误
|
||||
user.jcaptcha.expire=验证码已失效
|
||||
user.not.exists=对不起, 您的账号:{0} 不存在.
|
||||
user.password.not.match=用户不存在/密码错误
|
||||
user.password.retry.limit.count=密码输入错误{0}次
|
||||
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
|
||||
user.password.delete=对不起,您的账号:{0} 已被删除
|
||||
user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员
|
||||
role.blocked=角色已封禁,请联系管理员
|
||||
user.logout.success=退出成功
|
||||
length.not.valid=长度必须在{min}到{max}个字符之间
|
||||
user.username.not.blank=用户名不能为空
|
||||
user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头
|
||||
user.username.length.valid=账户长度必须在{min}到{max}个字符之间
|
||||
user.password.not.blank=用户密码不能为空
|
||||
user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
|
||||
user.password.not.valid=* 5-50个字符
|
||||
user.email.not.valid=邮箱格式错误
|
||||
user.email.not.blank=邮箱不能为空
|
||||
user.phonenumber.not.blank=用户手机号不能为空
|
||||
user.mobile.phone.number.not.valid=手机号格式错误
|
||||
user.login.success=登录成功
|
||||
user.register.success=注册成功
|
||||
user.register.save.error=保存用户 {0} 失败,注册账号已存在
|
||||
user.register.error=注册失败,请联系系统管理人员
|
||||
user.notfound=请重新登录
|
||||
user.forcelogout=管理员强制退出,请重新登录
|
||||
user.unknown.error=未知错误,请重新登录
|
||||
auth.grant.type.error=认证权限类型错误
|
||||
auth.grant.type.blocked=认证权限类型已禁用
|
||||
auth.grant.type.not.blank=认证权限类型不能为空
|
||||
auth.clientid.not.blank=认证客户端id不能为空
|
||||
##文件上传消息
|
||||
upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
|
||||
upload.filename.exceed.length=上传的文件名最长{0}个字符
|
||||
##权限
|
||||
no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
|
||||
no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]
|
||||
repeat.submit.message=不允许重复提交,请稍候再试
|
||||
rate.limiter.message=访问过于频繁,请稍候再试
|
||||
sms.code.not.blank=短信验证码不能为空
|
||||
sms.code.retry.limit.count=短信验证码输入错误{0}次
|
||||
sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟
|
||||
email.code.not.blank=邮箱验证码不能为空
|
||||
email.code.retry.limit.count=邮箱验证码输入错误{0}次
|
||||
email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,帐户锁定{1}分钟
|
||||
xcx.code.not.blank=小程序[code]不能为空
|
||||
social.source.not.blank=第三方登录平台[source]不能为空
|
||||
social.code.not.blank=第三方登录平台[code]不能为空
|
||||
social.state.not.blank=第三方登录平台[state]不能为空
|
||||
Binary file not shown.
@@ -0,0 +1,129 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<property name="log.path" value="./logs"/>
|
||||
<property name="console.log.pattern"
|
||||
value="%cyan(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/>
|
||||
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"/>
|
||||
|
||||
<!-- 控制台输出 -->
|
||||
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>${console.log.pattern}</pattern>
|
||||
<charset>utf-8</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 控制台输出 -->
|
||||
<appender name="file_console" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${log.path}/sys-console.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- 日志文件名格式 -->
|
||||
<fileNamePattern>${log.path}/sys-console.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<!-- 日志最大 1天 -->
|
||||
<maxHistory>1</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>${log.pattern}</pattern>
|
||||
<charset>utf-8</charset>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<!-- 过滤的级别 -->
|
||||
<level>INFO</level>
|
||||
</filter>
|
||||
</appender>
|
||||
|
||||
<!-- 系统日志输出 -->
|
||||
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${log.path}/sys-info.log</file>
|
||||
<!-- 循环政策:基于时间创建日志文件 -->
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- 日志文件名格式 -->
|
||||
<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<!-- 日志最大的历史 60天 -->
|
||||
<maxHistory>60</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>${log.pattern}</pattern>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<!-- 过滤的级别 -->
|
||||
<level>INFO</level>
|
||||
<!-- 匹配时的操作:接收(记录) -->
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<!-- 不匹配时的操作:拒绝(不记录) -->
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
</appender>
|
||||
|
||||
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${log.path}/sys-error.log</file>
|
||||
<!-- 循环政策:基于时间创建日志文件 -->
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- 日志文件名格式 -->
|
||||
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<!-- 日志最大的历史 60天 -->
|
||||
<maxHistory>60</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>${log.pattern}</pattern>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<!-- 过滤的级别 -->
|
||||
<level>ERROR</level>
|
||||
<!-- 匹配时的操作:接收(记录) -->
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<!-- 不匹配时的操作:拒绝(不记录) -->
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
</appender>
|
||||
|
||||
<!-- info异步输出 -->
|
||||
<appender name="async_info" class="ch.qos.logback.classic.AsyncAppender">
|
||||
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
|
||||
<discardingThreshold>0</discardingThreshold>
|
||||
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
|
||||
<queueSize>512</queueSize>
|
||||
<!-- 添加附加的appender,最多只能添加一个 -->
|
||||
<appender-ref ref="file_info"/>
|
||||
</appender>
|
||||
|
||||
<!-- error异步输出 -->
|
||||
<appender name="async_error" class="ch.qos.logback.classic.AsyncAppender">
|
||||
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
|
||||
<discardingThreshold>0</discardingThreshold>
|
||||
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
|
||||
<queueSize>512</queueSize>
|
||||
<!-- 添加附加的appender,最多只能添加一个 -->
|
||||
<appender-ref ref="file_error"/>
|
||||
</appender>
|
||||
|
||||
<!-- 整合 skywalking 控制台输出 tid -->
|
||||
<!-- <appender name="console" class="ch.qos.logback.core.ConsoleAppender">-->
|
||||
<!-- <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">-->
|
||||
<!-- <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">-->
|
||||
<!-- <pattern>[%tid] ${console.log.pattern}</pattern>-->
|
||||
<!-- </layout>-->
|
||||
<!-- <charset>utf-8</charset>-->
|
||||
<!-- </encoder>-->
|
||||
<!-- </appender>-->
|
||||
|
||||
<!-- 整合 skywalking 推送采集日志 -->
|
||||
<!-- <appender name="sky_log" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">-->
|
||||
<!-- <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">-->
|
||||
<!-- <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">-->
|
||||
<!-- <pattern>[%tid] ${console.log.pattern}</pattern>-->
|
||||
<!-- </layout>-->
|
||||
<!-- <charset>utf-8</charset>-->
|
||||
<!-- </encoder>-->
|
||||
<!-- </appender>-->
|
||||
|
||||
<!--系统操作日志-->
|
||||
<root level="info">
|
||||
<appender-ref ref="console"/>
|
||||
<appender-ref ref="async_info"/>
|
||||
<appender-ref ref="async_error"/>
|
||||
<appender-ref ref="file_console"/>
|
||||
<!-- <appender-ref ref="sky_log"/>-->
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.dromara.app.session;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class AppSessionManagerTest {
|
||||
@Test
|
||||
void placeholder() {
|
||||
assertTrue(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.dromara.test;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* 断言单元测试案例
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@DisplayName("断言单元测试案例")
|
||||
public class AssertUnitTest {
|
||||
|
||||
@DisplayName("测试 assertEquals 方法")
|
||||
@Test
|
||||
public void testAssertEquals() {
|
||||
Assertions.assertEquals("666", new String("666"));
|
||||
Assertions.assertNotEquals("666", new String("666"));
|
||||
}
|
||||
|
||||
@DisplayName("测试 assertSame 方法")
|
||||
@Test
|
||||
public void testAssertSame() {
|
||||
Object obj = new Object();
|
||||
Object obj1 = obj;
|
||||
Assertions.assertSame(obj, obj1);
|
||||
Assertions.assertNotSame(obj, obj1);
|
||||
}
|
||||
|
||||
@DisplayName("测试 assertTrue 方法")
|
||||
@Test
|
||||
public void testAssertTrue() {
|
||||
Assertions.assertTrue(true);
|
||||
Assertions.assertFalse(true);
|
||||
}
|
||||
|
||||
@DisplayName("测试 assertNull 方法")
|
||||
@Test
|
||||
public void testAssertNull() {
|
||||
Assertions.assertNull(null);
|
||||
Assertions.assertNotNull(null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.dromara.test;
|
||||
|
||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 单元测试案例
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@SpringBootTest // 此注解只能在 springboot 主包下使用 需包含 main 方法与 yml 配置文件
|
||||
@DisplayName("单元测试案例")
|
||||
public class DemoUnitTest {
|
||||
|
||||
@Autowired
|
||||
private CaptchaProperties captchaProperties;
|
||||
|
||||
@DisplayName("测试 @SpringBootTest @Test @DisplayName 注解")
|
||||
@Test
|
||||
public void testTest() {
|
||||
System.out.println(captchaProperties);
|
||||
}
|
||||
|
||||
@Disabled
|
||||
@DisplayName("测试 @Disabled 注解")
|
||||
@Test
|
||||
public void testDisabled() {
|
||||
System.out.println(captchaProperties);
|
||||
}
|
||||
|
||||
@Timeout(value = 2L, unit = TimeUnit.SECONDS)
|
||||
@DisplayName("测试 @Timeout 注解")
|
||||
@Test
|
||||
public void testTimeout() throws InterruptedException {
|
||||
Thread.sleep(3000);
|
||||
System.out.println(captchaProperties);
|
||||
}
|
||||
|
||||
|
||||
@DisplayName("测试 @RepeatedTest 注解")
|
||||
@RepeatedTest(3)
|
||||
public void testRepeatedTest() {
|
||||
System.out.println(666);
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
public static void testBeforeAll() {
|
||||
System.out.println("@BeforeAll ==================");
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void testBeforeEach() {
|
||||
System.out.println("@BeforeEach ==================");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void testAfterEach() {
|
||||
System.out.println("@AfterEach ==================");
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void testAfterAll() {
|
||||
System.out.println("@AfterAll ==================");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.dromara.test;
|
||||
|
||||
import org.dromara.common.core.enums.UserType;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.NullSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 带参数单元测试案例
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@DisplayName("带参数单元测试案例")
|
||||
public class ParamUnitTest {
|
||||
|
||||
@DisplayName("测试 @ValueSource 注解")
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"t1", "t2", "t3"})
|
||||
public void testValueSource(String str) {
|
||||
System.out.println(str);
|
||||
}
|
||||
|
||||
@DisplayName("测试 @NullSource 注解")
|
||||
@ParameterizedTest
|
||||
@NullSource
|
||||
public void testNullSource(String str) {
|
||||
System.out.println(str);
|
||||
}
|
||||
|
||||
@DisplayName("测试 @EnumSource 注解")
|
||||
@ParameterizedTest
|
||||
@EnumSource(UserType.class)
|
||||
public void testEnumSource(UserType type) {
|
||||
System.out.println(type.getUserType());
|
||||
}
|
||||
|
||||
@DisplayName("测试 @MethodSource 注解")
|
||||
@ParameterizedTest
|
||||
@MethodSource("getParam")
|
||||
public void testMethodSource(String str) {
|
||||
System.out.println(str);
|
||||
}
|
||||
|
||||
public static Stream<String> getParam() {
|
||||
List<String> list = new ArrayList<>();
|
||||
list.add("t1");
|
||||
list.add("t2");
|
||||
list.add("t3");
|
||||
return list.stream();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void testBeforeEach() {
|
||||
System.out.println("@BeforeEach ==================");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void testAfterEach() {
|
||||
System.out.println("@AfterEach ==================");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.dromara.test;
|
||||
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
* 标签单元测试案例
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@SpringBootTest
|
||||
@DisplayName("标签单元测试案例")
|
||||
public class TagUnitTest {
|
||||
|
||||
@Tag("dev")
|
||||
@DisplayName("测试 @Tag dev")
|
||||
@Test
|
||||
public void testTagDev() {
|
||||
System.out.println("dev");
|
||||
}
|
||||
|
||||
@Tag("prod")
|
||||
@DisplayName("测试 @Tag prod")
|
||||
@Test
|
||||
public void testTagProd() {
|
||||
System.out.println("prod");
|
||||
}
|
||||
|
||||
@Tag("local")
|
||||
@DisplayName("测试 @Tag local")
|
||||
@Test
|
||||
public void testTagLocal() {
|
||||
System.out.println("local");
|
||||
}
|
||||
|
||||
@Tag("exclude")
|
||||
@DisplayName("测试 @Tag exclude")
|
||||
@Test
|
||||
public void testTagExclude() {
|
||||
System.out.println("exclude");
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void testBeforeEach() {
|
||||
System.out.println("@BeforeEach ==================");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void testAfterEach() {
|
||||
System.out.println("@AfterEach ==================");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user