From 106518c5979ad04d00e1aa991389101d188ce050 Mon Sep 17 00:00:00 2001
From: xuekang <914468783@qq.com>
Date: 星期五, 10 五月 2024 20:42:10 +0800
Subject: [PATCH] 初始化
---
ruoyi-auth/src/main/java/org/dromara/auth/service/impl/EmailAuthStrategy.java | 84 ++
ruoyi-auth/src/main/java/org/dromara/auth/controller/CaptchaController.java | 79 ++
ruoyi-auth/src/main/resources/logback-plus.xml | 28
ruoyi-auth/src/main/java/org/dromara/auth/captcha/UnsignedMathGenerator.java | 88 ++
ruoyi-auth/src/main/java/org/dromara/auth/properties/UserPasswordProperties.java | 29
ruoyi-auth/src/main/java/org/dromara/auth/controller/TokenController.java | 209 +++++
ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SmsAuthStrategy.java | 84 ++
ruoyi-auth/src/main/java/org/dromara/auth/form/PasswordLoginBody.java | 34
ruoyi-auth/Dockerfile | 23
ruoyi-auth/src/main/java/org/dromara/auth/service/impl/XcxAuthStrategy.java | 69 +
ruoyi-auth/src/main/java/org/dromara/auth/service/SysLoginService.java | 257 +++++++
ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/LoginTenantVo.java | 25
ruoyi-auth/src/main/java/org/dromara/auth/service/impl/PasswordAuthStrategy.java | 104 ++
ruoyi-auth/src/main/java/org/dromara/auth/form/SocialLoginBody.java | 36 +
ruoyi-auth/src/main/java/org/dromara/auth/enums/CaptchaCategory.java | 35 +
ruoyi-auth/src/main/java/org/dromara/auth/enums/CaptchaType.java | 29
ruoyi-auth/src/main/resources/banner.txt | 10
ruoyi-auth/pom.xml | 146 ++++
ruoyi-auth/src/main/java/org/dromara/auth/listener/UserActionListener.java | 162 ++++
ruoyi-auth/src/main/java/org/dromara/auth/RuoYiAuthApplication.java | 23
ruoyi-auth/src/main/java/org/dromara/auth/form/RegisterBody.java | 36 +
ruoyi-auth/src/main/java/org/dromara/auth/config/CaptchaConfig.java | 62 +
ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SocialAuthStrategy.java | 109 +++
ruoyi-auth/src/main/java/org/dromara/auth/domain/convert/TenantVoConvert.java | 16
ruoyi-auth/src/main/resources/application.yml | 31
ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/LoginVo.java | 54 +
ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/TenantListVo.java | 19
ruoyi-auth/src/main/java/org/dromara/auth/properties/CaptchaProperties.java | 45 +
ruoyi-auth/src/main/java/org/dromara/auth/form/XcxLoginBody.java | 29
ruoyi-auth/src/main/java/org/dromara/auth/service/IAuthStrategy.java | 35 +
ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/CaptchaVo.java | 25
ruoyi-auth/src/main/java/org/dromara/auth/form/EmailLoginBody.java | 32
ruoyi-auth/src/main/java/org/dromara/auth/form/SmsLoginBody.java | 30
33 files changed, 2,077 insertions(+), 0 deletions(-)
diff --git a/ruoyi-auth/Dockerfile b/ruoyi-auth/Dockerfile
new file mode 100644
index 0000000..f6721d2
--- /dev/null
+++ b/ruoyi-auth/Dockerfile
@@ -0,0 +1,23 @@
+#FROM findepi/graalvm:java17-native
+FROM openjdk:17.0.2-oraclelinux8
+
+MAINTAINER Lion Li
+
+RUN mkdir -p /ruoyi/auth/logs \
+ /ruoyi/auth/temp \
+ /ruoyi/skywalking/agent
+
+WORKDIR /ruoyi/auth
+
+ENV SERVER_PORT=9210 LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS=""
+
+EXPOSE ${SERVER_PORT}
+
+ADD ./target/ruoyi-auth.jar ./app.jar
+
+ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -Dserver.port=${SERVER_PORT} \
+ #-Dskywalking.agent.service_name=ruoyi-auth \
+ #-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar \
+ -jar app.jar \
+ -XX:+HeapDumpOnOutOfMemoryError -Xlog:gc*,:time,tags,level -XX:+UseZGC ${JAVA_OPTS}
+
diff --git a/ruoyi-auth/pom.xml b/ruoyi-auth/pom.xml
new file mode 100644
index 0000000..a09df55
--- /dev/null
+++ b/ruoyi-auth/pom.xml
@@ -0,0 +1,146 @@
+<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-cloud-plus</artifactId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-auth</artifactId>
+
+ <description>
+ ruoyi-auth 璁よ瘉鎺堟潈涓績
+ </description>
+
+ <dependencies>
+
+ <!-- SpringCloud Alibaba Nacos -->
+ <dependency>
+ <groupId>com.alibaba.cloud</groupId>
+ <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+ </dependency>
+
+ <!-- SpringCloud Alibaba Nacos Config -->
+ <dependency>
+ <groupId>com.alibaba.cloud</groupId>
+ <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>cn.hutool</groupId>
+ <artifactId>hutool-captcha</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-sentinel</artifactId>
+ </dependency>
+
+ <!-- RuoYi Common Security-->
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-security</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-social</artifactId>
+ </dependency>
+
+ <!-- RuoYi Common Log -->
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-log</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-doc</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-web</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-ratelimiter</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-encrypt</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-dubbo</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-seata</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-tenant</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-common-mybatis</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.dromara</groupId>
+ <artifactId>ruoyi-api-resource</artifactId>
+ </dependency>
+
+ <!-- 鑷畾涔夎礋杞藉潎琛�(澶氬洟闃熷紑鍙戜娇鐢�) -->
+<!-- <dependency>-->
+<!-- <groupId>org.dromara</groupId>-->
+<!-- <artifactId>ruoyi-common-loadbalancer</artifactId>-->
+<!-- </dependency>-->
+
+ <!-- ELK 鏃ュ織鏀堕泦 -->
+<!-- <dependency>-->
+<!-- <groupId>org.dromara</groupId>-->
+<!-- <artifactId>ruoyi-common-logstash</artifactId>-->
+<!-- </dependency>-->
+
+ <!-- skywalking 鏃ュ織鏀堕泦 -->
+<!-- <dependency>-->
+<!-- <groupId>org.dromara</groupId>-->
+<!-- <artifactId>ruoyi-common-skylog</artifactId>-->
+<!-- </dependency>-->
+
+ <!-- prometheus 鐩戞帶 -->
+<!-- <dependency>-->
+<!-- <groupId>org.dromara</groupId>-->
+<!-- <artifactId>ruoyi-common-prometheus</artifactId>-->
+<!-- </dependency>-->
+
+ </dependencies>
+
+ <build>
+ <finalName>${project.artifactId}</finalName>
+ <plugins>
+ <plugin>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-maven-plugin</artifactId>
+ <version>${spring-boot.version}</version>
+ <executions>
+ <execution>
+ <goals>
+ <goal>repackage</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/RuoYiAuthApplication.java b/ruoyi-auth/src/main/java/org/dromara/auth/RuoYiAuthApplication.java
new file mode 100644
index 0000000..12d4d63
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/RuoYiAuthApplication.java
@@ -0,0 +1,23 @@
+package org.dromara.auth;
+
+import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
+
+/**
+ * 璁よ瘉鎺堟潈涓績
+ *
+ * @author ruoyi
+ */
+@EnableDubbo
+@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
+public class RuoYiAuthApplication {
+ public static void main(String[] args) {
+ SpringApplication application = new SpringApplication(RuoYiAuthApplication.class);
+ application.setApplicationStartup(new BufferingApplicationStartup(2048));
+ application.run(args);
+ System.out.println("(鈾モ棤鈥库棤)锞夛緸 璁よ瘉鎺堟潈涓績鍚姩鎴愬姛 醿�(麓凇`醿�)锞� ");
+ }
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/captcha/UnsignedMathGenerator.java b/ruoyi-auth/src/main/java/org/dromara/auth/captcha/UnsignedMathGenerator.java
new file mode 100644
index 0000000..feb4cdf
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/captcha/UnsignedMathGenerator.java
@@ -0,0 +1,88 @@
+package org.dromara.auth.captcha;
+
+import cn.hutool.captcha.generator.CodeGenerator;
+import cn.hutool.core.math.Calculator;
+import cn.hutool.core.util.CharUtil;
+import cn.hutool.core.util.RandomUtil;
+import org.dromara.common.core.utils.StringUtils;
+
+import java.io.Serial;
+
+/**
+ * 鏃犵鍙疯绠楃敓鎴愬櫒
+ *
+ * @author Lion Li
+ */
+public class UnsignedMathGenerator implements CodeGenerator {
+
+ @Serial
+ private static final long serialVersionUID = -5514819971774091076L;
+
+ private static final String OPERATORS = "+-*";
+
+ /**
+ * 鍙備笌璁$畻鏁板瓧鏈�澶ч暱搴�
+ */
+ private final int numberLength;
+
+ /**
+ * 鏋勯��
+ */
+ public UnsignedMathGenerator() {
+ this(2);
+ }
+
+ /**
+ * 鏋勯��
+ *
+ * @param numberLength 鍙備笌璁$畻鏈�澶ф暟瀛椾綅鏁�
+ */
+ public UnsignedMathGenerator(int numberLength) {
+ this.numberLength = numberLength;
+ }
+
+ @Override
+ public String generate() {
+ final int limit = getLimit();
+ int a = RandomUtil.randomInt(limit);
+ int b = RandomUtil.randomInt(limit);
+ String max = Integer.toString(Math.max(a,b));
+ String min = Integer.toString(Math.min(a,b));
+ max = StringUtils.rightPad(max, this.numberLength, CharUtil.SPACE);
+ min = StringUtils.rightPad(min, this.numberLength, CharUtil.SPACE);
+
+ return max + RandomUtil.randomChar(OPERATORS) + min + '=';
+ }
+
+ @Override
+ public boolean verify(String code, String userInputCode) {
+ int result;
+ try {
+ result = Integer.parseInt(userInputCode);
+ } catch (NumberFormatException e) {
+ // 鐢ㄦ埛杈撳叆闈炴暟瀛�
+ return false;
+ }
+
+ final int calculateResult = (int) Calculator.conversion(code);
+ return result == calculateResult;
+ }
+
+ /**
+ * 鑾峰彇楠岃瘉鐮侀暱搴�
+ *
+ * @return 楠岃瘉鐮侀暱搴�
+ */
+ public int getLength() {
+ return this.numberLength * 2 + 2;
+ }
+
+ /**
+ * 鏍规嵁闀垮害鑾峰彇鍙備笌璁$畻鏁板瓧鏈�澶у��
+ *
+ * @return 鏈�澶у��
+ */
+ private int getLimit() {
+ return Integer.parseInt("1" + StringUtils.repeat('0', this.numberLength));
+ }
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/config/CaptchaConfig.java b/ruoyi-auth/src/main/java/org/dromara/auth/config/CaptchaConfig.java
new file mode 100644
index 0000000..5880016
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/config/CaptchaConfig.java
@@ -0,0 +1,62 @@
+package org.dromara.auth.config;
+
+import cn.hutool.captcha.CaptchaUtil;
+import cn.hutool.captcha.CircleCaptcha;
+import cn.hutool.captcha.LineCaptcha;
+import cn.hutool.captcha.ShearCaptcha;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+
+import java.awt.*;
+
+/**
+ * 楠岃瘉鐮侀厤缃�
+ *
+ * @author Lion Li
+ */
+@Configuration
+public class CaptchaConfig {
+
+ private static final int WIDTH = 160;
+ private static final int HEIGHT = 60;
+ private static final Color BACKGROUND = Color.PINK;
+ private static final Font FONT = new Font("Arial", Font.BOLD, 48);
+
+ /**
+ * 鍦嗗湀骞叉壈楠岃瘉鐮�
+ */
+ @Lazy
+ @Bean
+ public CircleCaptcha circleCaptcha() {
+ CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(WIDTH, HEIGHT);
+ captcha.setBackground(BACKGROUND);
+ captcha.setFont(FONT);
+ return captcha;
+ }
+
+ /**
+ * 绾挎骞叉壈鐨勯獙璇佺爜
+ */
+ @Lazy
+ @Bean
+ public LineCaptcha lineCaptcha() {
+ LineCaptcha captcha = CaptchaUtil.createLineCaptcha(WIDTH, HEIGHT);
+ captcha.setBackground(BACKGROUND);
+ captcha.setFont(FONT);
+ return captcha;
+ }
+
+ /**
+ * 鎵洸骞叉壈楠岃瘉鐮�
+ */
+ @Lazy
+ @Bean
+ public ShearCaptcha shearCaptcha() {
+ ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(WIDTH, HEIGHT);
+ captcha.setBackground(BACKGROUND);
+ captcha.setFont(FONT);
+ return captcha;
+ }
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/controller/CaptchaController.java b/ruoyi-auth/src/main/java/org/dromara/auth/controller/CaptchaController.java
new file mode 100644
index 0000000..63d002c
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/controller/CaptchaController.java
@@ -0,0 +1,79 @@
+package org.dromara.auth.controller;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import cn.hutool.captcha.AbstractCaptcha;
+import cn.hutool.captcha.generator.CodeGenerator;
+import cn.hutool.core.util.IdUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.auth.domain.vo.CaptchaVo;
+import org.dromara.auth.enums.CaptchaType;
+import org.dromara.auth.properties.CaptchaProperties;
+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.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.reflect.ReflectUtils;
+import org.dromara.common.ratelimiter.annotation.RateLimiter;
+import org.dromara.common.ratelimiter.enums.LimitType;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.Duration;
+
+/**
+ * 楠岃瘉鐮佹搷浣滃鐞�
+ *
+ * @author Lion Li
+ */
+@SaIgnore
+@Slf4j
+@Validated
+@RequiredArgsConstructor
+@RestController
+public class CaptchaController {
+
+ private final CaptchaProperties captchaProperties;
+
+ /**
+ * 鐢熸垚楠岃瘉鐮�
+ */
+ @RateLimiter(time = 60, count = 10, limitType = LimitType.IP)
+ @GetMapping("/code")
+ public R<CaptchaVo> getCode() {
+ CaptchaVo captchaVo = new CaptchaVo();
+ boolean captchaEnabled = captchaProperties.getEnabled();
+ if (!captchaEnabled) {
+ captchaVo.setCaptchaEnabled(false);
+ return R.ok(captchaVo);
+ }
+ // 淇濆瓨楠岃瘉鐮佷俊鎭�
+ String uuid = IdUtil.simpleUUID();
+ String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
+ // 鐢熸垚楠岃瘉鐮�
+ CaptchaType captchaType = captchaProperties.getType();
+ boolean isMath = CaptchaType.MATH == captchaType;
+ Integer length = isMath ? captchaProperties.getNumberLength() : captchaProperties.getCharLength();
+ CodeGenerator codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), length);
+ AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz());
+ captcha.setGenerator(codeGenerator);
+ captcha.createCode();
+ String code = captcha.getCode();
+ if (isMath) {
+ ExpressionParser parser = new SpelExpressionParser();
+ Expression exp = parser.parseExpression(StringUtils.remove(code, "="));
+ code = exp.getValue(String.class);
+ }
+ RedisUtils.setCacheObject(verifyKey, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
+ captchaVo.setUuid(uuid);
+ captchaVo.setImg(captcha.getImageBase64());
+ return R.ok(captchaVo);
+ }
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/controller/TokenController.java b/ruoyi-auth/src/main/java/org/dromara/auth/controller/TokenController.java
new file mode 100644
index 0000000..6424a6a
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/controller/TokenController.java
@@ -0,0 +1,209 @@
+package org.dromara.auth.controller;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.zhyd.oauth.model.AuthResponse;
+import me.zhyd.oauth.model.AuthUser;
+import me.zhyd.oauth.request.AuthRequest;
+import me.zhyd.oauth.utils.AuthStateUtils;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.auth.domain.vo.LoginTenantVo;
+import org.dromara.auth.domain.vo.LoginVo;
+import org.dromara.auth.domain.vo.TenantListVo;
+import org.dromara.auth.form.RegisterBody;
+import org.dromara.auth.form.SocialLoginBody;
+import org.dromara.auth.service.IAuthStrategy;
+import org.dromara.auth.service.SysLoginService;
+import org.dromara.common.core.constant.UserConstants;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.domain.model.LoginBody;
+import org.dromara.common.core.utils.*;
+import org.dromara.common.encrypt.annotation.ApiEncrypt;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
+import org.dromara.common.social.config.properties.SocialProperties;
+import org.dromara.common.social.utils.SocialUtils;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.resource.api.RemoteMessageService;
+import org.dromara.system.api.RemoteClientService;
+import org.dromara.system.api.RemoteConfigService;
+import org.dromara.system.api.RemoteSocialService;
+import org.dromara.system.api.RemoteTenantService;
+import org.dromara.system.api.domain.vo.RemoteClientVo;
+import org.dromara.system.api.domain.vo.RemoteTenantVo;
+import org.springframework.web.bind.annotation.*;
+
+import java.net.URL;
+import java.util.List;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * token 鎺у埗
+ *
+ * @author Lion Li
+ */
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+public class TokenController {
+
+ private final SocialProperties socialProperties;
+ private final SysLoginService sysLoginService;
+ private final ScheduledExecutorService scheduledExecutorService;
+
+ @DubboReference
+ private final RemoteConfigService remoteConfigService;
+ @DubboReference
+ private final RemoteTenantService remoteTenantService;
+ @DubboReference
+ private final RemoteClientService remoteClientService;
+ @DubboReference
+ private final RemoteSocialService remoteSocialService;
+ @DubboReference(stub = "true")
+ private final RemoteMessageService remoteMessageService;
+
+ /**
+ * 鐧诲綍鏂规硶
+ *
+ * @param body 鐧诲綍淇℃伅
+ * @return 缁撴灉
+ */
+ @ApiEncrypt
+ @PostMapping("/login")
+ public R<LoginVo> login(@RequestBody String body) {
+ LoginBody loginBody = JsonUtils.parseObject(body, LoginBody.class);
+ ValidatorUtils.validate(loginBody);
+ // 鎺堟潈绫诲瀷鍜屽鎴风id
+ String clientId = loginBody.getClientId();
+ String grantType = loginBody.getGrantType();
+ RemoteClientVo clientVo = remoteClientService.queryByClientId(clientId);
+
+ // 鏌ヨ涓嶅埌 client 鎴� client 鍐呬笉鍖呭惈 grantType
+ if (ObjectUtil.isNull(clientVo) || !StringUtils.contains(clientVo.getGrantType(), grantType)) {
+ log.info("瀹㈡埛绔痠d: {} 璁よ瘉绫诲瀷锛歿} 寮傚父!.", clientId, grantType);
+ return R.fail(MessageUtils.message("auth.grant.type.error"));
+ } else if (!UserConstants.NORMAL.equals(clientVo.getStatus())) {
+ return R.fail(MessageUtils.message("auth.grant.type.blocked"));
+ }
+ // 鏍¢獙绉熸埛
+ sysLoginService.checkTenant(loginBody.getTenantId());
+ // 鐧诲綍
+ LoginVo loginVo = IAuthStrategy.login(body, clientVo, grantType);
+
+ Long userId = LoginHelper.getUserId();
+ scheduledExecutorService.schedule(() -> {
+ try {
+ remoteMessageService.sendMessage(userId, "娆㈣繋鐧诲綍RuoYi-Cloud-Plus寰湇鍔$鐞嗙郴缁�");
+ } catch (Exception ignored) {
+ }
+ }, 3, TimeUnit.SECONDS);
+ return R.ok(loginVo);
+ }
+
+ /**
+ * 绗笁鏂圭櫥褰曡姹�
+ *
+ * @param source 鐧诲綍鏉ユ簮
+ * @return 缁撴灉
+ */
+ @GetMapping("/binding/{source}")
+ public R<String> authBinding(@PathVariable("source") String source) {
+ SocialLoginConfigProperties obj = socialProperties.getType().get(source);
+ if (ObjectUtil.isNull(obj)) {
+ return R.fail(source + "骞冲彴璐﹀彿鏆備笉鏀寔");
+ }
+ AuthRequest authRequest = SocialUtils.getAuthRequest(source, socialProperties);
+ String authorizeUrl = authRequest.authorize(AuthStateUtils.createState());
+ return R.ok("鎿嶄綔鎴愬姛", authorizeUrl);
+ }
+
+ /**
+ * 绗笁鏂圭櫥褰曞洖璋冧笟鍔″鐞� 缁戝畾鎺堟潈
+ *
+ * @param loginBody 璇锋眰浣�
+ * @return 缁撴灉
+ */
+ @PostMapping("/social/callback")
+ public R<Void> socialCallback(@RequestBody SocialLoginBody loginBody) {
+ // 鑾峰彇绗笁鏂圭櫥褰曚俊鎭�
+ AuthResponse<AuthUser> response = SocialUtils.loginAuth(
+ loginBody.getSource(), loginBody.getSocialCode(),
+ loginBody.getSocialState(), socialProperties);
+ AuthUser authUserData = response.getData();
+ // 鍒ゆ柇鎺堟潈鍝嶅簲鏄惁鎴愬姛
+ if (!response.ok()) {
+ return R.fail(response.getMsg());
+ }
+ sysLoginService.socialRegister(authUserData);
+ return R.ok();
+ }
+
+
+ /**
+ * 鍙栨秷鎺堟潈
+ *
+ * @param socialId socialId
+ */
+ @DeleteMapping(value = "/unlock/{socialId}")
+ public R<Void> unlockSocial(@PathVariable Long socialId) {
+ Boolean rows = remoteSocialService.deleteWithValidById(socialId);
+ return rows ? R.ok() : R.fail("鍙栨秷鎺堟潈澶辫触");
+ }
+
+ /**
+ * 鐧诲嚭鏂规硶
+ */
+ @PostMapping("logout")
+ public R<Void> logout() {
+ sysLoginService.logout();
+ return R.ok();
+ }
+
+ /**
+ * 鐢ㄦ埛娉ㄥ唽
+ */
+ @ApiEncrypt
+ @PostMapping("register")
+ public R<Void> register(@RequestBody RegisterBody registerBody) {
+ if (!remoteConfigService.selectRegisterEnabled(registerBody.getTenantId())) {
+ return R.fail("褰撳墠绯荤粺娌℃湁寮�鍚敞鍐屽姛鑳斤紒");
+ }
+ // 鐢ㄦ埛娉ㄥ唽
+ sysLoginService.register(registerBody);
+ return R.ok();
+ }
+
+ /**
+ * 鐧诲綍椤甸潰绉熸埛涓嬫媺妗�
+ *
+ * @return 绉熸埛鍒楄〃
+ */
+ @GetMapping("/tenant/list")
+ public R<LoginTenantVo> tenantList(HttpServletRequest request) throws Exception {
+ List<RemoteTenantVo> tenantList = remoteTenantService.queryList();
+ List<TenantListVo> voList = MapstructUtils.convert(tenantList, TenantListVo.class);
+ // 鑾峰彇鍩熷悕
+ String host;
+ String referer = request.getHeader("referer");
+ if (StringUtils.isNotBlank(referer)) {
+ // 杩欓噷浠巖eferer涓彇鍊兼槸涓轰簡鏈湴浣跨敤hosts娣诲姞铏氭嫙鍩熷悕锛屾柟渚挎湰鍦扮幆澧冭皟璇�
+ host = referer.split("//")[1].split("/")[0];
+ } else {
+ host = new URL(request.getRequestURL().toString()).getHost();
+ }
+ // 鏍规嵁鍩熷悕杩涜绛涢��
+ List<TenantListVo> list = StreamUtils.filter(voList, vo ->
+ StringUtils.equals(vo.getDomain(), host));
+ // 杩斿洖瀵硅薄
+ LoginTenantVo vo = new LoginTenantVo();
+ vo.setVoList(CollUtil.isNotEmpty(list) ? list : voList);
+ vo.setTenantEnabled(TenantHelper.isEnable());
+ return R.ok(vo);
+ }
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/domain/convert/TenantVoConvert.java b/ruoyi-auth/src/main/java/org/dromara/auth/domain/convert/TenantVoConvert.java
new file mode 100644
index 0000000..7888f2c
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/domain/convert/TenantVoConvert.java
@@ -0,0 +1,16 @@
+package org.dromara.auth.domain.convert;
+
+import io.github.linpeilie.BaseMapper;
+import org.dromara.auth.domain.vo.TenantListVo;
+import org.dromara.system.api.domain.vo.RemoteTenantVo;
+import org.mapstruct.Mapper;
+import org.mapstruct.MappingConstants;
+
+/**
+ * 绉熸埛vo杞崲鍣�
+ * @author zhujie
+ */
+@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
+public interface TenantVoConvert extends BaseMapper<RemoteTenantVo, TenantListVo> {
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/CaptchaVo.java b/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/CaptchaVo.java
new file mode 100644
index 0000000..2a4c0bd
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/CaptchaVo.java
@@ -0,0 +1,25 @@
+package org.dromara.auth.domain.vo;
+
+import lombok.Data;
+
+/**
+ * 楠岃瘉鐮佷俊鎭�
+ *
+ * @author Michelle.Chung
+ */
+@Data
+public class CaptchaVo {
+
+ /**
+ * 鏄惁寮�鍚獙璇佺爜
+ */
+ private Boolean captchaEnabled = true;
+
+ private String uuid;
+
+ /**
+ * 楠岃瘉鐮佸浘鐗�
+ */
+ private String img;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/LoginTenantVo.java b/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/LoginTenantVo.java
new file mode 100644
index 0000000..fcfdcc3
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/LoginTenantVo.java
@@ -0,0 +1,25 @@
+package org.dromara.auth.domain.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 鐧诲綍绉熸埛瀵硅薄
+ *
+ * @author Michelle.Chung
+ */
+@Data
+public class LoginTenantVo {
+
+ /**
+ * 绉熸埛寮�鍏�
+ */
+ private Boolean tenantEnabled;
+
+ /**
+ * 绉熸埛瀵硅薄鍒楄〃
+ */
+ private List<TenantListVo> voList;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/LoginVo.java b/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/LoginVo.java
new file mode 100644
index 0000000..e4bea14
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/LoginVo.java
@@ -0,0 +1,54 @@
+package org.dromara.auth.domain.vo;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * 鐧诲綍楠岃瘉淇℃伅
+ *
+ * @author Michelle.Chung
+ */
+@Data
+public class LoginVo {
+
+ /**
+ * 鎺堟潈浠ょ墝
+ */
+ @JsonProperty("access_token")
+ private String accessToken;
+
+ /**
+ * 鍒锋柊浠ょ墝
+ */
+ @JsonProperty("refresh_token")
+ private String refreshToken;
+
+ /**
+ * 鎺堟潈浠ょ墝 access_token 鐨勬湁鏁堟湡
+ */
+ @JsonProperty("expire_in")
+ private Long expireIn;
+
+ /**
+ * 鍒锋柊浠ょ墝 refresh_token 鐨勬湁鏁堟湡
+ */
+ @JsonProperty("refresh_expire_in")
+ private Long refreshExpireIn;
+
+ /**
+ * 搴旂敤id
+ */
+ @JsonProperty("client_id")
+ private String clientId;
+
+ /**
+ * 浠ょ墝鏉冮檺
+ */
+ private String scope;
+
+ /**
+ * 鐢ㄦ埛 openid
+ */
+ private String openid;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/TenantListVo.java b/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/TenantListVo.java
new file mode 100644
index 0000000..b993b37
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/domain/vo/TenantListVo.java
@@ -0,0 +1,19 @@
+package org.dromara.auth.domain.vo;
+
+import lombok.Data;
+
+/**
+ * 绉熸埛鍒楄〃
+ *
+ * @author zhujie
+ */
+@Data
+public class TenantListVo {
+
+ private String tenantId;
+
+ private String companyName;
+
+ private String domain;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/enums/CaptchaCategory.java b/ruoyi-auth/src/main/java/org/dromara/auth/enums/CaptchaCategory.java
new file mode 100644
index 0000000..b387aed
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/enums/CaptchaCategory.java
@@ -0,0 +1,35 @@
+package org.dromara.auth.enums;
+
+import cn.hutool.captcha.AbstractCaptcha;
+import cn.hutool.captcha.CircleCaptcha;
+import cn.hutool.captcha.LineCaptcha;
+import cn.hutool.captcha.ShearCaptcha;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 楠岃瘉鐮佺被鍒�
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum CaptchaCategory {
+
+ /**
+ * 绾挎骞叉壈
+ */
+ LINE(LineCaptcha.class),
+
+ /**
+ * 鍦嗗湀骞叉壈
+ */
+ CIRCLE(CircleCaptcha.class),
+
+ /**
+ * 鎵洸骞叉壈
+ */
+ SHEAR(ShearCaptcha.class);
+
+ private final Class<? extends AbstractCaptcha> clazz;
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/enums/CaptchaType.java b/ruoyi-auth/src/main/java/org/dromara/auth/enums/CaptchaType.java
new file mode 100644
index 0000000..b663345
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/enums/CaptchaType.java
@@ -0,0 +1,29 @@
+package org.dromara.auth.enums;
+
+import cn.hutool.captcha.generator.CodeGenerator;
+import cn.hutool.captcha.generator.RandomGenerator;
+import org.dromara.auth.captcha.UnsignedMathGenerator;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 楠岃瘉鐮佺被鍨�
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum CaptchaType {
+
+ /**
+ * 鏁板瓧
+ */
+ MATH(UnsignedMathGenerator.class),
+
+ /**
+ * 瀛楃
+ */
+ CHAR(RandomGenerator.class);
+
+ private final Class<? extends CodeGenerator> clazz;
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/form/EmailLoginBody.java b/ruoyi-auth/src/main/java/org/dromara/auth/form/EmailLoginBody.java
new file mode 100644
index 0000000..931e236
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/form/EmailLoginBody.java
@@ -0,0 +1,32 @@
+package org.dromara.auth.form;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.domain.model.LoginBody;
+
+/**
+ * 閭欢鐧诲綍瀵硅薄
+ *
+ * @author Lion Li
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class EmailLoginBody extends LoginBody {
+
+ /**
+ * 閭
+ */
+ @NotBlank(message = "{user.email.not.blank}")
+ @Email(message = "{user.email.not.valid}")
+ private String email;
+
+ /**
+ * 閭code
+ */
+ @NotBlank(message = "{email.code.not.blank}")
+ private String emailCode;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/form/PasswordLoginBody.java b/ruoyi-auth/src/main/java/org/dromara/auth/form/PasswordLoginBody.java
new file mode 100644
index 0000000..edb7ab2
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/form/PasswordLoginBody.java
@@ -0,0 +1,34 @@
+package org.dromara.auth.form;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.domain.model.LoginBody;
+import org.hibernate.validator.constraints.Length;
+
+import static org.dromara.common.core.constant.UserConstants.*;
+
+/**
+ * 瀵嗙爜鐧诲綍瀵硅薄
+ *
+ * @author Lion Li
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class PasswordLoginBody extends LoginBody {
+
+ /**
+ * 鐢ㄦ埛鍚�
+ */
+ @NotBlank(message = "{user.username.not.blank}")
+ @Length(min = USERNAME_MIN_LENGTH, max = USERNAME_MAX_LENGTH, message = "{user.username.length.valid}")
+ private String username;
+
+ /**
+ * 鐢ㄦ埛瀵嗙爜
+ */
+ @NotBlank(message = "{user.password.not.blank}")
+ @Length(min = PASSWORD_MIN_LENGTH, max = PASSWORD_MAX_LENGTH, message = "{user.password.length.valid}")
+ private String password;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/form/RegisterBody.java b/ruoyi-auth/src/main/java/org/dromara/auth/form/RegisterBody.java
new file mode 100644
index 0000000..386c0fc
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/form/RegisterBody.java
@@ -0,0 +1,36 @@
+package org.dromara.auth.form;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.domain.model.LoginBody;
+import org.hibernate.validator.constraints.Length;
+
+import static org.dromara.common.core.constant.UserConstants.*;
+
+/**
+ * 鐢ㄦ埛娉ㄥ唽瀵硅薄
+ *
+ * @author Lion Li
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class RegisterBody extends LoginBody {
+
+ /**
+ * 鐢ㄦ埛鍚�
+ */
+ @NotBlank(message = "{user.username.not.blank}")
+ @Length(min = USERNAME_MIN_LENGTH, max = USERNAME_MAX_LENGTH, message = "{user.username.length.valid}")
+ private String username;
+
+ /**
+ * 鐢ㄦ埛瀵嗙爜
+ */
+ @NotBlank(message = "{user.password.not.blank}")
+ @Length(min = PASSWORD_MIN_LENGTH, max = PASSWORD_MAX_LENGTH, message = "{user.password.length.valid}")
+ private String password;
+
+ private String userType;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/form/SmsLoginBody.java b/ruoyi-auth/src/main/java/org/dromara/auth/form/SmsLoginBody.java
new file mode 100644
index 0000000..48e262f
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/form/SmsLoginBody.java
@@ -0,0 +1,30 @@
+package org.dromara.auth.form;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.domain.model.LoginBody;
+
+/**
+ * 鐭俊鐧诲綍瀵硅薄
+ *
+ * @author Lion Li
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SmsLoginBody extends LoginBody {
+
+ /**
+ * 鎵嬫満鍙�
+ */
+ @NotBlank(message = "{user.phonenumber.not.blank}")
+ private String phonenumber;
+
+ /**
+ * 鐭俊code
+ */
+ @NotBlank(message = "{sms.code.not.blank}")
+ private String smsCode;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/form/SocialLoginBody.java b/ruoyi-auth/src/main/java/org/dromara/auth/form/SocialLoginBody.java
new file mode 100644
index 0000000..cbd61c9
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/form/SocialLoginBody.java
@@ -0,0 +1,36 @@
+package org.dromara.auth.form;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.domain.model.LoginBody;
+
+/**
+ * 涓夋柟鐧诲綍瀵硅薄
+ *
+ * @author Lion Li
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SocialLoginBody extends LoginBody {
+
+ /**
+ * 绗笁鏂圭櫥褰曞钩鍙�
+ */
+ @NotBlank(message = "{social.source.not.blank}")
+ private String source;
+
+ /**
+ * 绗笁鏂圭櫥褰昪ode
+ */
+ @NotBlank(message = "{social.code.not.blank}")
+ private String socialCode;
+
+ /**
+ * 绗笁鏂圭櫥褰晄ocialState
+ */
+ @NotBlank(message = "{social.state.not.blank}")
+ private String socialState;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/form/XcxLoginBody.java b/ruoyi-auth/src/main/java/org/dromara/auth/form/XcxLoginBody.java
new file mode 100644
index 0000000..c68306c
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/form/XcxLoginBody.java
@@ -0,0 +1,29 @@
+package org.dromara.auth.form;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.domain.model.LoginBody;
+
+/**
+ * 涓夋柟鐧诲綍瀵硅薄
+ *
+ * @author Lion Li
+ */
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class XcxLoginBody extends LoginBody {
+
+ /**
+ * 灏忕▼搴廼d(澶氫釜灏忕▼搴忔椂浣跨敤)
+ */
+ private String appid;
+
+ /**
+ * 灏忕▼搴廲ode
+ */
+ @NotBlank(message = "{xcx.code.not.blank}")
+ private String xcxCode;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/listener/UserActionListener.java b/ruoyi-auth/src/main/java/org/dromara/auth/listener/UserActionListener.java
new file mode 100644
index 0000000..86df42c
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/listener/UserActionListener.java
@@ -0,0 +1,162 @@
+package org.dromara.auth.listener;
+
+import cn.dev33.satoken.config.SaTokenConfig;
+import cn.dev33.satoken.listener.SaTokenListener;
+import cn.dev33.satoken.stp.SaLoginModel;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.http.useragent.UserAgent;
+import cn.hutool.http.useragent.UserAgentUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.common.core.constant.CacheConstants;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.ServletUtils;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.ip.AddressUtils;
+import org.dromara.common.log.event.LogininforEvent;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.resource.api.RemoteMessageService;
+import org.dromara.system.api.RemoteUserService;
+import org.dromara.system.api.domain.SysUserOnline;
+import org.dromara.system.api.model.LoginUser;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * 鐢ㄦ埛琛屼负 渚﹀惉鍣ㄧ殑瀹炵幇
+ *
+ * @author Lion Li
+ */
+@RequiredArgsConstructor
+@Component
+@Slf4j
+public class UserActionListener implements SaTokenListener {
+
+ private final SaTokenConfig tokenConfig;
+ private final ScheduledExecutorService scheduledExecutorService;
+ @DubboReference
+ private RemoteUserService remoteUserService;
+ @DubboReference
+ private RemoteMessageService remoteMessageService;
+
+ /**
+ * 姣忔鐧诲綍鏃惰Е鍙�
+ */
+ @Override
+ public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
+ UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
+ String ip = ServletUtils.getClientIP();
+ LoginUser user = LoginHelper.getLoginUser();
+ SysUserOnline userOnline = new SysUserOnline();
+ userOnline.setIpaddr(ip);
+ userOnline.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
+ userOnline.setBrowser(userAgent.getBrowser().getName());
+ userOnline.setOs(userAgent.getOs().getName());
+ userOnline.setLoginTime(System.currentTimeMillis());
+ userOnline.setTokenId(tokenValue);
+ userOnline.setUserName(user.getUsername());
+ userOnline.setClientKey(user.getClientKey());
+ userOnline.setDeviceType(user.getDeviceType());
+ if (ObjectUtil.isNotNull(user.getDeptName())) {
+ userOnline.setDeptName(user.getDeptName());
+ }
+ if (tokenConfig.getTimeout() == -1) {
+ RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, userOnline);
+ } else {
+ RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, userOnline, Duration.ofSeconds(tokenConfig.getTimeout()));
+ }
+ // 璁板綍鐧诲綍鏃ュ織
+ LogininforEvent logininforEvent = new LogininforEvent();
+ logininforEvent.setTenantId(user.getTenantId());
+ logininforEvent.setUsername(user.getUsername());
+ logininforEvent.setStatus(Constants.LOGIN_SUCCESS);
+ logininforEvent.setMessage(MessageUtils.message("user.login.success"));
+ logininforEvent.setRequest(ServletUtils.getRequest());
+ SpringUtils.context().publishEvent(logininforEvent);
+ // 鏇存柊鐧诲綍淇℃伅
+ remoteUserService.recordLoginInfo(user.getUserId(), ServletUtils.getClientIP());
+ log.info("user doLogin, useId:{}, token:{}", loginId, tokenValue);
+ }
+
+ /**
+ * 姣忔娉ㄩ攢鏃惰Е鍙�
+ */
+ @Override
+ public void doLogout(String loginType, Object loginId, String tokenValue) {
+ RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+ log.info("user doLogout, useId:{}, token:{}", loginId, tokenValue);
+ }
+
+ /**
+ * 姣忔琚涪涓嬬嚎鏃惰Е鍙�
+ */
+ @Override
+ public void doKickout(String loginType, Object loginId, String tokenValue) {
+ RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+ log.info("user doLogoutByLoginId, useId:{}, token:{}", loginId, tokenValue);
+ }
+
+ /**
+ * 姣忔琚《涓嬬嚎鏃惰Е鍙�
+ */
+ @Override
+ public void doReplaced(String loginType, Object loginId, String tokenValue) {
+ RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
+ log.info("user doReplaced, useId:{}, token:{}", loginId, tokenValue);
+ }
+
+ /**
+ * 姣忔琚皝绂佹椂瑙﹀彂
+ */
+ @Override
+ public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {
+ }
+
+ /**
+ * 姣忔琚В灏佹椂瑙﹀彂
+ */
+ @Override
+ public void doUntieDisable(String loginType, Object loginId, String service) {
+ }
+
+ /**
+ * 姣忔鎵撳紑浜岀骇璁よ瘉鏃惰Е鍙�
+ */
+ @Override
+ public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {
+ }
+
+ /**
+ * 姣忔鍒涘缓Session鏃惰Е鍙�
+ */
+ @Override
+ public void doCloseSafe(String loginType, String tokenValue, String service) {
+ }
+
+ /**
+ * 姣忔鍒涘缓Session鏃惰Е鍙�
+ */
+ @Override
+ public void doCreateSession(String id) {
+ }
+
+ /**
+ * 姣忔娉ㄩ攢Session鏃惰Е鍙�
+ */
+ @Override
+ public void doLogoutSession(String id) {
+ }
+
+ /**
+ * 姣忔Token缁湡鏃惰Е鍙�
+ */
+ @Override
+ public void doRenewTimeout(String tokenValue, Object loginId, long timeout) {
+ }
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/properties/CaptchaProperties.java b/ruoyi-auth/src/main/java/org/dromara/auth/properties/CaptchaProperties.java
new file mode 100644
index 0000000..1cd7098
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/properties/CaptchaProperties.java
@@ -0,0 +1,45 @@
+package org.dromara.auth.properties;
+
+import org.dromara.auth.enums.CaptchaCategory;
+import org.dromara.auth.enums.CaptchaType;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 楠岃瘉鐮侀厤缃�
+ *
+ * @author ruoyi
+ */
+@Data
+@Configuration
+@RefreshScope
+@ConfigurationProperties(prefix = "security.captcha")
+public class CaptchaProperties {
+ /**
+ * 楠岃瘉鐮佺被鍨�
+ */
+ private CaptchaType type;
+
+ /**
+ * 楠岃瘉鐮佺被鍒�
+ */
+ private CaptchaCategory category;
+
+ /**
+ * 鏁板瓧楠岃瘉鐮佷綅鏁�
+ */
+ private Integer numberLength;
+
+ /**
+ * 瀛楃楠岃瘉鐮侀暱搴�
+ */
+ private Integer charLength;
+
+ /**
+ * 楠岃瘉鐮佸紑鍏�
+ */
+ private Boolean enabled;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/properties/UserPasswordProperties.java b/ruoyi-auth/src/main/java/org/dromara/auth/properties/UserPasswordProperties.java
new file mode 100644
index 0000000..5960d71
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/properties/UserPasswordProperties.java
@@ -0,0 +1,29 @@
+package org.dromara.auth.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 鐢ㄦ埛瀵嗙爜閰嶇疆
+ *
+ * @author Lion Li
+ */
+@Data
+@Configuration
+@RefreshScope
+@ConfigurationProperties(prefix = "user.password")
+public class UserPasswordProperties {
+
+ /**
+ * 瀵嗙爜鏈�澶ч敊璇鏁�
+ */
+ private Integer maxRetryCount;
+
+ /**
+ * 瀵嗙爜閿佸畾鏃堕棿锛堥粯璁�10鍒嗛挓锛�
+ */
+ private Integer lockTime;
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/service/IAuthStrategy.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/IAuthStrategy.java
new file mode 100644
index 0000000..0bc3657
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/IAuthStrategy.java
@@ -0,0 +1,35 @@
+package org.dromara.auth.service;
+
+import org.dromara.auth.domain.vo.LoginVo;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.system.api.domain.vo.RemoteClientVo;
+
+/**
+ * 鎺堟潈绛栫暐
+ *
+ * @author Michelle.Chung
+ */
+public interface IAuthStrategy {
+
+ String BASE_NAME = "AuthStrategy";
+
+ /**
+ * 鐧诲綍
+ */
+ static LoginVo login(String body, RemoteClientVo client, String grantType) {
+ // 鎺堟潈绫诲瀷鍜屽鎴风id
+ String beanName = grantType + BASE_NAME;
+ if (!SpringUtils.containsBean(beanName)) {
+ throw new ServiceException("鎺堟潈绫诲瀷涓嶆纭�!");
+ }
+ IAuthStrategy instance = SpringUtils.getBean(beanName);
+ return instance.login(body, client);
+ }
+
+ /**
+ * 鐧诲綍
+ */
+ LoginVo login(String body, RemoteClientVo client);
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/service/SysLoginService.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/SysLoginService.java
new file mode 100644
index 0000000..eb2d7e6
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/SysLoginService.java
@@ -0,0 +1,257 @@
+package org.dromara.auth.service;
+
+import cn.dev33.satoken.exception.NotLoginException;
+import cn.dev33.satoken.secure.BCrypt;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.zhyd.oauth.model.AuthUser;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.auth.form.RegisterBody;
+import org.dromara.auth.properties.CaptchaProperties;
+import org.dromara.auth.properties.UserPasswordProperties;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.constant.TenantConstants;
+import org.dromara.common.core.enums.LoginType;
+import org.dromara.common.core.enums.TenantStatus;
+import org.dromara.common.core.enums.UserType;
+import org.dromara.common.core.exception.user.CaptchaException;
+import org.dromara.common.core.exception.user.CaptchaExpireException;
+import org.dromara.common.core.exception.user.UserException;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.ServletUtils;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.log.event.LogininforEvent;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.tenant.exception.TenantException;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.system.api.RemoteSocialService;
+import org.dromara.system.api.RemoteTenantService;
+import org.dromara.system.api.RemoteUserService;
+import org.dromara.system.api.domain.bo.RemoteSocialBo;
+import org.dromara.system.api.domain.bo.RemoteUserBo;
+import org.dromara.system.api.domain.vo.RemoteSocialVo;
+import org.dromara.system.api.domain.vo.RemoteTenantVo;
+import org.dromara.system.api.model.LoginUser;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.Duration;
+import java.util.Date;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * 鐧诲綍鏍¢獙鏂规硶
+ *
+ * @author ruoyi
+ */
+@RequiredArgsConstructor
+@Service
+@Slf4j
+public class SysLoginService {
+
+ @DubboReference
+ private RemoteUserService remoteUserService;
+ @DubboReference
+ private RemoteTenantService remoteTenantService;
+ @DubboReference
+ private RemoteSocialService remoteSocialService;
+
+ @Autowired
+ private UserPasswordProperties userPasswordProperties;
+ @Autowired
+ private final CaptchaProperties captchaProperties;
+
+ /**
+ * 缁戝畾绗笁鏂圭敤鎴�
+ *
+ * @param authUserData 鎺堟潈鍝嶅簲瀹炰綋
+ */
+ public void socialRegister(AuthUser authUserData) {
+ String authId = authUserData.getSource() + authUserData.getUuid();
+ // 绗笁鏂圭敤鎴蜂俊鎭�
+ RemoteSocialBo bo = BeanUtil.toBean(authUserData, RemoteSocialBo.class);
+ BeanUtil.copyProperties(authUserData.getToken(), bo);
+ bo.setUserId(LoginHelper.getUserId());
+ bo.setAuthId(authId);
+ bo.setOpenId(authUserData.getUuid());
+ bo.setUserName(authUserData.getUsername());
+ bo.setNickName(authUserData.getNickname());
+ // 鏌ヨ鏄惁宸茬粡缁戝畾鐢ㄦ埛
+ List<RemoteSocialVo> list = remoteSocialService.selectByAuthId(authId);
+ if (CollUtil.isEmpty(list)) {
+ // 娌℃湁缁戝畾鐢ㄦ埛, 鏂板鐢ㄦ埛淇℃伅
+ remoteSocialService.insertByBo(bo);
+ } else {
+ // 鏇存柊鐢ㄦ埛淇℃伅
+ bo.setId(list.get(0).getId());
+ remoteSocialService.updateByBo(bo);
+ }
+ }
+
+ /**
+ * 閫�鍑虹櫥褰�
+ */
+ public void logout() {
+ try {
+ LoginUser loginUser = LoginHelper.getLoginUser();
+ if (ObjectUtil.isNull(loginUser)) {
+ return;
+ }
+ if (TenantHelper.isEnable() && LoginHelper.isSuperAdmin()) {
+ // 瓒呯骇绠$悊鍛� 鐧诲嚭娓呴櫎鍔ㄦ�佺鎴�
+ TenantHelper.clearDynamic();
+ }
+ recordLogininfor(loginUser.getTenantId(), loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success"));
+ } catch (NotLoginException ignored) {
+ } finally {
+ try {
+ StpUtil.logout();
+ } catch (NotLoginException ignored) {
+ }
+ }
+ }
+
+ /**
+ * 娉ㄥ唽
+ */
+ public void register(RegisterBody registerBody) {
+ String tenantId = registerBody.getTenantId();
+ String username = registerBody.getUsername();
+ String password = registerBody.getPassword();
+ // 鏍¢獙鐢ㄦ埛绫诲瀷鏄惁瀛樺湪
+ String userType = UserType.getUserType(registerBody.getUserType()).getUserType();
+
+ boolean captchaEnabled = captchaProperties.getEnabled();
+ // 楠岃瘉鐮佸紑鍏�
+ if (captchaEnabled) {
+ validateCaptcha(tenantId, username, registerBody.getCode(), registerBody.getUuid());
+ }
+
+ // 娉ㄥ唽鐢ㄦ埛淇℃伅
+ RemoteUserBo remoteUserBo = new RemoteUserBo();
+ remoteUserBo.setTenantId(tenantId);
+ remoteUserBo.setUserName(username);
+ remoteUserBo.setNickName(username);
+ remoteUserBo.setPassword(BCrypt.hashpw(password));
+ remoteUserBo.setUserType(userType);
+
+ boolean regFlag = remoteUserService.registerUserInfo(remoteUserBo);
+ if (!regFlag) {
+ throw new UserException("user.register.error");
+ }
+ recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.register.success"));
+ }
+
+ /**
+ * 鏍¢獙楠岃瘉鐮�
+ *
+ * @param username 鐢ㄦ埛鍚�
+ * @param code 楠岃瘉鐮�
+ * @param uuid 鍞竴鏍囪瘑
+ */
+ public void validateCaptcha(String tenantId, String username, String code, String uuid) {
+ String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.defaultString(uuid, "");
+ String captcha = RedisUtils.getCacheObject(verifyKey);
+ RedisUtils.deleteObject(verifyKey);
+ if (captcha == null) {
+ recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.jcaptcha.expire"));
+ throw new CaptchaExpireException();
+ }
+ if (!code.equalsIgnoreCase(captcha)) {
+ recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.jcaptcha.error"));
+ throw new CaptchaException();
+ }
+ }
+
+ /**
+ * 璁板綍鐧诲綍淇℃伅
+ *
+ * @param username 鐢ㄦ埛鍚�
+ * @param status 鐘舵��
+ * @param message 娑堟伅鍐呭
+ * @return
+ */
+ public void recordLogininfor(String tenantId, String username, String status, String message) {
+ // 灏佽瀵硅薄
+ LogininforEvent logininforEvent = new LogininforEvent();
+ logininforEvent.setTenantId(tenantId);
+ logininforEvent.setUsername(username);
+ logininforEvent.setStatus(status);
+ logininforEvent.setMessage(message);
+ logininforEvent.setRequest(ServletUtils.getRequest());
+ SpringUtils.context().publishEvent(logininforEvent);
+ }
+
+ /**
+ * 鐧诲綍鏍¢獙
+ */
+ public void checkLogin(LoginType loginType, String tenantId, String username, Supplier<Boolean> supplier) {
+ String errorKey = GlobalConstants.PWD_ERR_CNT_KEY + username;
+ String loginFail = Constants.LOGIN_FAIL;
+ Integer maxRetryCount = userPasswordProperties.getMaxRetryCount();
+ Integer lockTime = userPasswordProperties.getLockTime();
+
+ // 鑾峰彇鐢ㄦ埛鐧诲綍閿欒娆℃暟锛岄粯璁や负0 (鍙嚜瀹氫箟闄愬埗绛栫暐 渚嬪: key + username + ip)
+ int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
+ // 閿佸畾鏃堕棿鍐呯櫥褰� 鍒欒涪鍑�
+ if (errorNumber >= maxRetryCount) {
+ recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
+ throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
+ }
+
+ if (supplier.get()) {
+ // 閿欒娆℃暟閫掑
+ errorNumber++;
+ RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
+ // 杈惧埌瑙勫畾閿欒娆℃暟 鍒欓攣瀹氱櫥褰�
+ if (errorNumber >= maxRetryCount) {
+ recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
+ throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
+ } else {
+ // 鏈揪鍒拌瀹氶敊璇鏁�
+ recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
+ throw new UserException(loginType.getRetryLimitCount(), errorNumber);
+ }
+ }
+
+ // 鐧诲綍鎴愬姛 娓呯┖閿欒娆℃暟
+ RedisUtils.deleteObject(errorKey);
+ }
+
+ /**
+ * 鏍¢獙绉熸埛
+ *
+ * @param tenantId 绉熸埛ID
+ */
+ public void checkTenant(String tenantId) {
+ if (!TenantHelper.isEnable()) {
+ return;
+ }
+ if (TenantConstants.DEFAULT_TENANT_ID.equals(tenantId)) {
+ return;
+ }
+ if (StringUtils.isBlank(tenantId)) {
+ throw new TenantException("tenant.number.not.blank");
+ }
+ RemoteTenantVo tenant = remoteTenantService.queryByTenantId(tenantId);
+ if (ObjectUtil.isNull(tenant)) {
+ log.info("鐧诲綍绉熸埛锛歿} 涓嶅瓨鍦�.", tenantId);
+ throw new TenantException("tenant.not.exists");
+ } else if (TenantStatus.DISABLE.getCode().equals(tenant.getStatus())) {
+ log.info("鐧诲綍绉熸埛锛歿} 宸茶鍋滅敤.", tenantId);
+ throw new TenantException("tenant.blocked");
+ } else if (ObjectUtil.isNotNull(tenant.getExpireTime())
+ && new Date().after(tenant.getExpireTime())) {
+ log.info("鐧诲綍绉熸埛锛歿} 宸茶秴杩囨湁鏁堟湡.", tenantId);
+ throw new TenantException("tenant.expired");
+ }
+ }
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/EmailAuthStrategy.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/EmailAuthStrategy.java
new file mode 100644
index 0000000..8790fe1
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/EmailAuthStrategy.java
@@ -0,0 +1,84 @@
+package org.dromara.auth.service.impl;
+
+import cn.dev33.satoken.stp.SaLoginModel;
+import cn.dev33.satoken.stp.StpUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.auth.domain.vo.LoginVo;
+import org.dromara.auth.form.EmailLoginBody;
+import org.dromara.auth.service.IAuthStrategy;
+import org.dromara.auth.service.SysLoginService;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.enums.LoginType;
+import org.dromara.common.core.exception.user.CaptchaExpireException;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.system.api.RemoteUserService;
+import org.dromara.system.api.domain.vo.RemoteClientVo;
+import org.dromara.system.api.model.LoginUser;
+import org.springframework.stereotype.Service;
+
+/**
+ * 閭欢璁よ瘉绛栫暐
+ *
+ * @author Michelle.Chung
+ */
+@Slf4j
+@Service("email" + IAuthStrategy.BASE_NAME)
+@RequiredArgsConstructor
+public class EmailAuthStrategy implements IAuthStrategy {
+
+ private final SysLoginService loginService;
+
+ @DubboReference
+ private RemoteUserService remoteUserService;
+
+ @Override
+ public LoginVo login(String body, RemoteClientVo client) {
+ EmailLoginBody loginBody = JsonUtils.parseObject(body, EmailLoginBody.class);
+ ValidatorUtils.validate(loginBody);
+ String tenantId = loginBody.getTenantId();
+ String email = loginBody.getEmail();
+ String emailCode = loginBody.getEmailCode();
+
+ // 閫氳繃閭鏌ユ壘鐢ㄦ埛
+ LoginUser loginUser = remoteUserService.getUserInfoByEmail(email, tenantId);
+ loginService.checkLogin(LoginType.EMAIL, tenantId, loginUser.getUsername(), () -> !validateEmailCode(tenantId, email, emailCode));
+ loginUser.setClientKey(client.getClientKey());
+ loginUser.setDeviceType(client.getDeviceType());
+ SaLoginModel model = new SaLoginModel();
+ model.setDevice(client.getDeviceType());
+ // 鑷畾涔夊垎閰� 涓嶅悓鐢ㄦ埛浣撶郴 涓嶅悓 token 鎺堟潈鏃堕棿 涓嶈缃粯璁よ蛋鍏ㄥ眬 yml 閰嶇疆
+ // 渚嬪: 鍚庡彴鐢ㄦ埛30鍒嗛挓杩囨湡 app鐢ㄦ埛1澶╄繃鏈�
+ model.setTimeout(client.getTimeout());
+ model.setActiveTimeout(client.getActiveTimeout());
+ model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
+ // 鐢熸垚token
+ LoginHelper.login(loginUser, model);
+
+ LoginVo loginVo = new LoginVo();
+ loginVo.setAccessToken(StpUtil.getTokenValue());
+ loginVo.setExpireIn(StpUtil.getTokenTimeout());
+ loginVo.setClientId(client.getClientId());
+ return loginVo;
+ }
+
+ /**
+ * 鏍¢獙閭楠岃瘉鐮�
+ */
+ private boolean validateEmailCode(String tenantId, String email, String emailCode) {
+ String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + email);
+ if (StringUtils.isBlank(code)) {
+ loginService.recordLogininfor(tenantId, email, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
+ throw new CaptchaExpireException();
+ }
+ return code.equals(emailCode);
+ }
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/PasswordAuthStrategy.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/PasswordAuthStrategy.java
new file mode 100644
index 0000000..9550485
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/PasswordAuthStrategy.java
@@ -0,0 +1,104 @@
+package org.dromara.auth.service.impl;
+
+import cn.dev33.satoken.secure.BCrypt;
+import cn.dev33.satoken.stp.SaLoginModel;
+import cn.dev33.satoken.stp.StpUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.auth.domain.vo.LoginVo;
+import org.dromara.auth.form.PasswordLoginBody;
+import org.dromara.auth.properties.CaptchaProperties;
+import org.dromara.auth.service.IAuthStrategy;
+import org.dromara.auth.service.SysLoginService;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.enums.LoginType;
+import org.dromara.common.core.exception.user.CaptchaException;
+import org.dromara.common.core.exception.user.CaptchaExpireException;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.system.api.RemoteUserService;
+import org.dromara.system.api.domain.vo.RemoteClientVo;
+import org.dromara.system.api.model.LoginUser;
+import org.springframework.stereotype.Service;
+
+/**
+ * 瀵嗙爜璁よ瘉绛栫暐
+ *
+ * @author Michelle.Chung
+ */
+@Slf4j
+@Service("password" + IAuthStrategy.BASE_NAME)
+@RequiredArgsConstructor
+public class PasswordAuthStrategy implements IAuthStrategy {
+
+ private final CaptchaProperties captchaProperties;
+
+ private final SysLoginService loginService;
+
+ @DubboReference
+ private RemoteUserService remoteUserService;
+
+ @Override
+ public LoginVo login(String body, RemoteClientVo client) {
+ PasswordLoginBody loginBody = JsonUtils.parseObject(body, PasswordLoginBody.class);
+ ValidatorUtils.validate(loginBody);
+ String tenantId = loginBody.getTenantId();
+ String username = loginBody.getUsername();
+ String password = loginBody.getPassword();
+ String code = loginBody.getCode();
+ String uuid = loginBody.getUuid();
+
+ // 楠岃瘉鐮佸紑鍏�
+ if (captchaProperties.getEnabled()) {
+ validateCaptcha(tenantId, username, code, uuid);
+ }
+
+ LoginUser loginUser = remoteUserService.getUserInfo(username, tenantId);
+ loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, loginUser.getPassword()));
+ loginUser.setClientKey(client.getClientKey());
+ loginUser.setDeviceType(client.getDeviceType());
+ SaLoginModel model = new SaLoginModel();
+ model.setDevice(client.getDeviceType());
+ // 鑷畾涔夊垎閰� 涓嶅悓鐢ㄦ埛浣撶郴 涓嶅悓 token 鎺堟潈鏃堕棿 涓嶈缃粯璁よ蛋鍏ㄥ眬 yml 閰嶇疆
+ // 渚嬪: 鍚庡彴鐢ㄦ埛30鍒嗛挓杩囨湡 app鐢ㄦ埛1澶╄繃鏈�
+ model.setTimeout(client.getTimeout());
+ model.setActiveTimeout(client.getActiveTimeout());
+ model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
+ // 鐢熸垚token
+ LoginHelper.login(loginUser, model);
+
+ LoginVo loginVo = new LoginVo();
+ loginVo.setAccessToken(StpUtil.getTokenValue());
+ loginVo.setExpireIn(StpUtil.getTokenTimeout());
+ loginVo.setClientId(client.getClientId());
+ return loginVo;
+ }
+
+ /**
+ * 鏍¢獙楠岃瘉鐮�
+ *
+ * @param username 鐢ㄦ埛鍚�
+ * @param code 楠岃瘉鐮�
+ * @param uuid 鍞竴鏍囪瘑
+ */
+ private void validateCaptcha(String tenantId, String username, String code, String uuid) {
+ String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.defaultString(uuid, "");
+ String captcha = RedisUtils.getCacheObject(verifyKey);
+ RedisUtils.deleteObject(verifyKey);
+ if (captcha == null) {
+ loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
+ throw new CaptchaExpireException();
+ }
+ if (!code.equalsIgnoreCase(captcha)) {
+ loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
+ throw new CaptchaException();
+ }
+ }
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SmsAuthStrategy.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SmsAuthStrategy.java
new file mode 100644
index 0000000..d32b5aa
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SmsAuthStrategy.java
@@ -0,0 +1,84 @@
+package org.dromara.auth.service.impl;
+
+import cn.dev33.satoken.stp.SaLoginModel;
+import cn.dev33.satoken.stp.StpUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.auth.domain.vo.LoginVo;
+import org.dromara.auth.form.SmsLoginBody;
+import org.dromara.auth.service.IAuthStrategy;
+import org.dromara.auth.service.SysLoginService;
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.core.constant.GlobalConstants;
+import org.dromara.common.core.enums.LoginType;
+import org.dromara.common.core.exception.user.CaptchaExpireException;
+import org.dromara.common.core.utils.MessageUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.redis.utils.RedisUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.system.api.RemoteUserService;
+import org.dromara.system.api.domain.vo.RemoteClientVo;
+import org.dromara.system.api.model.LoginUser;
+import org.springframework.stereotype.Service;
+
+/**
+ * 鐭俊璁よ瘉绛栫暐
+ *
+ * @author Michelle.Chung
+ */
+@Slf4j
+@Service("sms" + IAuthStrategy.BASE_NAME)
+@RequiredArgsConstructor
+public class SmsAuthStrategy implements IAuthStrategy {
+
+ private final SysLoginService loginService;
+
+ @DubboReference
+ private RemoteUserService remoteUserService;
+
+ @Override
+ public LoginVo login(String body, RemoteClientVo client) {
+ SmsLoginBody loginBody = JsonUtils.parseObject(body, SmsLoginBody.class);
+ ValidatorUtils.validate(loginBody);
+ String tenantId = loginBody.getTenantId();
+ String phonenumber = loginBody.getPhonenumber();
+ String smsCode = loginBody.getSmsCode();
+
+ // 閫氳繃鎵嬫満鍙锋煡鎵剧敤鎴�
+ LoginUser loginUser = remoteUserService.getUserInfoByPhonenumber(phonenumber, tenantId);
+ loginService.checkLogin(LoginType.SMS, tenantId, loginUser.getUsername(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
+ loginUser.setClientKey(client.getClientKey());
+ loginUser.setDeviceType(client.getDeviceType());
+ SaLoginModel model = new SaLoginModel();
+ model.setDevice(client.getDeviceType());
+ // 鑷畾涔夊垎閰� 涓嶅悓鐢ㄦ埛浣撶郴 涓嶅悓 token 鎺堟潈鏃堕棿 涓嶈缃粯璁よ蛋鍏ㄥ眬 yml 閰嶇疆
+ // 渚嬪: 鍚庡彴鐢ㄦ埛30鍒嗛挓杩囨湡 app鐢ㄦ埛1澶╄繃鏈�
+ model.setTimeout(client.getTimeout());
+ model.setActiveTimeout(client.getActiveTimeout());
+ model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
+ // 鐢熸垚token
+ LoginHelper.login(loginUser, model);
+
+ LoginVo loginVo = new LoginVo();
+ loginVo.setAccessToken(StpUtil.getTokenValue());
+ loginVo.setExpireIn(StpUtil.getTokenTimeout());
+ loginVo.setClientId(client.getClientId());
+ return loginVo;
+ }
+
+ /**
+ * 鏍¢獙鐭俊楠岃瘉鐮�
+ */
+ private boolean validateSmsCode(String tenantId, String phonenumber, String smsCode) {
+ String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + phonenumber);
+ if (StringUtils.isBlank(code)) {
+ loginService.recordLogininfor(tenantId, phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
+ throw new CaptchaExpireException();
+ }
+ return code.equals(smsCode);
+ }
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SocialAuthStrategy.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SocialAuthStrategy.java
new file mode 100644
index 0000000..ea1bde1
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SocialAuthStrategy.java
@@ -0,0 +1,109 @@
+package org.dromara.auth.service.impl;
+
+import cn.dev33.satoken.stp.SaLoginModel;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.http.HttpUtil;
+import cn.hutool.http.Method;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.zhyd.oauth.model.AuthResponse;
+import me.zhyd.oauth.model.AuthUser;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.auth.domain.vo.LoginVo;
+import org.dromara.auth.form.SocialLoginBody;
+import org.dromara.auth.service.IAuthStrategy;
+import org.dromara.auth.service.SysLoginService;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.social.config.properties.SocialProperties;
+import org.dromara.common.social.utils.SocialUtils;
+import org.dromara.system.api.RemoteSocialService;
+import org.dromara.system.api.RemoteUserService;
+import org.dromara.system.api.domain.vo.RemoteClientVo;
+import org.dromara.system.api.domain.vo.RemoteSocialVo;
+import org.dromara.system.api.model.LoginUser;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * 绗笁鏂规巿鏉冪瓥鐣�
+ *
+ * @author thiszhc is 涓変笁
+ */
+@Slf4j
+@Service("social" + IAuthStrategy.BASE_NAME)
+@RequiredArgsConstructor
+public class SocialAuthStrategy implements IAuthStrategy {
+
+ private final SocialProperties socialProperties;
+ private final SysLoginService loginService;
+
+ @DubboReference
+ private RemoteSocialService remoteSocialService;
+ @DubboReference
+ private RemoteUserService remoteUserService;
+
+ /**
+ * 鐧诲綍-绗笁鏂规巿鏉冪櫥褰�
+ *
+ * @param body 鐧诲綍淇℃伅
+ * @param client 瀹㈡埛绔俊鎭�
+ */
+ @Override
+ public LoginVo login(String body, RemoteClientVo client) {
+ SocialLoginBody loginBody = JsonUtils.parseObject(body, SocialLoginBody.class);
+ ValidatorUtils.validate(loginBody);
+ AuthResponse<AuthUser> response = SocialUtils.loginAuth(
+ loginBody.getSource(), loginBody.getSocialCode(),
+ loginBody.getSocialState(), socialProperties);
+ if (!response.ok()) {
+ throw new ServiceException(response.getMsg());
+ }
+ AuthUser authUserData = response.getData();
+ if ("GITEE".equals(authUserData.getSource())) {
+ // 濡傜敤鎴蜂娇鐢� gitee 鐧诲綍椤烘墜 star 缁欎綔鑰呬竴鐐规敮鎸� 鎷掔粷鐧藉珫
+ HttpUtil.createRequest(Method.PUT, "https://gitee.com/api/v5/user/starred/dromara/RuoYi-Vue-Plus")
+ .formStr(MapUtil.of("access_token", authUserData.getToken().getAccessToken()))
+ .executeAsync();
+ HttpUtil.createRequest(Method.PUT, "https://gitee.com/api/v5/user/starred/dromara/RuoYi-Cloud-Plus")
+ .formStr(MapUtil.of("access_token", authUserData.getToken().getAccessToken()))
+ .executeAsync();
+ }
+
+ List<RemoteSocialVo> list = remoteSocialService.selectByAuthId(authUserData.getSource() + authUserData.getUuid());
+ if (CollUtil.isEmpty(list)) {
+ throw new ServiceException("浣犺繕娌℃湁缁戝畾绗笁鏂硅处鍙凤紝缁戝畾鍚庢墠鍙互鐧诲綍锛�");
+ }
+ Optional<RemoteSocialVo> opt = list.stream().filter(x -> x.getTenantId().equals(loginBody.getTenantId())).findAny();
+ if (opt.isEmpty()) {
+ throw new ServiceException("瀵逛笉璧凤紝浣犳病鏈夋潈闄愮櫥褰曞綋鍓嶇鎴凤紒");
+ }
+ RemoteSocialVo socialVo = opt.get();
+
+ LoginUser loginUser = remoteUserService.getUserInfo(socialVo.getUserId(), socialVo.getTenantId());
+ loginUser.setClientKey(client.getClientKey());
+ loginUser.setDeviceType(client.getDeviceType());
+ SaLoginModel model = new SaLoginModel();
+ model.setDevice(client.getDeviceType());
+ // 鑷畾涔夊垎閰� 涓嶅悓鐢ㄦ埛浣撶郴 涓嶅悓 token 鎺堟潈鏃堕棿 涓嶈缃粯璁よ蛋鍏ㄥ眬 yml 閰嶇疆
+ // 渚嬪: 鍚庡彴鐢ㄦ埛30鍒嗛挓杩囨湡 app鐢ㄦ埛1澶╄繃鏈�
+ model.setTimeout(client.getTimeout());
+ model.setActiveTimeout(client.getActiveTimeout());
+ model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
+ // 鐢熸垚token
+ LoginHelper.login(loginUser, model);
+
+ LoginVo loginVo = new LoginVo();
+ loginVo.setAccessToken(StpUtil.getTokenValue());
+ loginVo.setExpireIn(StpUtil.getTokenTimeout());
+ loginVo.setClientId(client.getClientId());
+ return loginVo;
+ }
+
+}
diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/XcxAuthStrategy.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/XcxAuthStrategy.java
new file mode 100644
index 0000000..20ec4cc
--- /dev/null
+++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/XcxAuthStrategy.java
@@ -0,0 +1,69 @@
+package org.dromara.auth.service.impl;
+
+import cn.dev33.satoken.stp.SaLoginModel;
+import cn.dev33.satoken.stp.StpUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.auth.domain.vo.LoginVo;
+import org.dromara.auth.form.XcxLoginBody;
+import org.dromara.auth.service.IAuthStrategy;
+import org.dromara.auth.service.SysLoginService;
+import org.dromara.common.core.utils.ValidatorUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.system.api.RemoteUserService;
+import org.dromara.system.api.domain.vo.RemoteClientVo;
+import org.dromara.system.api.model.XcxLoginUser;
+import org.springframework.stereotype.Service;
+
+/**
+ * 閭欢璁よ瘉绛栫暐
+ *
+ * @author Michelle.Chung
+ */
+@Slf4j
+@Service("xcx" + IAuthStrategy.BASE_NAME)
+@RequiredArgsConstructor
+public class XcxAuthStrategy implements IAuthStrategy {
+
+ private final SysLoginService loginService;
+
+ @DubboReference
+ private RemoteUserService remoteUserService;
+
+ @Override
+ public LoginVo login(String body, RemoteClientVo client) {
+ XcxLoginBody loginBody = JsonUtils.parseObject(body, XcxLoginBody.class);
+ ValidatorUtils.validate(loginBody);
+ // xcxCode 涓� 灏忕▼搴忚皟鐢� wx.login 鎺堟潈鍚庤幏鍙�
+ String xcxCode = loginBody.getXcxCode();
+ // 澶氫釜灏忕▼搴忚瘑鍒娇鐢�
+ String appid = loginBody.getAppid();
+
+ // todo 浠ヤ笅鑷瀹炵幇
+ // 鏍¢獙 appid + appsrcret + xcxCode 璋冪敤鐧诲綍鍑瘉鏍¢獙鎺ュ彛 鑾峰彇 session_key 涓� openid
+ String openid = "";
+ XcxLoginUser loginUser = remoteUserService.getUserInfoByOpenid(openid);
+ loginUser.setClientKey(client.getClientKey());
+ loginUser.setDeviceType(client.getDeviceType());
+
+ SaLoginModel model = new SaLoginModel();
+ model.setDevice(client.getDeviceType());
+ // 鑷畾涔夊垎閰� 涓嶅悓鐢ㄦ埛浣撶郴 涓嶅悓 token 鎺堟潈鏃堕棿 涓嶈缃粯璁よ蛋鍏ㄥ眬 yml 閰嶇疆
+ // 渚嬪: 鍚庡彴鐢ㄦ埛30鍒嗛挓杩囨湡 app鐢ㄦ埛1澶╄繃鏈�
+ model.setTimeout(client.getTimeout());
+ model.setActiveTimeout(client.getActiveTimeout());
+ model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId());
+ // 鐢熸垚token
+ LoginHelper.login(loginUser, model);
+
+ LoginVo loginVo = new LoginVo();
+ loginVo.setAccessToken(StpUtil.getTokenValue());
+ loginVo.setExpireIn(StpUtil.getTokenTimeout());
+ loginVo.setClientId(client.getClientId());
+ loginVo.setOpenid(openid);
+ return loginVo;
+ }
+
+}
diff --git a/ruoyi-auth/src/main/resources/application.yml b/ruoyi-auth/src/main/resources/application.yml
new file mode 100644
index 0000000..24f5810
--- /dev/null
+++ b/ruoyi-auth/src/main/resources/application.yml
@@ -0,0 +1,31 @@
+# Tomcat
+server:
+ port: 9210
+
+# Spring
+spring:
+ application:
+ # 搴旂敤鍚嶇О
+ name: ruoyi-auth
+ profiles:
+ # 鐜閰嶇疆
+ active: @profiles.active@
+
+--- # nacos 閰嶇疆
+spring:
+ cloud:
+ nacos:
+ # nacos 鏈嶅姟鍦板潃
+ server-addr: @nacos.server@
+ discovery:
+ # 娉ㄥ唽缁�
+ group: @nacos.discovery.group@
+ namespace: ${spring.profiles.active}
+ config:
+ # 閰嶇疆缁�
+ group: @nacos.config.group@
+ namespace: ${spring.profiles.active}
+ config:
+ import:
+ - optional:nacos:application-common.yml
+ - optional:nacos:${spring.application.name}.yml
diff --git a/ruoyi-auth/src/main/resources/banner.txt b/ruoyi-auth/src/main/resources/banner.txt
new file mode 100644
index 0000000..97c5c27
--- /dev/null
+++ b/ruoyi-auth/src/main/resources/banner.txt
@@ -0,0 +1,10 @@
+Spring Boot Version: ${spring-boot.version}
+Spring Application Name: ${spring.application.name}
+ _ _ _
+ (_) | | | |
+ _ __ _ _ ___ _ _ _ ______ __ _ _ _ | |_ | |__
+| '__|| | | | / _ \ | | | || ||______| / _` || | | || __|| '_ \
+| | | |_| || (_) || |_| || | | (_| || |_| || |_ | | | |
+|_| \__,_| \___/ \__, ||_| \__,_| \__,_| \__||_| |_|
+ __/ |
+ |___/
\ No newline at end of file
diff --git a/ruoyi-auth/src/main/resources/logback-plus.xml b/ruoyi-auth/src/main/resources/logback-plus.xml
new file mode 100644
index 0000000..a2e187f
--- /dev/null
+++ b/ruoyi-auth/src/main/resources/logback-plus.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="60 seconds" debug="false">
+ <!-- 鏃ュ織瀛樻斁璺緞 -->
+ <property name="log.path" value="logs/${project.artifactId}"/>
+ <!-- 鏃ュ織杈撳嚭鏍煎紡 -->
+ <property name="console.log.pattern"
+ value="%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/>
+
+ <!-- 鎺у埗鍙拌緭鍑� -->
+ <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>${console.log.pattern}</pattern>
+ <charset>utf-8</charset>
+ </encoder>
+ </appender>
+
+ <include resource="logback-common.xml" />
+
+ <include resource="logback-logstash.xml" />
+
+ <!-- 寮�鍚� skywalking 鏃ュ織鏀堕泦 -->
+ <include resource="logback-skylog.xml" />
+
+ <!--绯荤粺鎿嶄綔鏃ュ織-->
+ <root level="info">
+ <appender-ref ref="console"/>
+ </root>
+</configuration>
--
Gitblit v1.9.1