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