Compare commits
10 Commits
a2e3eda278
...
182a16efca
| Author | SHA1 | Date | |
|---|---|---|---|
| 182a16efca | |||
| 430478c040 | |||
| 70deffdf05 | |||
| fba7b40075 | |||
| 6d8c0d6d47 | |||
| d31082f4a7 | |||
| c76212003c | |||
| 41ea764a8c | |||
| 7180b52977 | |||
| 070d0bdd45 |
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<!-- 项目基础配置 -->
|
<!-- 项目基础配置 -->
|
||||||
<revision>6.0.0-BETA</revision>
|
<revision>6.0.0</revision>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
<java.version>21</java.version>
|
<java.version>21</java.version>
|
||||||
@@ -36,14 +36,14 @@
|
|||||||
|
|
||||||
<!-- 安全认证与加密相关依赖版本 -->
|
<!-- 安全认证与加密相关依赖版本 -->
|
||||||
<satoken.version>1.45.0</satoken.version>
|
<satoken.version>1.45.0</satoken.version>
|
||||||
<justauth.version>2.0.0</justauth.version>
|
<justauth.version>3.0.1</justauth.version>
|
||||||
<bouncycastle.version>1.85</bouncycastle.version>
|
<bouncycastle.version>1.85</bouncycastle.version>
|
||||||
|
|
||||||
<!-- 缓存、锁与任务调度相关依赖版本 -->
|
<!-- 缓存、锁与任务调度相关依赖版本 -->
|
||||||
<redisson.version>4.6.1</redisson.version>
|
<redisson.version>4.6.1</redisson.version>
|
||||||
<lock4j.version>2.2.7</lock4j.version>
|
<lock4j.version>2.2.7</lock4j.version>
|
||||||
<snailjob.version>2.0.2</snailjob.version>
|
<snailjob.version>2.0.2</snailjob.version>
|
||||||
<snailai.version>1.1.0</snailai.version>
|
<snailai.version>1.1.1</snailai.version>
|
||||||
|
|
||||||
<!-- 通用工具、编译增强与代码生成相关依赖版本 -->
|
<!-- 通用工具、编译增强与代码生成相关依赖版本 -->
|
||||||
<hutool.version>5.8.47</hutool.version>
|
<hutool.version>5.8.47</hutool.version>
|
||||||
@@ -55,6 +55,8 @@
|
|||||||
<fory.version>1.3.0</fory.version>
|
<fory.version>1.3.0</fory.version>
|
||||||
|
|
||||||
<!-- 三方集成与业务扩展相关依赖版本 -->
|
<!-- 三方集成与业务扩展相关依赖版本 -->
|
||||||
|
<!-- tianai验证码 -->
|
||||||
|
<tianai-captcha.version>1.5.5</tianai-captcha.version>
|
||||||
<!-- 离线IP地址定位库 -->
|
<!-- 离线IP地址定位库 -->
|
||||||
<ip2region.version>3.3.7</ip2region.version>
|
<ip2region.version>3.3.7</ip2region.version>
|
||||||
<!-- OSS 配置 -->
|
<!-- OSS 配置 -->
|
||||||
@@ -396,7 +398,7 @@
|
|||||||
|
|
||||||
<!-- JustAuth 的依赖配置-->
|
<!-- JustAuth 的依赖配置-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.herodotus.opensteward</groupId>
|
<groupId>io.github.windtool</groupId>
|
||||||
<artifactId>JustAuth</artifactId>
|
<artifactId>JustAuth</artifactId>
|
||||||
<version>${justauth.version}</version>
|
<version>${justauth.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
@@ -408,6 +410,13 @@
|
|||||||
<version>${ip2region.version}</version>
|
<version>${ip2region.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 行为验证码 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>cloud.tianai.captcha</groupId>
|
||||||
|
<artifactId>tianai-captcha-springboot-starter</artifactId>
|
||||||
|
<version>${tianai-captcha.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- 系统模块 -->
|
<!-- 系统模块 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.dromara</groupId>
|
<groupId>org.dromara</groupId>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
|
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
|
||||||
FROM bellsoft/liberica-openjdk-rocky:21.0.11-cds
|
FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds
|
||||||
# FROM bellsoft/liberica-openjdk-rocky:25.0.3-cds
|
# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds
|
||||||
# FROM findepi/graalvm:java21-native
|
# FROM findepi/graalvm:java21-native
|
||||||
|
|
||||||
LABEL maintainer="Lion Li"
|
LABEL maintainer="Lion Li"
|
||||||
|
|||||||
@@ -117,6 +117,12 @@
|
|||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 验证码模块 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.dromara</groupId>
|
||||||
|
<artifactId>ruoyi-common-captcha</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- skywalking 整合 logback -->
|
<!-- skywalking 整合 logback -->
|
||||||
<!-- <dependency>-->
|
<!-- <dependency>-->
|
||||||
<!-- <groupId>org.apache.skywalking</groupId>-->
|
<!-- <groupId>org.apache.skywalking</groupId>-->
|
||||||
|
|||||||
@@ -1,42 +1,25 @@
|
|||||||
package org.dromara.web.controller;
|
package org.dromara.web.controller;
|
||||||
|
|
||||||
|
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
|
||||||
|
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||||
|
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||||
import cn.dev33.satoken.annotation.SaIgnore;
|
import cn.dev33.satoken.annotation.SaIgnore;
|
||||||
import cn.hutool.captcha.generator.CodeGenerator;
|
import org.dromara.common.captcha.utils.CaptchaUtils;
|
||||||
import cn.hutool.captcha.generator.MathGenerator;
|
import org.dromara.common.captcha.utils.CaptchaUtils.CaptchaVo;
|
||||||
import cn.hutool.captcha.generator.RandomGenerator;
|
|
||||||
import cn.hutool.core.util.IdUtil;
|
|
||||||
import cn.hutool.core.util.RandomUtil;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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.domain.R;
|
||||||
import org.dromara.common.core.exception.ServiceException;
|
|
||||||
import org.dromara.common.core.utils.SpringUtils;
|
import org.dromara.common.core.utils.SpringUtils;
|
||||||
import org.dromara.common.core.utils.StringUtils;
|
|
||||||
import org.dromara.common.core.utils.regex.RegexValidator;
|
import org.dromara.common.core.utils.regex.RegexValidator;
|
||||||
import org.dromara.common.mail.config.properties.MailProperties;
|
import org.dromara.common.mail.config.properties.MailProperties;
|
||||||
import org.dromara.common.mail.core.MailBuilder;
|
|
||||||
import org.dromara.common.redis.annotation.RateLimiter;
|
import org.dromara.common.redis.annotation.RateLimiter;
|
||||||
import org.dromara.common.redis.enums.LimitType;
|
|
||||||
import org.dromara.common.redis.utils.RedisUtils;
|
|
||||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
|
||||||
import org.dromara.common.web.core.WaveAndCircleCaptcha;
|
|
||||||
import org.dromara.sms4j.api.SmsBlend;
|
|
||||||
import org.dromara.sms4j.api.entity.SmsResponse;
|
|
||||||
import org.dromara.sms4j.core.factory.SmsFactory;
|
|
||||||
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.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.awt.*;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证码操作处理
|
* 验证码操作处理
|
||||||
*
|
*
|
||||||
@@ -47,9 +30,10 @@ import java.util.LinkedHashMap;
|
|||||||
@Validated
|
@Validated
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@RestController
|
@RestController
|
||||||
|
@RequestMapping("/resource/captcha")
|
||||||
public class CaptchaController {
|
public class CaptchaController {
|
||||||
|
|
||||||
private final CaptchaProperties captchaProperties;
|
private final CaptchaUtils captchaUtils;
|
||||||
private final MailProperties mailProperties;
|
private final MailProperties mailProperties;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,25 +43,9 @@ public class CaptchaController {
|
|||||||
* @return 操作结果
|
* @return 操作结果
|
||||||
*/
|
*/
|
||||||
@RateLimiter(key = "#phoneNumber", time = 60, count = 1)
|
@RateLimiter(key = "#phoneNumber", time = 60, count = 1)
|
||||||
@GetMapping("/resource/sms/code")
|
@GetMapping("/sms")
|
||||||
public R<Void> smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phoneNumber) {
|
public R<Void> sendSmsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phoneNumber) {
|
||||||
if (!RegexValidator.isMobile(phoneNumber)) {
|
captchaUtils.generateSmsCaptcha(phoneNumber);
|
||||||
return R.fail("请输入正确的手机号!");
|
|
||||||
}
|
|
||||||
String key = GlobalConstants.CAPTCHA_CODE_KEY + phoneNumber;
|
|
||||||
String code = RandomUtil.randomNumbers(4);
|
|
||||||
// 验证码模板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);
|
|
||||||
Object data = smsResponse.getData();
|
|
||||||
return R.fail(data == null ? "验证码短信发送失败" : data.toString());
|
|
||||||
}
|
|
||||||
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
|
||||||
return R.ok();
|
return R.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,96 +55,60 @@ public class CaptchaController {
|
|||||||
* @param email 邮箱
|
* @param email 邮箱
|
||||||
* @return 操作结果
|
* @return 操作结果
|
||||||
*/
|
*/
|
||||||
@GetMapping("/resource/email/code")
|
@GetMapping("/email")
|
||||||
public R<Void> emailCode(@NotBlank(message = "{user.email.not.blank}") String email) {
|
public R<Void> sendEmailCode(@NotBlank(message = "{user.email.not.blank}") String email) {
|
||||||
if (!mailProperties.getEnabled()) {
|
if (!mailProperties.getEnabled()) {
|
||||||
return R.fail("当前系统没有开启邮箱功能!");
|
return R.fail("当前系统没有开启邮箱功能!");
|
||||||
}
|
}
|
||||||
if (!RegexValidator.isEmail(email)) {
|
if (!RegexValidator.isEmail(email)) {
|
||||||
return R.fail("请输入正确的邮箱地址!");
|
return R.fail("请输入正确的邮箱地址!");
|
||||||
}
|
}
|
||||||
SpringUtils.getAopProxy(this).emailCodeImpl(email);
|
SpringUtils.getAopProxy(this).captchaUtils.generateEmailCaptcha(email);
|
||||||
return R.ok();
|
return R.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送邮箱验证码的实际执行方法,拆分出来避免开关关闭时仍触发限流。
|
|
||||||
*
|
|
||||||
* @param email 邮箱
|
|
||||||
*/
|
|
||||||
@RateLimiter(key = "#email", time = 60, count = 1)
|
|
||||||
public void emailCodeImpl(String email) {
|
|
||||||
String key = GlobalConstants.CAPTCHA_CODE_KEY + email;
|
|
||||||
String code = RandomUtil.randomNumbers(4);
|
|
||||||
try {
|
|
||||||
MailBuilder.of()
|
|
||||||
.to(email)
|
|
||||||
.subject("登录验证码")
|
|
||||||
.text("您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。")
|
|
||||||
.send();
|
|
||||||
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("验证码短信发送异常 => {}", e.getMessage());
|
|
||||||
throw new ServiceException(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取图片验证码。
|
* 获取图片验证码。
|
||||||
*
|
*
|
||||||
* @return 验证码信息
|
* @return 验证码信息
|
||||||
*/
|
*/
|
||||||
@GetMapping("/auth/code")
|
@GetMapping("/image")
|
||||||
public R<CaptchaVo> getCode() {
|
public R<CaptchaVo> getImageCaptcha() {
|
||||||
boolean captchaEnabled = captchaProperties.getEnable();
|
return R.ok(captchaUtils.generateImageCaptcha());
|
||||||
if (!captchaEnabled) {
|
|
||||||
return R.ok(new CaptchaVo(false, null, null));
|
|
||||||
}
|
|
||||||
return R.ok(SpringUtils.getAopProxy(this).getCodeImpl());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 实际生成图片验证码并缓存结果。
|
* 获取【行为滑块验证码】生成接口
|
||||||
|
* <p>返回滑块背景图、缺口图、验证码key</p>
|
||||||
*
|
*
|
||||||
* @return 验证码信息
|
* @return 行为验证码vo
|
||||||
*/
|
*/
|
||||||
@RateLimiter(time = 60, count = 10, limitType = LimitType.IP)
|
@RequestMapping("/behavior/generate")
|
||||||
public CaptchaVo getCodeImpl() {
|
public ApiResponse<ImageCaptchaVO> generateBehaviorCaptcha() {
|
||||||
// 保存验证码信息
|
return captchaUtils.generateBehaviorCaptchaTicket();
|
||||||
String uuid = IdUtil.simpleUUID();
|
|
||||||
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
|
|
||||||
// 生成验证码
|
|
||||||
String captchaType = captchaProperties.getType();
|
|
||||||
CodeGenerator codeGenerator;
|
|
||||||
if ("math".equals(captchaType)) {
|
|
||||||
codeGenerator = new MathGenerator(captchaProperties.getNumberLength(), false);
|
|
||||||
} else {
|
|
||||||
codeGenerator = new RandomGenerator(captchaProperties.getCharLength());
|
|
||||||
}
|
|
||||||
WaveAndCircleCaptcha captcha = new WaveAndCircleCaptcha(160, 60);
|
|
||||||
// captcha.setBackground(Color.WHITE); // 不设置就是透明底
|
|
||||||
captcha.setFont(new Font("Arial", Font.BOLD, 45));
|
|
||||||
captcha.setGenerator(codeGenerator);
|
|
||||||
captcha.createCode();
|
|
||||||
// 如果是数学验证码,使用SpEL表达式处理验证码结果
|
|
||||||
String code = captcha.getCode();
|
|
||||||
if ("math".equals(captchaType)) {
|
|
||||||
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));
|
|
||||||
return new CaptchaVo(true, uuid, captcha.getImageBase64());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 图片验证码响应对象。
|
* 校验【行为滑块验证码】
|
||||||
|
* POST语义更合适,提交校验参数
|
||||||
*
|
*
|
||||||
* @param captchaEnabled 是否启用验证码
|
* @param captchaKey 验证码缓存key
|
||||||
* @param uuid 验证码标识
|
* @param slideX 用户滑动偏移x坐标
|
||||||
* @param img Base64 图片数据
|
* @return 校验结果
|
||||||
*/
|
*/
|
||||||
public record CaptchaVo(Boolean captchaEnabled, String uuid, String img) {
|
@PostMapping("/behavior/verify")
|
||||||
|
public ApiResponse<?> verifyBehaviorCaptcha(behaviorData data) {
|
||||||
|
return captchaUtils.verifyBehaviorCaptchaFirstStage(data.id(), data.data());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 行为验证码验证接口传回BO
|
||||||
|
*
|
||||||
|
* @param id 验证码唯一ID,请求登录接口时携带该标识,后端根据ID读取缓存验证码做校验
|
||||||
|
* @param data 验证码业务数据,包含Base64图片、滑块滑动轨迹信息等,详见 {@link ImageCaptchaTrack}
|
||||||
|
*/
|
||||||
|
public record behaviorData(String id, ImageCaptchaTrack data) {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import cn.hutool.core.collection.CollUtil;
|
|||||||
import cn.hutool.core.lang.Opt;
|
import cn.hutool.core.lang.Opt;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import com.baomidou.lock.annotation.Lock4j;
|
import com.baomidou.lock.annotation.Lock4j;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import me.zhyd.oauth.model.AuthUser;
|
import me.zhyd.oauth.model.AuthUser;
|
||||||
@@ -136,7 +137,12 @@ public class SysLoginService {
|
|||||||
loginInfoEvent.setUsername(username);
|
loginInfoEvent.setUsername(username);
|
||||||
loginInfoEvent.setStatus(status);
|
loginInfoEvent.setStatus(status);
|
||||||
loginInfoEvent.setMessage(message);
|
loginInfoEvent.setMessage(message);
|
||||||
loginInfoEvent.setRequest(ServletUtils.getRequest());
|
HttpServletRequest request = ServletUtils.getRequest();
|
||||||
|
if (request != null) {
|
||||||
|
loginInfoEvent.setIp(ServletUtils.getClientIP(request));
|
||||||
|
loginInfoEvent.setUserAgent(request.getHeader("User-Agent"));
|
||||||
|
loginInfoEvent.setClientId(request.getHeader(LoginHelper.CLIENT_KEY));
|
||||||
|
}
|
||||||
SpringUtils.context().publishEvent(loginInfoEvent);
|
SpringUtils.context().publishEvent(loginInfoEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.dromara.web.service;
|
package org.dromara.web.service;
|
||||||
|
|
||||||
import cn.hutool.crypto.digest.BCrypt;
|
import cn.hutool.crypto.digest.BCrypt;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.dromara.common.core.constant.Constants;
|
import org.dromara.common.core.constant.Constants;
|
||||||
import org.dromara.common.core.constant.GlobalConstants;
|
import org.dromara.common.core.constant.GlobalConstants;
|
||||||
@@ -14,7 +15,8 @@ import org.dromara.common.core.utils.SpringUtils;
|
|||||||
import org.dromara.common.core.utils.StringUtils;
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
import org.dromara.common.log.event.LoginInfoEvent;
|
import org.dromara.common.log.event.LoginInfoEvent;
|
||||||
import org.dromara.common.redis.utils.RedisUtils;
|
import org.dromara.common.redis.utils.RedisUtils;
|
||||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
import org.dromara.common.satoken.utils.LoginHelper;
|
||||||
|
import org.dromara.common.captcha.config.properties.CaptchaProperties;
|
||||||
import org.dromara.system.api.model.RegisterBody;
|
import org.dromara.system.api.model.RegisterBody;
|
||||||
import org.dromara.system.domain.SysUser;
|
import org.dromara.system.domain.SysUser;
|
||||||
import org.dromara.system.domain.bo.SysUserBo;
|
import org.dromara.system.domain.bo.SysUserBo;
|
||||||
@@ -103,7 +105,12 @@ public class SysRegisterService {
|
|||||||
loginInfoEvent.setUsername(username);
|
loginInfoEvent.setUsername(username);
|
||||||
loginInfoEvent.setStatus(status);
|
loginInfoEvent.setStatus(status);
|
||||||
loginInfoEvent.setMessage(message);
|
loginInfoEvent.setMessage(message);
|
||||||
loginInfoEvent.setRequest(ServletUtils.getRequest());
|
HttpServletRequest request = ServletUtils.getRequest();
|
||||||
|
if (request != null) {
|
||||||
|
loginInfoEvent.setIp(ServletUtils.getClientIP(request));
|
||||||
|
loginInfoEvent.setUserAgent(request.getHeader("User-Agent"));
|
||||||
|
loginInfoEvent.setClientId(request.getHeader(LoginHelper.CLIENT_KEY));
|
||||||
|
}
|
||||||
SpringUtils.context().publishEvent(loginInfoEvent);
|
SpringUtils.context().publishEvent(loginInfoEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import org.dromara.common.core.utils.ValidatorUtils;
|
|||||||
import org.dromara.common.json.utils.JsonUtils;
|
import org.dromara.common.json.utils.JsonUtils;
|
||||||
import org.dromara.common.redis.utils.RedisUtils;
|
import org.dromara.common.redis.utils.RedisUtils;
|
||||||
import org.dromara.common.satoken.utils.LoginHelper;
|
import org.dromara.common.satoken.utils.LoginHelper;
|
||||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
import org.dromara.common.captcha.config.properties.CaptchaProperties;
|
||||||
import org.dromara.system.api.model.LoginUser;
|
import org.dromara.system.api.model.LoginUser;
|
||||||
import org.dromara.system.api.model.PasswordLoginBody;
|
import org.dromara.system.api.model.PasswordLoginBody;
|
||||||
import org.dromara.system.domain.SysUser;
|
import org.dromara.system.domain.SysUser;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ snail-ai:
|
|||||||
app-id: 1
|
app-id: 1
|
||||||
# 认证令牌(在 Server「应用管理」页面创建时自动生成)
|
# 认证令牌(在 Server「应用管理」页面创建时自动生成)
|
||||||
token: SAI_566a6bfbc26e4998b4841cc927d50c5d
|
token: SAI_566a6bfbc26e4998b4841cc927d50c5d
|
||||||
port: 18889
|
port: 3${server.port}
|
||||||
# Skill 文件临时目录
|
# Skill 文件临时目录
|
||||||
skill-temp-dir: /tmp/snail-ai-agent/skills
|
skill-temp-dir: /tmp/snail-ai-agent/skills
|
||||||
# ==================== Chat 嵌入模式配置 ====================
|
# ==================== Chat 嵌入模式配置 ====================
|
||||||
@@ -155,7 +155,7 @@ spring.data:
|
|||||||
# 数据库索引
|
# 数据库索引
|
||||||
database: 0
|
database: 0
|
||||||
# redis 密码必须配置
|
# redis 密码必须配置
|
||||||
password: ruoyi123
|
# password: ruoyi123
|
||||||
# 连接超时时间
|
# 连接超时时间
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
# 是否开启ssl
|
# 是否开启ssl
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ snail-ai:
|
|||||||
app-id: 1
|
app-id: 1
|
||||||
# 认证令牌(在 Server「应用管理」页面创建时自动生成)
|
# 认证令牌(在 Server「应用管理」页面创建时自动生成)
|
||||||
token: SAI_566a6bfbc26e4998b4841cc927d50c5d
|
token: SAI_566a6bfbc26e4998b4841cc927d50c5d
|
||||||
port: 18889
|
port: 3${server.port}
|
||||||
# Skill 文件临时目录
|
# Skill 文件临时目录
|
||||||
skill-temp-dir: /tmp/snail-ai-agent/skills
|
skill-temp-dir: /tmp/snail-ai-agent/skills
|
||||||
# ==================== Chat 嵌入模式配置 ====================
|
# ==================== Chat 嵌入模式配置 ====================
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ server:
|
|||||||
# 最大线程数
|
# 最大线程数
|
||||||
max: 256
|
max: 256
|
||||||
|
|
||||||
captcha:
|
image-captcha:
|
||||||
# 是否启用验证码校验
|
# 是否启用验证码校验
|
||||||
enable: true
|
enable: true
|
||||||
# 验证码类型 math 数组计算 char 字符验证
|
# 验证码类型 math 数组计算 char 字符验证
|
||||||
@@ -25,6 +25,39 @@ captcha:
|
|||||||
# 字符验证码长度
|
# 字符验证码长度
|
||||||
charLength: 4
|
charLength: 4
|
||||||
|
|
||||||
|
# 行为验证码配置, 详细请看 cloud.tianai.captcha.autoconfiguration.ImageCaptchaProperties 类
|
||||||
|
captcha:
|
||||||
|
# 如果项目中使用到了redis,滑块验证码会自动把验证码数据存到redis中, 这里配置redis的key的前缀,默认是captcha:slider
|
||||||
|
prefix: captcha
|
||||||
|
# 验证码过期时间,默认是2分钟,单位毫秒, 可以根据自身业务进行调整
|
||||||
|
expire:
|
||||||
|
# 默认缓存时间 2分钟
|
||||||
|
default: 10000
|
||||||
|
# 针对 点选验证码 过期时间设置为 2分钟, 因为点选验证码验证比较慢,把过期时间调整大一些
|
||||||
|
WORD_IMAGE_CLICK: 20000
|
||||||
|
# 使用加载系统自带的资源, 默认是 false(这里系统的默认资源包含 滑动验证码模板/旋转验证码模板,如果想使用系统的模板,这里设置为true)
|
||||||
|
init-default-resource: true
|
||||||
|
# 缓存控制, 默认为false不开启
|
||||||
|
local-cache-enabled: false
|
||||||
|
# 缓存开启后,验证码会提前缓存一些生成好的验证数据, 默认是20
|
||||||
|
local-cache-size: 20
|
||||||
|
# 缓存开启后,缓存拉取失败后等待时间 默认是 5秒钟
|
||||||
|
local-cache-wait-time: 5000
|
||||||
|
# 缓存开启后,缓存检查间隔 默认是2秒钟
|
||||||
|
local-cache-period: 2000
|
||||||
|
# 缓存开启后,忽略的字段,默认是 ""(不忽略任何字段)
|
||||||
|
local-cache-ignored-cache-fields: ""
|
||||||
|
# 配置字体包,供文字点选验证码使用,可以配置多个,不配置使用默认的字体
|
||||||
|
font-path:
|
||||||
|
- classpath:font/SimHei.ttf
|
||||||
|
secondary:
|
||||||
|
# 二次验证, 默认false 不开启
|
||||||
|
enabled: true
|
||||||
|
# 二次验证过期时间, 默认 2分钟
|
||||||
|
expire: 240000
|
||||||
|
# 二次验证缓存key前缀,默认是 captcha:secondary
|
||||||
|
keyPrefix: "captcha:secondary"
|
||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
|
|||||||
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 142 KiB |
@@ -18,6 +18,7 @@
|
|||||||
<modules>
|
<modules>
|
||||||
<module>ruoyi-common-bom</module>
|
<module>ruoyi-common-bom</module>
|
||||||
<module>ruoyi-common-social</module>
|
<module>ruoyi-common-social</module>
|
||||||
|
<module>ruoyi-common-captcha</module>
|
||||||
<module>ruoyi-common-core</module>
|
<module>ruoyi-common-core</module>
|
||||||
<module>ruoyi-common-doc</module>
|
<module>ruoyi-common-doc</module>
|
||||||
<module>ruoyi-common-excel</module>
|
<module>ruoyi-common-excel</module>
|
||||||
|
|||||||
@@ -14,11 +14,18 @@
|
|||||||
</description>
|
</description>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>6.0.0-BETA</revision>
|
<revision>6.0.0</revision>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<!-- 验证码模块 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.dromara</groupId>
|
||||||
|
<artifactId>ruoyi-common-captcha</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- 核心模块 -->
|
<!-- 核心模块 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.dromara</groupId>
|
<groupId>org.dromara</groupId>
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?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>ruoyi-common</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>ruoyi-common-captcha</artifactId>
|
||||||
|
|
||||||
|
<description>
|
||||||
|
ruoyi-common-code 验证码模块
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.dromara</groupId>
|
||||||
|
<artifactId>ruoyi-common-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.dromara</groupId>
|
||||||
|
<artifactId>ruoyi-common-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.dromara</groupId>
|
||||||
|
<artifactId>ruoyi-common-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.dromara</groupId>
|
||||||
|
<artifactId>ruoyi-common-mail</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cloud.tianai.captcha</groupId>
|
||||||
|
<artifactId>tianai-captcha-springboot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.dromara.sms4j</groupId>
|
||||||
|
<artifactId>sms4j-api</artifactId>
|
||||||
|
<version>3.3.5</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.dromara.sms4j</groupId>
|
||||||
|
<artifactId>sms4j-core</artifactId>
|
||||||
|
<version>3.3.5</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.annotation</groupId>
|
||||||
|
<artifactId>javax.annotation-api</artifactId>
|
||||||
|
<version>1.3.2</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package org.dromara.common.captcha.config;
|
||||||
|
|
||||||
|
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||||
|
import cloud.tianai.captcha.resource.ResourceStore;
|
||||||
|
import cloud.tianai.captcha.resource.common.model.dto.Resource;
|
||||||
|
import cloud.tianai.captcha.resource.impl.LocalMemoryResourceStore;
|
||||||
|
import org.dromara.common.captcha.config.properties.CaptchaProperties;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码配置
|
||||||
|
*
|
||||||
|
* @author Lion Li
|
||||||
|
*/
|
||||||
|
@AutoConfiguration
|
||||||
|
@EnableConfigurationProperties(CaptchaProperties.class)
|
||||||
|
@Configuration
|
||||||
|
public class CaptchaConfig {
|
||||||
|
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ResourceStore resourceStore() {
|
||||||
|
// 使用简单的本地内存存储器,实际项目中可以使用数据库等存储
|
||||||
|
LocalMemoryResourceStore resourceStore = new LocalMemoryResourceStore();
|
||||||
|
|
||||||
|
// 2. 添加自定义背景图片
|
||||||
|
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/a.jpg", "default"));
|
||||||
|
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/b.jpg", "default"));
|
||||||
|
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/c.jpg", "default"));
|
||||||
|
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/d.jpg", "default"));
|
||||||
|
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/e.jpg", "default"));
|
||||||
|
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/g.jpg", "default"));
|
||||||
|
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/h.jpg", "default"));
|
||||||
|
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/i.jpg", "default"));
|
||||||
|
resourceStore.addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/j.jpg", "default"));
|
||||||
|
resourceStore.addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/48.jpg", "default"));
|
||||||
|
resourceStore.addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/48.jpg", "default"));
|
||||||
|
// resourceStore.addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/c.jpg", "default"));
|
||||||
|
|
||||||
|
return resourceStore;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.dromara.common.web.config.properties;
|
package org.dromara.common.captcha.config.properties;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
@@ -9,7 +9,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
|||||||
* @author Lion Li
|
* @author Lion Li
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@ConfigurationProperties(prefix = "captcha")
|
@ConfigurationProperties(prefix = "image-captcha")
|
||||||
public class CaptchaProperties {
|
public class CaptchaProperties {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
package org.dromara.common.captcha.utils;
|
||||||
|
|
||||||
|
import cloud.tianai.captcha.application.ImageCaptchaApplication;
|
||||||
|
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
|
||||||
|
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||||
|
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||||
|
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||||
|
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||||
|
import cn.hutool.captcha.generator.CodeGenerator;
|
||||||
|
import cn.hutool.captcha.generator.MathGenerator;
|
||||||
|
import cn.hutool.captcha.generator.RandomGenerator;
|
||||||
|
import cn.hutool.core.util.IdUtil;
|
||||||
|
import cn.hutool.core.util.RandomUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.dromara.common.captcha.config.properties.CaptchaProperties;
|
||||||
|
import org.dromara.common.core.constant.Constants;
|
||||||
|
import org.dromara.common.core.constant.GlobalConstants;
|
||||||
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
|
import org.dromara.common.core.utils.regex.RegexValidator;
|
||||||
|
import org.dromara.common.mail.core.MailBuilder;
|
||||||
|
import org.dromara.common.redis.annotation.RateLimiter;
|
||||||
|
import org.dromara.common.redis.enums.LimitType;
|
||||||
|
import org.dromara.common.redis.utils.RedisUtils;
|
||||||
|
import org.dromara.common.web.core.WaveAndCircleCaptcha;
|
||||||
|
import org.dromara.sms4j.api.SmsBlend;
|
||||||
|
import org.dromara.sms4j.api.entity.SmsResponse;
|
||||||
|
import org.dromara.sms4j.core.factory.SmsFactory;
|
||||||
|
import org.springframework.expression.Expression;
|
||||||
|
import org.springframework.expression.ExpressionParser;
|
||||||
|
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码工具类
|
||||||
|
* 支持:图形验证码、手机短信验证码、邮箱验证码、行为验证码
|
||||||
|
* 规范:一次下发生成验证码、二次校验验证码;内置通用随机码生成、图片生成基础能力
|
||||||
|
*
|
||||||
|
* @author xiaozhi
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CaptchaUtils {
|
||||||
|
|
||||||
|
private final CaptchaProperties captchaProperties;
|
||||||
|
private final ImageCaptchaApplication imageCaptchaApplication;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SpEL解析器,单例复用,避免频繁创建
|
||||||
|
*/
|
||||||
|
private final ExpressionParser expressionParser = new SpelExpressionParser();
|
||||||
|
|
||||||
|
|
||||||
|
// ====================== 【图形验证码】 ======================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片验证码响应对象。
|
||||||
|
*
|
||||||
|
* @param captchaEnabled 是否启用验证码
|
||||||
|
* @param uuid 验证码标识
|
||||||
|
* @param img Base64 图片数据
|
||||||
|
*/
|
||||||
|
public record CaptchaVo(Boolean captchaEnabled, String uuid, String img) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成图形验证码
|
||||||
|
* 限流:同一IP 60秒最多10次请求
|
||||||
|
*
|
||||||
|
* @return CaptchaVo
|
||||||
|
*/
|
||||||
|
@RateLimiter(time = 60, count = 10, limitType = LimitType.IP)
|
||||||
|
public CaptchaVo generateImageCaptcha() {
|
||||||
|
boolean captchaEnabled = captchaProperties.getEnable();
|
||||||
|
if (!captchaEnabled) {
|
||||||
|
return new CaptchaVo(false, null, null);
|
||||||
|
}
|
||||||
|
// 保存验证码信息
|
||||||
|
String uuid = IdUtil.simpleUUID();
|
||||||
|
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
|
||||||
|
String captchaType = captchaProperties.getType();
|
||||||
|
|
||||||
|
CodeGenerator codeGenerator;
|
||||||
|
if ("math".equals(captchaType)) {
|
||||||
|
codeGenerator = new MathGenerator(captchaProperties.getNumberLength(), false);
|
||||||
|
} else {
|
||||||
|
codeGenerator = new RandomGenerator(captchaProperties.getCharLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
WaveAndCircleCaptcha captcha = new WaveAndCircleCaptcha(160, 60);
|
||||||
|
// captcha.setBackground(Color.WHITE); // 不设置就是透明底
|
||||||
|
captcha.setFont(new Font("Arial", Font.BOLD, 45));
|
||||||
|
captcha.setGenerator(codeGenerator);
|
||||||
|
captcha.createCode();
|
||||||
|
String code = captcha.getCode();
|
||||||
|
|
||||||
|
// 数学验证码,计算表达式结果存储
|
||||||
|
if ("math".equals(captchaType)) {
|
||||||
|
Expression exp = expressionParser.parseExpression(StringUtils.remove(code, "="));
|
||||||
|
code = exp.getValue(String.class);
|
||||||
|
}
|
||||||
|
RedisUtils.setCacheObject(verifyKey, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||||
|
return new CaptchaVo(true, uuid, captcha.getImageBase64());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验图形验证码
|
||||||
|
* <p>【一次性消费】校验通过自动删除缓存,防止重复使用、重放攻击
|
||||||
|
*
|
||||||
|
* @param uuid 前端传递验证码唯一标识
|
||||||
|
* @param inputCode 用户输入验证码
|
||||||
|
* @return true=验证通过 false=验证失败
|
||||||
|
*/
|
||||||
|
public boolean verifyImageCaptcha(String uuid, String inputCode) {
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 【手机短信验证码】 ======================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成手机短信验证码并发送
|
||||||
|
*
|
||||||
|
* @param phoneNumber 手机号
|
||||||
|
*/
|
||||||
|
public void generateSmsCaptcha(String phoneNumber) {
|
||||||
|
if (!RegexValidator.isMobile(phoneNumber)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String key = GlobalConstants.CAPTCHA_CODE_KEY + phoneNumber;
|
||||||
|
String code = RandomUtil.randomNumbers(6);
|
||||||
|
// 验证码模板id 自行处理 (查数据库或写死均可)
|
||||||
|
String templateId = "";
|
||||||
|
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
|
||||||
|
map.put("code", code);
|
||||||
|
SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
|
||||||
|
|
||||||
|
SmsResponse smsResponse;
|
||||||
|
try {
|
||||||
|
smsResponse = smsBlend.sendMessage(phoneNumber, templateId, map);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("验证码短信发送异常,手机号:{}", phoneNumber, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!smsResponse.isSuccess()) {
|
||||||
|
log.error("验证码短信发送失败 => {}", smsResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 发送成功才写入缓存
|
||||||
|
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验短信验证码
|
||||||
|
* <p>【一次性消费】校验成功自动删除缓存
|
||||||
|
*
|
||||||
|
* @param phoneNumber 手机号
|
||||||
|
* @param inputCode 用户输入验证码
|
||||||
|
* @return 是否通过
|
||||||
|
*/
|
||||||
|
public boolean verifySmsCaptcha(String phoneNumber, String inputCode) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// ====================== 【邮箱验证码】 ======================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成邮箱验证码并发送
|
||||||
|
* 限流:同一个邮箱60秒最多1次
|
||||||
|
*
|
||||||
|
* @param email 邮箱地址
|
||||||
|
*/
|
||||||
|
@RateLimiter(key = "#email", time = 60, count = 1)
|
||||||
|
public void generateEmailCaptcha(String email) {
|
||||||
|
if (!RegexValidator.isEmail(email)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String key = GlobalConstants.CAPTCHA_CODE_KEY + email;
|
||||||
|
String code = RandomUtil.randomNumbers(6);
|
||||||
|
try {
|
||||||
|
MailBuilder.of().to(email).subject("登录验证码").text("您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。").send();
|
||||||
|
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("验证码短信发送异常 => {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验邮箱验证码
|
||||||
|
*
|
||||||
|
* @param inputCode 用户输入
|
||||||
|
* @param cacheCode 缓存验证码
|
||||||
|
* @return 是否通过
|
||||||
|
*/
|
||||||
|
public boolean verifyEmailCaptcha(String inputCode, String cacheCode) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 【行为验证码(滑块/点选)】 ======================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成行为验证码初始票据 ticket
|
||||||
|
* 用于前端拉取滑块背景图、滑块图
|
||||||
|
*
|
||||||
|
* @return ticket字符串
|
||||||
|
*/
|
||||||
|
public ApiResponse<ImageCaptchaVO> generateBehaviorCaptchaTicket() {
|
||||||
|
String type;
|
||||||
|
int i = ThreadLocalRandom.current().nextInt(0, 4);
|
||||||
|
if (i == 0) {
|
||||||
|
type = CaptchaTypeConstant.SLIDER;
|
||||||
|
} else if (i == 1) {
|
||||||
|
type = CaptchaTypeConstant.CONCAT;
|
||||||
|
} else if (i == 2) {
|
||||||
|
type = CaptchaTypeConstant.ROTATE;
|
||||||
|
} else {
|
||||||
|
type = CaptchaTypeConstant.WORD_IMAGE_CLICK;
|
||||||
|
}
|
||||||
|
GenerateParam generateParam = new GenerateParam();
|
||||||
|
// 要生成的验证码类型
|
||||||
|
generateParam.setType(type);
|
||||||
|
return imageCaptchaApplication.generateCaptcha(generateParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 行为验证码一次校验(滑块坐标、轨迹校验由第三方SDK/自研逻辑实现)
|
||||||
|
* 本方法仅做凭证匹配占位,实际行为判定需要额外业务逻辑
|
||||||
|
*
|
||||||
|
* @param ticket 请求票据
|
||||||
|
* @param cacheTicket 缓存票据
|
||||||
|
* @return 票据是否匹配
|
||||||
|
*/
|
||||||
|
public ApiResponse<?> verifyBehaviorCaptchaFirstStage(String id, ImageCaptchaTrack data) {
|
||||||
|
ApiResponse<?> response = imageCaptchaApplication.matching(id, data);
|
||||||
|
if (response.isSuccess()) {
|
||||||
|
return ApiResponse.ofSuccess(Collections.singletonMap("id", id));
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 行为验证码二次验证(业务接口调用,例如登录接口)
|
||||||
|
*
|
||||||
|
* @param verifyTicket 前端传入二次凭证
|
||||||
|
* @param cacheVerifyTicket 服务端缓存凭证
|
||||||
|
* @return 验证结果
|
||||||
|
*/
|
||||||
|
public boolean verifyBehaviorCaptchaSecondStage(String verifyTicket, String cacheVerifyTicket) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
org.dromara.common.captcha.config.CaptchaConfig
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.dromara.common.log.event;
|
package org.dromara.common.log.event;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
@@ -34,9 +33,19 @@ public class LoginInfoEvent implements Serializable {
|
|||||||
private String message;
|
private String message;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求体
|
* 客户端IP
|
||||||
*/
|
*/
|
||||||
private HttpServletRequest request;
|
private String ip;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户代理
|
||||||
|
*/
|
||||||
|
private String userAgent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端标识
|
||||||
|
*/
|
||||||
|
private String clientId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 其他参数
|
* 其他参数
|
||||||
|
|||||||
@@ -18,23 +18,10 @@
|
|||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- 授权认证 -->
|
<!-- 授权认证 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.herodotus.opensteward</groupId>
|
<groupId>io.github.windtool</groupId>
|
||||||
<artifactId>JustAuth</artifactId>
|
<artifactId>JustAuth</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 仅供 justauth 内部解析参数使用 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.alibaba.fastjson2</groupId>
|
|
||||||
<artifactId>fastjson2</artifactId>
|
|
||||||
<version>2.0.60</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- 序列化模块 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.dromara</groupId>
|
|
||||||
<artifactId>ruoyi-common-json</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- 缓存服务 -->
|
<!-- 缓存服务 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.dromara</groupId>
|
<groupId>org.dromara</groupId>
|
||||||
|
|||||||
@@ -1,185 +0,0 @@
|
|||||||
package me.zhyd.oauth.request;
|
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
|
||||||
import me.zhyd.oauth.cache.AuthStateCache;
|
|
||||||
import me.zhyd.oauth.config.AuthConfig;
|
|
||||||
import me.zhyd.oauth.config.AuthSource;
|
|
||||||
import me.zhyd.oauth.constant.Keys;
|
|
||||||
import me.zhyd.oauth.enums.AuthResponseStatus;
|
|
||||||
import me.zhyd.oauth.enums.AuthUserGender;
|
|
||||||
import me.zhyd.oauth.exception.AuthException;
|
|
||||||
import me.zhyd.oauth.model.AuthCallback;
|
|
||||||
import me.zhyd.oauth.model.AuthToken;
|
|
||||||
import me.zhyd.oauth.model.AuthUser;
|
|
||||||
import me.zhyd.oauth.utils.HttpUtils;
|
|
||||||
import me.zhyd.oauth.utils.StringUtils;
|
|
||||||
import me.zhyd.oauth.utils.UrlBuilder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>
|
|
||||||
* 企业微信登录父类
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @author liguanhua (347826496(a)qq.com)
|
|
||||||
* @since 1.15.9
|
|
||||||
*/
|
|
||||||
public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultRequest {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建企业微信认证请求。
|
|
||||||
*
|
|
||||||
* @param config 授权配置
|
|
||||||
* @param source 授权来源
|
|
||||||
*/
|
|
||||||
public AbstractAuthWeChatEnterpriseRequest(AuthConfig config, AuthSource source) {
|
|
||||||
super(config, source);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建企业微信认证请求。
|
|
||||||
*
|
|
||||||
* @param config 授权配置
|
|
||||||
* @param source 授权来源
|
|
||||||
* @param authStateCache 授权状态缓存
|
|
||||||
*/
|
|
||||||
public AbstractAuthWeChatEnterpriseRequest(AuthConfig config, AuthSource source, AuthStateCache authStateCache) {
|
|
||||||
super(config, source, authStateCache);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取企业微信访问令牌。
|
|
||||||
*
|
|
||||||
* @param authCallback 授权回调参数
|
|
||||||
* @return 访问令牌
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public AuthToken getAccessToken(AuthCallback authCallback) {
|
|
||||||
String response = doGetAuthorizationCode(accessTokenUrl(null));
|
|
||||||
|
|
||||||
JSONObject object = this.checkResponse(response);
|
|
||||||
|
|
||||||
return AuthToken.builder()
|
|
||||||
.accessToken(object.getString(Keys.OAUTH2_ACCESS_TOKEN))
|
|
||||||
.expireIn(object.getIntValue(Keys.OAUTH2_EXPIRES_IN))
|
|
||||||
.code(authCallback.getCode())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取企业微信用户信息。
|
|
||||||
*
|
|
||||||
* @param authToken 访问令牌
|
|
||||||
* @return 授权用户信息
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public AuthUser getUserInfo(AuthToken authToken) {
|
|
||||||
String response = doGetUserInfo(authToken);
|
|
||||||
JSONObject object = this.checkResponse(response);
|
|
||||||
|
|
||||||
// 返回 OpenId 或其他,均代表非当前企业用户,不支持
|
|
||||||
// https://github.com/justauth/JustAuth/issues/227 修复bug
|
|
||||||
String userId = null;
|
|
||||||
if (object.containsKey("userid")) {
|
|
||||||
userId = object.getString("userid");
|
|
||||||
} else if (object.containsKey("UserId")) {
|
|
||||||
userId = object.getString("UserId");
|
|
||||||
}
|
|
||||||
if (userId == null) {
|
|
||||||
throw new AuthException(AuthResponseStatus.UNIDENTIFIED_PLATFORM, source);
|
|
||||||
}
|
|
||||||
String userTicket = object.getString("user_ticket");
|
|
||||||
JSONObject userDetail = getUserDetail(authToken.getAccessToken(), userId, userTicket);
|
|
||||||
|
|
||||||
return AuthUser.builder()
|
|
||||||
.rawUserInfo(userDetail)
|
|
||||||
.username(userDetail.getString(Keys.NAME))
|
|
||||||
.nickname(userDetail.getString("alias"))
|
|
||||||
.avatar(userDetail.getString(Keys.AVATAR))
|
|
||||||
.location(userDetail.getString(Keys.OAUTH2_SCOPE__ADDRESS))
|
|
||||||
.email(userDetail.getString(Keys.OAUTH2_SCOPE__EMAIL))
|
|
||||||
.uuid(userId)
|
|
||||||
.gender(AuthUserGender.getWechatRealGender(userDetail.getString(Keys.GENDER)))
|
|
||||||
.token(authToken)
|
|
||||||
.source(source.toString())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验请求结果
|
|
||||||
*
|
|
||||||
* @param response 请求结果
|
|
||||||
* @return 如果请求结果正常,则返回JSONObject
|
|
||||||
*/
|
|
||||||
private JSONObject checkResponse(String response) {
|
|
||||||
JSONObject object = JSONObject.parseObject(response);
|
|
||||||
|
|
||||||
if (object.containsKey(Keys.VARIANT__ERRCODE) && object.getIntValue(Keys.VARIANT__ERRCODE) != 0) {
|
|
||||||
throw new AuthException(object.getString(Keys.VARIANT__ERRMSG), source);
|
|
||||||
}
|
|
||||||
|
|
||||||
return object;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 返回获取accessToken的url
|
|
||||||
*
|
|
||||||
* @param code 授权码
|
|
||||||
* @return 返回获取accessToken的url
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected String accessTokenUrl(String code) {
|
|
||||||
return UrlBuilder.fromBaseUrl(source.accessToken())
|
|
||||||
.queryParam("corpid", config.getClientId())
|
|
||||||
.queryParam("corpsecret", config.getClientSecret())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 返回获取userInfo的url
|
|
||||||
*
|
|
||||||
* @param authToken 用户授权后的token
|
|
||||||
* @return 返回获取userInfo的url
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected String userInfoUrl(AuthToken authToken) {
|
|
||||||
return UrlBuilder.fromBaseUrl(source.userInfo())
|
|
||||||
.queryParam(Keys.OAUTH2_ACCESS_TOKEN, authToken.getAccessToken())
|
|
||||||
.queryParam(Keys.OAUTH2_CODE, authToken.getCode())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户详情
|
|
||||||
*
|
|
||||||
* @param accessToken accessToken
|
|
||||||
* @param userId 企业内用户id
|
|
||||||
* @param userTicket 成员票据,用于获取用户信息或敏感信息
|
|
||||||
* @return 用户详情
|
|
||||||
*/
|
|
||||||
private JSONObject getUserDetail(String accessToken, String userId, String userTicket) {
|
|
||||||
// 用户基础信息
|
|
||||||
String userInfoUrl = UrlBuilder.fromBaseUrl("https://qyapi.weixin.qq.com/cgi-bin/user/get")
|
|
||||||
.queryParam(Keys.OAUTH2_ACCESS_TOKEN, accessToken)
|
|
||||||
.queryParam(Keys.USERID, userId)
|
|
||||||
.build();
|
|
||||||
String userInfoResponse = new HttpUtils(config.getHttpConfig()).get(userInfoUrl).getBody();
|
|
||||||
JSONObject userInfo = checkResponse(userInfoResponse);
|
|
||||||
|
|
||||||
// 用户敏感信息
|
|
||||||
if (StringUtils.isNotEmpty(userTicket)) {
|
|
||||||
String userDetailUrl = UrlBuilder.fromBaseUrl("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail")
|
|
||||||
.queryParam(Keys.OAUTH2_ACCESS_TOKEN, accessToken)
|
|
||||||
.build();
|
|
||||||
JSONObject param = new JSONObject();
|
|
||||||
param.put("user_ticket", userTicket);
|
|
||||||
String userDetailResponse = new HttpUtils(config.getHttpConfig()).post(userDetailUrl, param.toJSONString()).getBody();
|
|
||||||
JSONObject userDetail = checkResponse(userDetailResponse);
|
|
||||||
|
|
||||||
userInfo.putAll(userDetail);
|
|
||||||
}
|
|
||||||
return userInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
package me.zhyd.oauth.request;
|
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
|
||||||
import com.xkcoding.http.support.HttpHeader;
|
|
||||||
import me.zhyd.oauth.cache.AuthStateCache;
|
|
||||||
import me.zhyd.oauth.config.AuthConfig;
|
|
||||||
import me.zhyd.oauth.config.AuthDefaultSource;
|
|
||||||
import me.zhyd.oauth.constant.Keys;
|
|
||||||
import me.zhyd.oauth.enums.scope.AuthDingTalkScope;
|
|
||||||
import me.zhyd.oauth.exception.AuthException;
|
|
||||||
import me.zhyd.oauth.model.AuthCallback;
|
|
||||||
import me.zhyd.oauth.model.AuthToken;
|
|
||||||
import me.zhyd.oauth.model.AuthUser;
|
|
||||||
import me.zhyd.oauth.utils.AuthScopeUtils;
|
|
||||||
import me.zhyd.oauth.utils.HttpUtils;
|
|
||||||
import me.zhyd.oauth.utils.UrlBuilder;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 新版钉钉二维码登录
|
|
||||||
*
|
|
||||||
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
|
|
||||||
* @since 1.16.7
|
|
||||||
*/
|
|
||||||
public class AuthDingTalkV2Request extends AuthDefaultRequest {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建钉钉 V2 认证请求。
|
|
||||||
*
|
|
||||||
* @param config 授权配置
|
|
||||||
*/
|
|
||||||
public AuthDingTalkV2Request(AuthConfig config) {
|
|
||||||
super(config, AuthDefaultSource.DINGTALK_V2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建钉钉 V2 认证请求。
|
|
||||||
*
|
|
||||||
* @param config 授权配置
|
|
||||||
* @param authStateCache 授权状态缓存
|
|
||||||
*/
|
|
||||||
public AuthDingTalkV2Request(AuthConfig config, AuthStateCache authStateCache) {
|
|
||||||
super(config, AuthDefaultSource.DINGTALK_V2, authStateCache);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成钉钉 V2 授权地址。
|
|
||||||
*
|
|
||||||
* @param state 授权状态
|
|
||||||
* @return 授权地址
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String authorize(String state) {
|
|
||||||
return UrlBuilder.fromBaseUrl(source.authorize())
|
|
||||||
.queryParam(Keys.OAUTH2_RESPONSE_TYPE, Keys.OAUTH2_CODE)
|
|
||||||
.queryParam(Keys.OAUTH2_CLIENT_ID, config.getClientId())
|
|
||||||
.queryParam(Keys.OAUTH2_SCOPE, this.getScopes(",", true, AuthScopeUtils.getDefaultScopes(AuthDingTalkScope.values())))
|
|
||||||
.queryParam(Keys.OAUTH2_REDIRECT_URI, config.getRedirectUri())
|
|
||||||
.queryParam("prompt", "consent")
|
|
||||||
.queryParam("org_type", config.getDingTalkOrgType())
|
|
||||||
.queryParam("corpId", config.getDingTalkCorpId())
|
|
||||||
.queryParam("exclusiveLogin", config.isDingTalkExclusiveLogin())
|
|
||||||
.queryParam("exclusiveCorpId", config.getDingTalkExclusiveCorpId())
|
|
||||||
.queryParam(Keys.OAUTH2_STATE, getRealState(state))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取钉钉 V2 访问令牌。
|
|
||||||
*
|
|
||||||
* @param authCallback 授权回调参数
|
|
||||||
* @return 访问令牌
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public AuthToken getAccessToken(AuthCallback authCallback) {
|
|
||||||
Map<String, String> params = new HashMap<>();
|
|
||||||
params.put("grantType", Keys.OAUTH2_GRANT_TYPE__AUTHORIZATION_CODE);
|
|
||||||
params.put("clientId", config.getClientId());
|
|
||||||
params.put("clientSecret", config.getClientSecret());
|
|
||||||
params.put(Keys.OAUTH2_CODE, authCallback.getCode());
|
|
||||||
String response = new HttpUtils(config.getHttpConfig()).post(this.source.accessToken(), JSONObject.toJSONString(params)).getBody();
|
|
||||||
JSONObject accessTokenObject = JSONObject.parseObject(response);
|
|
||||||
if (!accessTokenObject.containsKey("accessToken")) {
|
|
||||||
throw new AuthException(JSONObject.toJSONString(response), source);
|
|
||||||
}
|
|
||||||
return AuthToken.builder()
|
|
||||||
.accessToken(accessTokenObject.getString("accessToken"))
|
|
||||||
.refreshToken(accessTokenObject.getString("refreshToken"))
|
|
||||||
.expireIn(accessTokenObject.getIntValue("expireIn"))
|
|
||||||
.corpId(accessTokenObject.getString("corpId"))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取钉钉 V2 用户信息。
|
|
||||||
*
|
|
||||||
* @param authToken 访问令牌
|
|
||||||
* @return 授权用户信息
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public AuthUser getUserInfo(AuthToken authToken) {
|
|
||||||
HttpHeader header = new HttpHeader();
|
|
||||||
header.add("x-acs-dingtalk-access-token", authToken.getAccessToken());
|
|
||||||
|
|
||||||
String response = new HttpUtils(config.getHttpConfig()).get(this.source.userInfo(), null, header, false).getBody();
|
|
||||||
JSONObject object = JSONObject.parseObject(response);
|
|
||||||
|
|
||||||
authToken.setOpenId(object.getString("openId"));
|
|
||||||
authToken.setUnionId(object.getString("unionId"));
|
|
||||||
return AuthUser.builder()
|
|
||||||
.rawUserInfo(object)
|
|
||||||
.uuid(object.getString("unionId"))
|
|
||||||
.username(object.getString("nick"))
|
|
||||||
.nickname(object.getString("nick"))
|
|
||||||
.avatar(object.getString("avatarUrl"))
|
|
||||||
.snapshotUser(object.getBooleanValue("visitor"))
|
|
||||||
.token(authToken)
|
|
||||||
.source(source.toString())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 返回获取accessToken的url
|
|
||||||
*
|
|
||||||
* @param code 授权码
|
|
||||||
* @return 返回获取accessToken的url
|
|
||||||
*/
|
|
||||||
protected String accessTokenUrl(String code) {
|
|
||||||
return UrlBuilder.fromBaseUrl(source.accessToken())
|
|
||||||
.queryParam(Keys.OAUTH2_CODE, code)
|
|
||||||
.queryParam("clientId", config.getClientId())
|
|
||||||
.queryParam("clientSecret", config.getClientSecret())
|
|
||||||
.queryParam("grantType", Keys.OAUTH2_GRANT_TYPE__AUTHORIZATION_CODE)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -68,7 +68,7 @@ public class SocialLoginConfigProperties {
|
|||||||
private String clientOsType;
|
private String clientOsType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* maxkey 服务器地址
|
* 自托管授权服务器地址
|
||||||
*/
|
*/
|
||||||
private String serverUrl;
|
private String serverUrl;
|
||||||
|
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
package org.dromara.common.social.gitea;
|
|
||||||
|
|
||||||
import cn.hutool.core.lang.Dict;
|
|
||||||
import cn.hutool.http.HttpRequest;
|
|
||||||
import cn.hutool.http.HttpResponse;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import me.zhyd.oauth.cache.AuthStateCache;
|
|
||||||
import me.zhyd.oauth.config.AuthConfig;
|
|
||||||
import me.zhyd.oauth.exception.AuthException;
|
|
||||||
import me.zhyd.oauth.model.AuthCallback;
|
|
||||||
import me.zhyd.oauth.model.AuthToken;
|
|
||||||
import me.zhyd.oauth.model.AuthUser;
|
|
||||||
import me.zhyd.oauth.request.AuthDefaultRequest;
|
|
||||||
import org.dromara.common.core.utils.SpringUtils;
|
|
||||||
import org.dromara.common.json.utils.JsonUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author lcry
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class AuthGiteaRequest extends AuthDefaultRequest {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 服务器地址。
|
|
||||||
*/
|
|
||||||
public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.gitea.server-url");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设定归属域
|
|
||||||
*/
|
|
||||||
public AuthGiteaRequest(AuthConfig config) {
|
|
||||||
super(config, AuthGiteaSource.GITEA);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 Gitea 认证请求。
|
|
||||||
*
|
|
||||||
* @param config 授权配置
|
|
||||||
* @param authStateCache 授权状态缓存
|
|
||||||
*/
|
|
||||||
public AuthGiteaRequest(AuthConfig config, AuthStateCache authStateCache) {
|
|
||||||
super(config, AuthGiteaSource.GITEA, authStateCache);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 Gitea 访问令牌。
|
|
||||||
*
|
|
||||||
* @param authCallback 授权回调参数
|
|
||||||
* @return 访问令牌
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public AuthToken getAccessToken(AuthCallback authCallback) {
|
|
||||||
String body = doPostAuthorizationCode(authCallback.getCode());
|
|
||||||
Dict object = JsonUtils.parseMap(body);
|
|
||||||
// oauth/token 验证异常
|
|
||||||
if (object.containsKey("error")) {
|
|
||||||
throw new AuthException(object.getStr("error_description"));
|
|
||||||
}
|
|
||||||
// user 验证异常
|
|
||||||
if (object.containsKey("message")) {
|
|
||||||
throw new AuthException(object.getStr("message"));
|
|
||||||
}
|
|
||||||
return AuthToken.builder()
|
|
||||||
.accessToken(object.getStr("access_token"))
|
|
||||||
.refreshToken(object.getStr("refresh_token"))
|
|
||||||
.idToken(object.getStr("id_token"))
|
|
||||||
.tokenType(object.getStr("token_type"))
|
|
||||||
.scope(object.getStr("scope"))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 使用授权码换取 Gitea 访问令牌响应。
|
|
||||||
*
|
|
||||||
* @param code 授权码
|
|
||||||
* @return 令牌接口响应体
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected String doPostAuthorizationCode(String code) {
|
|
||||||
HttpRequest request = HttpRequest.post(source.accessToken())
|
|
||||||
.form("client_id", config.getClientId())
|
|
||||||
.form("client_secret", config.getClientSecret())
|
|
||||||
.form("grant_type", "authorization_code")
|
|
||||||
.form("code", code)
|
|
||||||
.form("redirect_uri", config.getRedirectUri());
|
|
||||||
HttpResponse response = request.execute();
|
|
||||||
return response.body();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 Gitea 用户信息。
|
|
||||||
*
|
|
||||||
* @param authToken 访问令牌
|
|
||||||
* @return 授权用户信息
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public AuthUser getUserInfo(AuthToken authToken) {
|
|
||||||
String body = doGetUserInfo(authToken);
|
|
||||||
Dict object = JsonUtils.parseMap(body);
|
|
||||||
// oauth/token 验证异常
|
|
||||||
if (object.containsKey("error")) {
|
|
||||||
throw new AuthException(object.getStr("error_description"));
|
|
||||||
}
|
|
||||||
// user 验证异常
|
|
||||||
if (object.containsKey("message")) {
|
|
||||||
throw new AuthException(object.getStr("message"));
|
|
||||||
}
|
|
||||||
return AuthUser.builder()
|
|
||||||
.uuid(object.getStr("sub"))
|
|
||||||
.username(object.getStr("name"))
|
|
||||||
.nickname(object.getStr("preferred_username"))
|
|
||||||
.avatar(object.getStr("picture"))
|
|
||||||
.email(object.getStr("email"))
|
|
||||||
.token(authToken)
|
|
||||||
.source(source.toString())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package org.dromara.common.social.gitea;
|
|
||||||
|
|
||||||
import me.zhyd.oauth.config.AuthSource;
|
|
||||||
import me.zhyd.oauth.request.AuthDefaultRequest;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* gitea Oauth2 默认接口说明
|
|
||||||
*
|
|
||||||
* @author lcry
|
|
||||||
*/
|
|
||||||
public enum AuthGiteaSource implements AuthSource {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 自己搭建的 gitea 私服
|
|
||||||
*/
|
|
||||||
GITEA {
|
|
||||||
/**
|
|
||||||
* 授权的api
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String authorize() {
|
|
||||||
return AuthGiteaRequest.SERVER_URL + "/login/oauth/authorize";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取accessToken的api
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String accessToken() {
|
|
||||||
return AuthGiteaRequest.SERVER_URL + "/login/oauth/access_token";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户信息的api
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String userInfo() {
|
|
||||||
return AuthGiteaRequest.SERVER_URL + "/login/oauth/userinfo";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 平台对应的 AuthRequest 实现类,必须继承自 {@link AuthDefaultRequest}
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Class<? extends AuthDefaultRequest> getTargetClass() {
|
|
||||||
return AuthGiteaRequest.class;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
package org.dromara.common.social.maxkey;
|
|
||||||
|
|
||||||
import cn.hutool.core.lang.Dict;
|
|
||||||
import me.zhyd.oauth.cache.AuthStateCache;
|
|
||||||
import me.zhyd.oauth.config.AuthConfig;
|
|
||||||
import me.zhyd.oauth.exception.AuthException;
|
|
||||||
import me.zhyd.oauth.model.AuthCallback;
|
|
||||||
import me.zhyd.oauth.model.AuthToken;
|
|
||||||
import me.zhyd.oauth.model.AuthUser;
|
|
||||||
import me.zhyd.oauth.request.AuthDefaultRequest;
|
|
||||||
import org.dromara.common.core.utils.SpringUtils;
|
|
||||||
import org.dromara.common.json.utils.JsonUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author 长春叭哥 2023年03月26日
|
|
||||||
*/
|
|
||||||
public class AuthMaxKeyRequest extends AuthDefaultRequest {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 服务器地址。
|
|
||||||
*/
|
|
||||||
public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.maxkey.server-url");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设定归属域
|
|
||||||
*/
|
|
||||||
public AuthMaxKeyRequest(AuthConfig config) {
|
|
||||||
super(config, AuthMaxKeySource.MAXKEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 MaxKey 认证请求。
|
|
||||||
*
|
|
||||||
* @param config 授权配置
|
|
||||||
* @param authStateCache 授权状态缓存
|
|
||||||
*/
|
|
||||||
public AuthMaxKeyRequest(AuthConfig config, AuthStateCache authStateCache) {
|
|
||||||
super(config, AuthMaxKeySource.MAXKEY, authStateCache);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 MaxKey 访问令牌。
|
|
||||||
*
|
|
||||||
* @param authCallback 授权回调参数
|
|
||||||
* @return 访问令牌
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public AuthToken getAccessToken(AuthCallback authCallback) {
|
|
||||||
String body = doPostAuthorizationCode(authCallback.getCode());
|
|
||||||
Dict object = JsonUtils.parseMap(body);
|
|
||||||
// oauth/token 验证异常
|
|
||||||
if (object.containsKey("error")) {
|
|
||||||
throw new AuthException(object.getStr("error_description"));
|
|
||||||
}
|
|
||||||
// user 验证异常
|
|
||||||
if (object.containsKey("message")) {
|
|
||||||
throw new AuthException(object.getStr("message"));
|
|
||||||
}
|
|
||||||
return AuthToken.builder()
|
|
||||||
.accessToken(object.getStr("access_token"))
|
|
||||||
.refreshToken(object.getStr("refresh_token"))
|
|
||||||
.idToken(object.getStr("id_token"))
|
|
||||||
.tokenType(object.getStr("token_type"))
|
|
||||||
.scope(object.getStr("scope"))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 MaxKey 用户信息。
|
|
||||||
*
|
|
||||||
* @param authToken 访问令牌
|
|
||||||
* @return 授权用户信息
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public AuthUser getUserInfo(AuthToken authToken) {
|
|
||||||
String body = doGetUserInfo(authToken);
|
|
||||||
Dict object = JsonUtils.parseMap(body);
|
|
||||||
// oauth/token 验证异常
|
|
||||||
if (object.containsKey("error")) {
|
|
||||||
throw new AuthException(object.getStr("error_description"));
|
|
||||||
}
|
|
||||||
// user 验证异常
|
|
||||||
if (object.containsKey("message")) {
|
|
||||||
throw new AuthException(object.getStr("message"));
|
|
||||||
}
|
|
||||||
return AuthUser.builder()
|
|
||||||
.uuid(object.getStr("userId"))
|
|
||||||
.username(object.getStr("username"))
|
|
||||||
.nickname(object.getStr("displayName"))
|
|
||||||
.avatar(object.getStr("avatar_url"))
|
|
||||||
.blog(object.getStr("web_url"))
|
|
||||||
.company(object.getStr("organization"))
|
|
||||||
.location(object.getStr("location"))
|
|
||||||
.email(object.getStr("email"))
|
|
||||||
.remark(object.getStr("bio"))
|
|
||||||
.token(authToken)
|
|
||||||
.source(source.toString())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package org.dromara.common.social.maxkey;
|
|
||||||
|
|
||||||
import me.zhyd.oauth.config.AuthSource;
|
|
||||||
import me.zhyd.oauth.request.AuthDefaultRequest;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Oauth2 默认接口说明
|
|
||||||
*
|
|
||||||
* @author 长春叭哥 2023年03月26日
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public enum AuthMaxKeySource implements AuthSource {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 自己搭建的 maxkey 私服
|
|
||||||
*/
|
|
||||||
MAXKEY {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 授权的api
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String authorize() {
|
|
||||||
return AuthMaxKeyRequest.SERVER_URL + "/sign/authz/oauth/v20/authorize";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取accessToken的api
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String accessToken() {
|
|
||||||
return AuthMaxKeyRequest.SERVER_URL + "/sign/authz/oauth/v20/token";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户信息的api
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String userInfo() {
|
|
||||||
return AuthMaxKeyRequest.SERVER_URL + "/sign/api/oauth/v20/me";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 平台对应的 AuthRequest 实现类,必须继承自 {@link AuthDefaultRequest}
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Class<? extends AuthDefaultRequest> getTargetClass() {
|
|
||||||
return AuthMaxKeyRequest.class;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
package org.dromara.common.social.topiam;
|
|
||||||
|
|
||||||
import cn.hutool.core.codec.Base64;
|
|
||||||
import cn.hutool.core.lang.Dict;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import cn.hutool.http.HttpRequest;
|
|
||||||
import cn.hutool.http.HttpResponse;
|
|
||||||
import com.xkcoding.http.support.HttpHeader;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import me.zhyd.oauth.cache.AuthStateCache;
|
|
||||||
import me.zhyd.oauth.config.AuthConfig;
|
|
||||||
import me.zhyd.oauth.exception.AuthException;
|
|
||||||
import me.zhyd.oauth.model.AuthCallback;
|
|
||||||
import me.zhyd.oauth.model.AuthToken;
|
|
||||||
import me.zhyd.oauth.model.AuthUser;
|
|
||||||
import me.zhyd.oauth.request.AuthDefaultRequest;
|
|
||||||
import me.zhyd.oauth.utils.HttpUtils;
|
|
||||||
import me.zhyd.oauth.utils.UrlBuilder;
|
|
||||||
import org.dromara.common.core.utils.SpringUtils;
|
|
||||||
import org.dromara.common.json.utils.JsonUtils;
|
|
||||||
|
|
||||||
import static org.dromara.common.social.topiam.AuthTopIamSource.TOPIAM;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TopIAM 认证请求
|
|
||||||
*
|
|
||||||
* @author xlsea
|
|
||||||
* @since 2024-01-06
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class AuthTopIamRequest extends AuthDefaultRequest {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 服务器地址。
|
|
||||||
*/
|
|
||||||
public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.topiam.server-url");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设定归属域
|
|
||||||
*/
|
|
||||||
public AuthTopIamRequest(AuthConfig config) {
|
|
||||||
super(config, TOPIAM);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建 TopIAM 认证请求。
|
|
||||||
*
|
|
||||||
* @param config 授权配置
|
|
||||||
* @param authStateCache 授权状态缓存
|
|
||||||
*/
|
|
||||||
public AuthTopIamRequest(AuthConfig config, AuthStateCache authStateCache) {
|
|
||||||
super(config, TOPIAM, authStateCache);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 TopIAM 访问令牌。
|
|
||||||
*
|
|
||||||
* @param authCallback 授权回调参数
|
|
||||||
* @return 访问令牌
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public AuthToken getAccessToken(AuthCallback authCallback) {
|
|
||||||
String body = doPostAuthorizationCode(authCallback.getCode());
|
|
||||||
Dict object = JsonUtils.parseMap(body);
|
|
||||||
checkResponse(object);
|
|
||||||
return AuthToken.builder()
|
|
||||||
.accessToken(object.getStr("access_token"))
|
|
||||||
.refreshToken(object.getStr("refresh_token"))
|
|
||||||
.idToken(object.getStr("id_token"))
|
|
||||||
.tokenType(object.getStr("token_type"))
|
|
||||||
.scope(object.getStr("scope"))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 TopIAM 用户信息。
|
|
||||||
*
|
|
||||||
* @param authToken 访问令牌
|
|
||||||
* @return 授权用户信息
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public AuthUser getUserInfo(AuthToken authToken) {
|
|
||||||
String body = doGetUserInfo(authToken);
|
|
||||||
Dict object = JsonUtils.parseMap(body);
|
|
||||||
checkResponse(object);
|
|
||||||
return AuthUser.builder()
|
|
||||||
.uuid(object.getStr("sub"))
|
|
||||||
.username(object.getStr("preferred_username"))
|
|
||||||
.nickname(object.getStr("nickname"))
|
|
||||||
.avatar(object.getStr("picture"))
|
|
||||||
.email(object.getStr("email"))
|
|
||||||
.token(authToken)
|
|
||||||
.source(source.toString())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 使用授权码换取 TopIAM 访问令牌响应。
|
|
||||||
*
|
|
||||||
* @param code 授权码
|
|
||||||
* @return 令牌接口响应体
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected String doPostAuthorizationCode(String code) {
|
|
||||||
HttpRequest request = HttpRequest.post(source.accessToken())
|
|
||||||
.header("Authorization", "Basic " + Base64.encode("%s:%s".formatted(config.getClientId(), config.getClientSecret())))
|
|
||||||
.form("grant_type", "authorization_code")
|
|
||||||
.form("code", code)
|
|
||||||
.form("redirect_uri", config.getRedirectUri());
|
|
||||||
HttpResponse response = request.execute();
|
|
||||||
return response.body();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 TopIAM 用户信息接口响应。
|
|
||||||
*
|
|
||||||
* @param authToken 访问令牌
|
|
||||||
* @return 用户信息响应体
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected String doGetUserInfo(AuthToken authToken) {
|
|
||||||
return new HttpUtils(config.getHttpConfig()).get(source.userInfo(), null, new HttpHeader()
|
|
||||||
.add("Content-Type", "application/json")
|
|
||||||
.add("Authorization", "Bearer " + authToken.getAccessToken()), false).getBody();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成 TopIAM 授权地址。
|
|
||||||
*
|
|
||||||
* @param state 授权状态
|
|
||||||
* @return 授权地址
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String authorize(String state) {
|
|
||||||
return UrlBuilder.fromBaseUrl(super.authorize(state))
|
|
||||||
.queryParam("scope", StrUtil.join("%20", config.getScopes()))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验 TopIAM 接口响应。
|
|
||||||
*
|
|
||||||
* @param object 响应内容
|
|
||||||
*/
|
|
||||||
private static void checkResponse(Dict object) {
|
|
||||||
// oauth/token 验证异常
|
|
||||||
if (object.containsKey("error")) {
|
|
||||||
throw new AuthException(object.getStr("error_description"));
|
|
||||||
}
|
|
||||||
// user 验证异常
|
|
||||||
if (object.containsKey("message")) {
|
|
||||||
throw new AuthException(object.getStr("message"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package org.dromara.common.social.topiam;
|
|
||||||
|
|
||||||
import me.zhyd.oauth.config.AuthSource;
|
|
||||||
import me.zhyd.oauth.request.AuthDefaultRequest;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Oauth2 默认接口说明
|
|
||||||
*
|
|
||||||
* @author xlsea
|
|
||||||
* @since 2024-01-06
|
|
||||||
*/
|
|
||||||
public enum AuthTopIamSource implements AuthSource {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 测试
|
|
||||||
*/
|
|
||||||
TOPIAM {
|
|
||||||
/**
|
|
||||||
* 授权的api
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String authorize() {
|
|
||||||
return AuthTopIamRequest.SERVER_URL + "/oauth2/auth";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取accessToken的api
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String accessToken() {
|
|
||||||
return AuthTopIamRequest.SERVER_URL + "/oauth2/token";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户信息的api
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String userInfo() {
|
|
||||||
return AuthTopIamRequest.SERVER_URL + "/oauth2/userinfo";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 平台对应的 AuthRequest 实现类,必须继承自 {@link AuthDefaultRequest}
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public Class<? extends AuthDefaultRequest> getTargetClass() {
|
|
||||||
return AuthTopIamRequest.class;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,9 +10,6 @@ import me.zhyd.oauth.request.*;
|
|||||||
import org.dromara.common.core.utils.SpringUtils;
|
import org.dromara.common.core.utils.SpringUtils;
|
||||||
import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
|
import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
|
||||||
import org.dromara.common.social.config.properties.SocialProperties;
|
import org.dromara.common.social.config.properties.SocialProperties;
|
||||||
import org.dromara.common.social.gitea.AuthGiteaRequest;
|
|
||||||
import org.dromara.common.social.maxkey.AuthMaxKeyRequest;
|
|
||||||
import org.dromara.common.social.topiam.AuthTopIamRequest;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 认证授权工具类
|
* 认证授权工具类
|
||||||
@@ -58,6 +55,7 @@ public class SocialUtils {
|
|||||||
.clientId(obj.getClientId())
|
.clientId(obj.getClientId())
|
||||||
.clientSecret(obj.getClientSecret())
|
.clientSecret(obj.getClientSecret())
|
||||||
.redirectUri(obj.getRedirectUri())
|
.redirectUri(obj.getRedirectUri())
|
||||||
|
.serverUrl(obj.getServerUrl())
|
||||||
.scopes(obj.getScopes());
|
.scopes(obj.getScopes());
|
||||||
return switch (source.toLowerCase()) {
|
return switch (source.toLowerCase()) {
|
||||||
case "dingtalk" -> new AuthDingTalkV2Request(builder.build(), STATE_CACHE);
|
case "dingtalk" -> new AuthDingTalkV2Request(builder.build(), STATE_CACHE);
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package org.dromara.common.web.config;
|
|
||||||
|
|
||||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证码配置
|
|
||||||
*
|
|
||||||
* @author Lion Li
|
|
||||||
*/
|
|
||||||
@AutoConfiguration
|
|
||||||
@EnableConfigurationProperties(CaptchaProperties.class)
|
|
||||||
public class CaptchaConfig {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
org.dromara.common.web.config.CaptchaConfig
|
|
||||||
org.dromara.common.web.config.FilterConfig
|
org.dromara.common.web.config.FilterConfig
|
||||||
org.dromara.common.web.config.I18nConfig
|
org.dromara.common.web.config.I18nConfig
|
||||||
org.dromara.common.web.config.ResourcesConfig
|
org.dromara.common.web.config.ResourcesConfig
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
|
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
|
||||||
FROM bellsoft/liberica-openjdk-rocky:21.0.11-cds
|
FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds
|
||||||
# FROM bellsoft/liberica-openjdk-rocky:25.0.3-cds
|
# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds
|
||||||
# FROM findepi/graalvm:java21-native
|
# FROM findepi/graalvm:java21-native
|
||||||
|
|
||||||
LABEL maintainer="Lion Li"
|
LABEL maintainer="Lion Li"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
|
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
|
||||||
FROM bellsoft/liberica-openjdk-rocky:21.0.11-cds
|
FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds
|
||||||
# FROM bellsoft/liberica-openjdk-rocky:25.0.3-cds
|
# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds
|
||||||
# FROM findepi/graalvm:java21-native
|
# FROM findepi/graalvm:java21-native
|
||||||
|
|
||||||
LABEL maintainer="Lion Li"
|
LABEL maintainer="Lion Li"
|
||||||
|
|||||||
@@ -19,6 +19,17 @@
|
|||||||
<version>${snailai.version}</version>
|
<version>${snailai.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>co.elastic.clients</groupId>
|
||||||
|
<artifactId>elasticsearch-rest5-client</artifactId>
|
||||||
|
<version>9.4.2</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>co.elastic.clients</groupId>
|
||||||
|
<artifactId>elasticsearch-java</artifactId>
|
||||||
|
<version>9.4.2</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Spring Boot Admin 客户端 -->
|
<!-- Spring Boot Admin 客户端 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>de.codecentric</groupId>
|
<groupId>de.codecentric</groupId>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
|
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
|
||||||
FROM bellsoft/liberica-openjdk-rocky:21.0.11-cds
|
FROM bellsoft/liberica-openjdk-rocky:21.0.12-cds
|
||||||
# FROM bellsoft/liberica-openjdk-rocky:25.0.3-cds
|
# FROM bellsoft/liberica-openjdk-rocky:25.0.4-cds
|
||||||
# FROM findepi/graalvm:java21-native
|
# FROM findepi/graalvm:java21-native
|
||||||
|
|
||||||
LABEL maintainer="Lion Li"
|
LABEL maintainer="Lion Li"
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ public class ${ClassName}Controller extends BaseController {
|
|||||||
}
|
}
|
||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
|
<#if enableExport>
|
||||||
/**
|
/**
|
||||||
* 导出${functionName}列表
|
* 导出${functionName}列表
|
||||||
*/
|
*/
|
||||||
<#if enableExport>
|
|
||||||
@SaCheckPermission("${permissionPrefix}:export")
|
@SaCheckPermission("${permissionPrefix}:export")
|
||||||
@Log(title = "${functionName}", businessType = BusinessType.EXPORT)
|
@Log(title = "${functionName}", businessType = BusinessType.EXPORT)
|
||||||
@PostMapping("/export")
|
@PostMapping("/export")
|
||||||
@@ -153,5 +153,3 @@ public class ${ClassName}Controller extends BaseController {
|
|||||||
return toAjax(${className}Service.deleteWithValidByIds(List.of(${pkColumn.javaField}s), true));
|
return toAjax(${className}Service.deleteWithValidByIds(List.of(${pkColumn.javaField}s), true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -46,5 +46,3 @@ public class ${ClassName} extends BaseEntity {
|
|||||||
</#list>
|
</#list>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,3 @@ import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
|||||||
public interface ${ClassName}Mapper extends BaseMapperPlus<${ClassName}, ${ClassName}Vo> {
|
public interface ${ClassName}Mapper extends BaseMapperPlus<${ClassName}, ${ClassName}Vo> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,5 @@ public interface I${ClassName}Service {
|
|||||||
* @return 是否删除成功
|
* @return 是否删除成功
|
||||||
*/
|
*/
|
||||||
Boolean deleteWithValidByIds(Collection<${pkColumn.javaType}> ids, Boolean isValid);
|
Boolean deleteWithValidByIds(Collection<${pkColumn.javaType}> ids, Boolean isValid);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -342,6 +342,5 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service {
|
|||||||
}
|
}
|
||||||
return ${className}Mapper.deleteByIds(ids) > 0;
|
return ${className}Mapper.deleteByIds(ids) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ import java.io.Serial;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ${functionName}视图对象 ${tableName}
|
* ${functionName}视图对象 ${tableName}
|
||||||
*
|
*
|
||||||
@@ -61,5 +59,3 @@ public class ${ClassName}Vo implements Serializable {
|
|||||||
</#list>
|
</#list>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ values(${table.menuIds[3]}, '${functionName}修改', ${table.menuIds[0]}, 3, '#
|
|||||||
|
|
||||||
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
||||||
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '');
|
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '');
|
||||||
|
<#if enableExport>
|
||||||
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
||||||
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '');
|
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '');
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ values(${table.menuIds[3]}, '${functionName}修改', ${table.menuIds[0]}, 3, '#
|
|||||||
|
|
||||||
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
||||||
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, sysdate, null, null, '');
|
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, sysdate, null, null, '');
|
||||||
|
<#if enableExport>
|
||||||
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
||||||
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, sysdate, null, null, '');
|
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, sysdate, null, null, '');
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ values(${table.menuIds[3]}, '${functionName}修改', ${table.menuIds[0]}, 3, '#
|
|||||||
|
|
||||||
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
||||||
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, now(), null, null, '');
|
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, now(), null, null, '');
|
||||||
|
<#if enableExport>
|
||||||
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
||||||
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, now(), null, null, '');
|
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, now(), null, null, '');
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ values(${table.menuIds[3]}, '${functionName}修改', ${table.menuIds[0]}, 3, '#
|
|||||||
|
|
||||||
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
||||||
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, getdate(), null, null, '');
|
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, getdate(), null, null, '');
|
||||||
|
<#if enableExport>
|
||||||
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
|
||||||
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, getdate(), null, null, '');
|
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, getdate(), null, null, '');
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import request from '@/utils/request';
|
|||||||
* @param query
|
* @param query
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const list${BusinessName} = (query?: ${BusinessName}Query): AxiosPromise<<#if table.tree>${BusinessName}VO[]<#else>PageResult<${BusinessName}VO></#if>> => {
|
export const list${BusinessName} = (query?: ${BusinessName}Query): AxiosPromise<<#if table.tree>${BusinessName}VO[]<#else>PageResult<${BusinessName}VO></#if>> => {
|
||||||
return request({
|
return request({
|
||||||
url: '/${moduleName}/${businessName}/list',
|
url: '/${moduleName}/${businessName}/list',
|
||||||
@@ -100,5 +99,3 @@ export const del${BusinessName} = (${pkColumn.javaField}: string | number | Arra
|
|||||||
method: 'delete'
|
method: 'delete'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -425,7 +425,7 @@ import { useTreeTableExpand } from '@/hooks/tree/useTreeTableExpand';
|
|||||||
import { useDict } from '@/utils/dict';
|
import { useDict } from '@/utils/dict';
|
||||||
</#if>
|
</#if>
|
||||||
import modal from '@/plugins/modal';
|
import modal from '@/plugins/modal';
|
||||||
import { handleTree } from '@/utils/ruoyi';
|
import { handleTree<#if needParseTime>, parseTime</#if> } from '@/utils/ruoyi';
|
||||||
<#if enableExport>
|
<#if enableExport>
|
||||||
import { download as requestDownload } from '@/utils/request';
|
import { download as requestDownload } from '@/utils/request';
|
||||||
</#if>
|
</#if>
|
||||||
|
|||||||
@@ -4,6 +4,3 @@
|
|||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
<mapper namespace="${packageName}.mapper.${ClassName}Mapper">
|
<mapper namespace="${packageName}.mapper.${ClassName}Mapper">
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,16 @@ import cn.hutool.core.util.ObjectUtil;
|
|||||||
import cn.hutool.http.useragent.UserAgent;
|
import cn.hutool.http.useragent.UserAgent;
|
||||||
import cn.hutool.http.useragent.UserAgentUtil;
|
import cn.hutool.http.useragent.UserAgentUtil;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.dromara.common.core.constant.Constants;
|
import org.dromara.common.core.constant.Constants;
|
||||||
import org.dromara.common.core.domain.PageResult;
|
import org.dromara.common.core.domain.PageResult;
|
||||||
import org.dromara.common.core.utils.MapstructUtils;
|
import org.dromara.common.core.utils.MapstructUtils;
|
||||||
import org.dromara.common.core.utils.ServletUtils;
|
|
||||||
import org.dromara.common.core.utils.StringUtils;
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
import org.dromara.common.core.utils.ip.AddressUtils;
|
import org.dromara.common.core.utils.ip.AddressUtils;
|
||||||
import org.dromara.common.log.event.LoginInfoEvent;
|
import org.dromara.common.log.event.LoginInfoEvent;
|
||||||
import org.dromara.common.mybatis.core.mapper.LambdaCrudChainWrapper;
|
import org.dromara.common.mybatis.core.mapper.LambdaCrudChainWrapper;
|
||||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||||
import org.dromara.common.satoken.utils.LoginHelper;
|
|
||||||
import org.dromara.system.domain.SysLoginInfo;
|
import org.dromara.system.domain.SysLoginInfo;
|
||||||
import org.dromara.system.domain.bo.SysLoginInfoBo;
|
import org.dromara.system.domain.bo.SysLoginInfoBo;
|
||||||
import org.dromara.system.domain.vo.SysClientVo;
|
import org.dromara.system.domain.vo.SysClientVo;
|
||||||
@@ -55,11 +52,10 @@ public class SysLoginInfoServiceImpl implements ISysLoginInfoService {
|
|||||||
@Async
|
@Async
|
||||||
@EventListener
|
@EventListener
|
||||||
public void recordLoginInfo(LoginInfoEvent loginInfoEvent) {
|
public void recordLoginInfo(LoginInfoEvent loginInfoEvent) {
|
||||||
HttpServletRequest request = loginInfoEvent.getRequest();
|
UserAgent userAgent = UserAgentUtil.parse(loginInfoEvent.getUserAgent());
|
||||||
UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent"));
|
String ip = loginInfoEvent.getIp();
|
||||||
String ip = ServletUtils.getClientIP(request);
|
|
||||||
// 客户端信息
|
// 客户端信息
|
||||||
String clientId = request.getHeader(LoginHelper.CLIENT_KEY);
|
String clientId = loginInfoEvent.getClientId();
|
||||||
SysClientVo client = null;
|
SysClientVo client = null;
|
||||||
if (StringUtils.isNotBlank(clientId)) {
|
if (StringUtils.isNotBlank(clientId)) {
|
||||||
client = clientService.queryByClientId(clientId);
|
client = clientService.queryByClientId(clientId);
|
||||||
|
|||||||