Browse Source

feat 若依登录接口

jay 2 years ago
parent
commit
cdf77d22a0
17 changed files with 1207 additions and 0 deletions
  1. 44 0
      iot-common/iot-common-core/src/main/java/cc/iotkit/common/enums/LoginType.java
  2. 30 0
      iot-common/iot-common-core/src/main/java/cc/iotkit/common/enums/UserStatus.java
  3. 80 0
      iot-common/iot-common-core/src/main/java/cc/iotkit/common/exception/BaseException.java
  4. 44 0
      iot-common/iot-common-core/src/main/java/cc/iotkit/common/exception/user/UserException.java
  5. 35 0
      iot-common/iot-common-core/src/main/java/cc/iotkit/common/undefined/EmailLoginBody.java
  6. 33 0
      iot-common/iot-common-core/src/main/java/cc/iotkit/common/undefined/SmsLoginBody.java
  7. 26 0
      iot-common/iot-common-core/src/main/java/cc/iotkit/common/undefined/XcxLoginUser.java
  8. 28 0
      iot-common/iot-common-core/src/main/java/cc/iotkit/common/utils/MessageUtils.java
  9. 3 0
      iot-module/iot-system/src/main/java/cc/iotkit/system/dto/bo/SysUserBo.java
  10. 155 0
      iot-starter/src/main/java/cc/iotkit/web/controller/AuthController.java
  11. 132 0
      iot-starter/src/main/java/cc/iotkit/web/controller/CaptchaController.java
  12. 25 0
      iot-starter/src/main/java/cc/iotkit/web/domain/vo/CaptchaVo.java
  13. 25 0
      iot-starter/src/main/java/cc/iotkit/web/domain/vo/LoginTenantVo.java
  14. 15 0
      iot-starter/src/main/java/cc/iotkit/web/domain/vo/LoginVo.java
  15. 23 0
      iot-starter/src/main/java/cc/iotkit/web/domain/vo/TenantListVo.java
  16. 403 0
      iot-starter/src/main/java/cc/iotkit/web/service/SysLoginService.java
  17. 106 0
      iot-starter/src/main/java/cc/iotkit/web/service/SysRegisterService.java

+ 44 - 0
iot-common/iot-common-core/src/main/java/cc/iotkit/common/enums/LoginType.java

@@ -0,0 +1,44 @@
+package cc.iotkit.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 登录类型
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum LoginType {
+
+    /**
+     * 密码登录
+     */
+    PASSWORD("user.password.retry.limit.exceed", "user.password.retry.limit.count"),
+
+    /**
+     * 短信登录
+     */
+    SMS("sms.code.retry.limit.exceed", "sms.code.retry.limit.count"),
+
+    /**
+     * 邮箱登录
+     */
+    EMAIL("email.code.retry.limit.exceed", "email.code.retry.limit.count"),
+
+    /**
+     * 小程序登录
+     */
+    XCX("", "");
+
+    /**
+     * 登录重试超出限制提示
+     */
+    final String retryLimitExceed;
+
+    /**
+     * 登录重试限制计数提示
+     */
+    final String retryLimitCount;
+}

+ 30 - 0
iot-common/iot-common-core/src/main/java/cc/iotkit/common/enums/UserStatus.java

@@ -0,0 +1,30 @@
+package cc.iotkit.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 用户状态
+ *
+ * @author ruoyi
+ */
+@Getter
+@AllArgsConstructor
+public enum UserStatus {
+    /**
+     * 正常
+     */
+    OK("0", "正常"),
+    /**
+     * 停用
+     */
+    DISABLE("1", "停用"),
+    /**
+     * 删除
+     */
+    DELETED("2", "删除");
+
+    private final String code;
+    private final String info;
+
+}

+ 80 - 0
iot-common/iot-common-core/src/main/java/cc/iotkit/common/exception/BaseException.java

@@ -0,0 +1,80 @@
+package cc.iotkit.common.exception;
+
+
+import cc.iotkit.common.utils.MessageUtils;
+import cc.iotkit.common.utils.StringUtils;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+
+
+/**
+ * 基础异常
+ *
+ * @author ruoyi
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+public class BaseException extends RuntimeException {
+
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 所属模块
+     */
+    private String module;
+
+    /**
+     * 错误码
+     */
+    private String code;
+
+    /**
+     * 错误码对应的参数
+     */
+    private Object[] args;
+
+    /**
+     * 错误消息
+     */
+    private String defaultMessage;
+
+    public BaseException(String module, String code, Object[] args, String defaultMessage) {
+        this.module = module;
+        this.code = code;
+        this.args = args;
+        this.defaultMessage = defaultMessage;
+    }
+
+    public BaseException(String module, String code, Object[] args) {
+        this(module, code, args, null);
+    }
+
+    public BaseException(String module, String defaultMessage) {
+        this(module, null, null, defaultMessage);
+    }
+
+    public BaseException(String code, Object[] args) {
+        this(null, code, args, null);
+    }
+
+    public BaseException(String defaultMessage) {
+        this(null, null, null, defaultMessage);
+    }
+
+    @Override
+    public String getMessage() {
+        String message = null;
+        if (!StringUtils.isEmpty(code)) {
+            message = MessageUtils.message(code, args);
+        }
+        if (message == null) {
+            message = defaultMessage;
+        }
+        return message;
+    }
+
+}

+ 44 - 0
iot-common/iot-common-core/src/main/java/cc/iotkit/common/exception/user/UserException.java

@@ -0,0 +1,44 @@
+/*
+ * +----------------------------------------------------------------------
+ * | Copyright (c) 奇特物联 2021-2022 All rights reserved.
+ * +----------------------------------------------------------------------
+ * | Licensed 未经许可不能去掉「奇特物联」相关版权
+ * +----------------------------------------------------------------------
+ * | Author: xw2sy@163.com
+ * +----------------------------------------------------------------------
+ */
+package cc.iotkit.common.exception.user;
+
+import cc.iotkit.common.enums.ErrCode;
+import cc.iotkit.common.exception.BaseException;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class UserException extends BaseException {
+
+    /**
+     * 所属模块
+     */
+    private String module;
+
+    /**
+     * 错误码
+     */
+    private Integer code;
+
+    /**
+     * 错误消息
+     */
+    private String message;
+
+    public UserException(String code, Object... args) {
+        super("user", code, args, null);
+    }
+
+}

+ 35 - 0
iot-common/iot-common-core/src/main/java/cc/iotkit/common/undefined/EmailLoginBody.java

@@ -0,0 +1,35 @@
+package cc.iotkit.common.undefined;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+/**
+ * 短信登录对象
+ *
+ * @author Lion Li
+ */
+
+@Data
+public class EmailLoginBody {
+
+    /**
+     * 租户ID
+     */
+    @NotBlank(message = "{tenant.number.not.blank}")
+    private String tenantId;
+
+    /**
+     * 邮箱
+     */
+    @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;
+
+}

+ 33 - 0
iot-common/iot-common-core/src/main/java/cc/iotkit/common/undefined/SmsLoginBody.java

@@ -0,0 +1,33 @@
+package cc.iotkit.common.undefined;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+/**
+ * 短信登录对象
+ *
+ * @author Lion Li
+ */
+
+@Data
+public class SmsLoginBody {
+
+    /**
+     * 租户ID
+     */
+    @NotBlank(message = "{tenant.number.not.blank}")
+    private String tenantId;
+
+    /**
+     * 手机号
+     */
+    @NotBlank(message = "{user.phonenumber.not.blank}")
+    private String phonenumber;
+
+    /**
+     * 短信code
+     */
+    @NotBlank(message = "{sms.code.not.blank}")
+    private String smsCode;
+
+}

+ 26 - 0
iot-common/iot-common-core/src/main/java/cc/iotkit/common/undefined/XcxLoginUser.java

@@ -0,0 +1,26 @@
+package cc.iotkit.common.undefined;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+
+/**
+ * 小程序登录用户身份权限
+ *
+ * @author Lion Li
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+public class XcxLoginUser extends LoginUser {
+
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * openid
+     */
+    private String openid;
+
+}

+ 28 - 0
iot-common/iot-common-core/src/main/java/cc/iotkit/common/utils/MessageUtils.java

@@ -0,0 +1,28 @@
+package cc.iotkit.common.utils;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.springframework.context.MessageSource;
+import org.springframework.context.i18n.LocaleContextHolder;
+
+/**
+ * 获取i18n资源文件
+ *
+ * @author Lion Li
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class MessageUtils {
+
+    private static final MessageSource MESSAGE_SOURCE = SpringUtils.getBean(MessageSource.class);
+
+    /**
+     * 根据消息键和参数 获取消息 委托给spring messageSource
+     *
+     * @param code 消息键
+     * @param args 参数
+     * @return 获取国际化翻译值
+     */
+    public static String message(String code, Object... args) {
+        return MESSAGE_SOURCE.getMessage(code, args, LocaleContextHolder.getLocale());
+    }
+}

+ 3 - 0
iot-module/iot-system/src/main/java/cc/iotkit/system/dto/bo/SysUserBo.java

@@ -9,6 +9,7 @@ import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.Size;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
 
 /**
  * 用户信息业务对象 sys_user
@@ -17,6 +18,8 @@ import lombok.EqualsAndHashCode;
  */
 
 @Data
+@NoArgsConstructor
+
 @EqualsAndHashCode(callSuper = true)
 @AutoMapper(target = SysUser.class, reverseConvertGenerate = false)
 public class SysUserBo extends BaseDto {

+ 155 - 0
iot-starter/src/main/java/cc/iotkit/web/controller/AuthController.java

@@ -0,0 +1,155 @@
+package cc.iotkit.web.controller;
+
+import cc.iotkit.common.exception.BizException;
+import cc.iotkit.common.tenant.helper.TenantHelper;
+import cc.iotkit.common.undefined.EmailLoginBody;
+import cc.iotkit.common.undefined.LoginBody;
+import cc.iotkit.common.undefined.RegisterBody;
+import cc.iotkit.common.undefined.SmsLoginBody;
+import cc.iotkit.common.utils.MapstructUtils;
+import cc.iotkit.common.utils.StreamUtils;
+import cc.iotkit.common.utils.StringUtils;
+import cc.iotkit.system.dto.bo.SysTenantBo;
+import cc.iotkit.system.dto.vo.SysTenantVo;
+import cc.iotkit.system.service.ISysConfigService;
+import cc.iotkit.system.service.ISysTenantService;
+import cc.iotkit.web.domain.vo.LoginTenantVo;
+import cc.iotkit.web.domain.vo.LoginVo;
+import cc.iotkit.web.domain.vo.TenantListVo;
+import cc.iotkit.web.service.SysLoginService;
+import cc.iotkit.web.service.SysRegisterService;
+import cn.dev33.satoken.annotation.SaIgnore;
+import cn.hutool.core.collection.CollUtil;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.RequiredArgsConstructor;
+
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.net.URL;
+import java.util.List;
+
+/**
+ * 认证
+ *
+ * @author Lion Li
+ */
+@SaIgnore
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/auth")
+public class AuthController {
+
+    private final SysLoginService loginService;
+    private final SysRegisterService registerService;
+    private final ISysConfigService configService;
+    private final ISysTenantService tenantService;
+
+    /**
+     * 登录方法
+     *
+     * @param body 登录信息
+     * @return 结果
+     */
+    @PostMapping("/login")
+    public LoginVo login(@Validated @RequestBody LoginBody body) {
+        LoginVo loginVo = new LoginVo();
+        // 生成令牌
+        String token = loginService.login(
+                body.getTenantId(),
+                body.getUsername(), body.getPassword(),
+                body.getCode(), body.getUuid());
+        loginVo.setToken(token);
+        return loginVo;
+    }
+
+    /**
+     * 短信登录
+     *
+     * @param body 登录信息
+     * @return 结果
+     */
+    @PostMapping("/smsLogin")
+    public LoginVo smsLogin(@Validated @RequestBody SmsLoginBody body) {
+        LoginVo loginVo = new LoginVo();
+        // 生成令牌
+        String token = loginService.smsLogin(body.getTenantId(), body.getPhonenumber(), body.getSmsCode());
+        loginVo.setToken(token);
+        return loginVo;
+    }
+
+    /**
+     * 邮件登录
+     *
+     * @param body 登录信息
+     * @return 结果
+     */
+    @PostMapping("/emailLogin")
+    public LoginVo emailLogin(@Validated @RequestBody EmailLoginBody body) {
+        LoginVo loginVo = new LoginVo();
+        // 生成令牌
+        String token = loginService.emailLogin(body.getTenantId(), body.getEmail(), body.getEmailCode());
+        loginVo.setToken(token);
+        return loginVo;
+    }
+
+    /**
+     * 小程序登录(示例)
+     *
+     * @param xcxCode 小程序code
+     * @return 结果
+     */
+    @PostMapping("/xcxLogin")
+    public LoginVo xcxLogin(@NotBlank(message = "{xcx.code.not.blank}") String xcxCode) {
+        LoginVo loginVo = new LoginVo();
+        // 生成令牌
+        String token = loginService.xcxLogin(xcxCode);
+        loginVo.setToken(token);
+        return loginVo;
+    }
+
+    /**
+     * 退出登录
+     */
+    @PostMapping("/logout")
+    public void logout() {
+        loginService.logout();
+
+    }
+
+    /**
+     * 用户注册
+     */
+    @PostMapping("/register")
+    public void register(@Validated @RequestBody RegisterBody user) {
+        if (!configService.selectRegisterEnabled(user.getTenantId())) {
+            throw new BizException("当前租户不允许注册");
+        }
+        registerService.register(user);
+
+    }
+
+    /**
+     * 登录页面租户下拉框
+     *
+     * @return 租户列表
+     */
+    @GetMapping("/tenant/list")
+    public LoginTenantVo tenantList(HttpServletRequest request) throws Exception {
+        List<SysTenantVo> tenantList = tenantService.queryList(new SysTenantBo());
+        List<TenantListVo> voList = MapstructUtils.convert(tenantList, TenantListVo.class);
+        // 获取域名
+        String 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 vo;
+    }
+
+}

+ 132 - 0
iot-starter/src/main/java/cc/iotkit/web/controller/CaptchaController.java

@@ -0,0 +1,132 @@
+package cc.iotkit.web.controller;
+
+import cc.iotkit.common.constant.Constants;
+import cc.iotkit.common.constant.GlobalConstants;
+import cc.iotkit.common.exception.BizException;
+import cc.iotkit.common.redis.utils.RedisUtils;
+import cc.iotkit.common.utils.ReflectUtils;
+import cc.iotkit.common.utils.SpringUtils;
+import cc.iotkit.common.utils.StringUtils;
+import cc.iotkit.common.web.config.properties.CaptchaProperties;
+import cc.iotkit.common.web.enums.CaptchaType;
+import cc.iotkit.web.domain.vo.CaptchaVo;
+import cn.dev33.satoken.annotation.SaIgnore;
+import cn.hutool.captcha.AbstractCaptcha;
+import cn.hutool.captcha.generator.CodeGenerator;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.RandomUtil;
+import jakarta.validation.constraints.NotBlank;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import org.springframework.boot.autoconfigure.mail.MailProperties;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 验证码操作处理
+ *
+ * @author Lion Li
+ */
+@SaIgnore
+@Slf4j
+@Validated
+@RequiredArgsConstructor
+@RestController
+public class CaptchaController {
+
+    private final CaptchaProperties captchaProperties;
+//    private final SmsProperties smsProperties;
+    private final MailProperties mailProperties;
+
+    /**
+     * 短信验证码
+     *
+     * @param phonenumber 用户手机号
+     */
+//    @GetMapping("/resource/sms/code")
+//    public void smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) {
+//        if (!smsProperties.getEnabled()) {
+//            throw new BizException("当前系统没有开启短信功能!");
+//        }
+//        String key = GlobalConstants.CAPTCHA_CODE_KEY + phonenumber;
+//        String code = RandomUtil.randomNumbers(4);
+//        RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
+//        // 验证码模板id 自行处理 (查数据库或写死均可)
+//        String templateId = "";
+//        Map<String, String> map = new HashMap<>(1);
+//        map.put("code", code);
+//        SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class);
+//        SmsResult result = smsTemplate.send(phonenumber, templateId, map);
+//        if (!result.isSuccess()) {
+//            log.error("验证码短信发送异常 => {}", result);
+//            throw new RuntimeException("验证码短信发送异常");
+//        }
+//        return ;
+//    }
+
+    /**
+     * 邮箱验证码
+     *
+     * @param email 邮箱
+     */
+//    @GetMapping("/resource/email/code")
+//    public void emailCode(@NotBlank(message = "{user.email.not.blank}") String email) {
+//        if (!mailProperties.getEnabled()) {
+//            throw new BizException("当前系统没有开启邮件功能!"));
+//        }
+//        String key = GlobalConstants.CAPTCHA_CODE_KEY + email;
+//        String code = RandomUtil.randomNumbers(4);
+//        RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
+//        try {
+//            MailUtils.sendText(email, "登录验证码", "您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。");
+//        } catch (Exception e) {
+//            log.error("验证码短信发送异常 => {}", e.getMessage());
+//           throw new RuntimeException("验证码短信发送异常");
+//        }
+//
+//    }
+
+    /**
+     * 生成验证码
+     */
+    @GetMapping("/code")
+    public CaptchaVo getCode() {
+        CaptchaVo captchaVo = new CaptchaVo();
+        boolean captchaEnabled = captchaProperties.getEnable();
+        if (!captchaEnabled) {
+            captchaVo.setCaptchaEnabled(false);
+            return 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 captchaVo;
+    }
+
+}

+ 25 - 0
iot-starter/src/main/java/cc/iotkit/web/domain/vo/CaptchaVo.java

@@ -0,0 +1,25 @@
+package cc.iotkit.web.domain.vo;
+
+import lombok.Data;
+
+/**
+ * 验证码信息
+ *
+ * @author Michelle.Chung
+ */
+@Data
+public class CaptchaVo {
+
+    /**
+     * 是否开启验证码
+     */
+    private Boolean captchaEnabled = true;
+
+    private String uuid;
+
+    /**
+     * 验证码图片
+     */
+    private String img;
+
+}

+ 25 - 0
iot-starter/src/main/java/cc/iotkit/web/domain/vo/LoginTenantVo.java

@@ -0,0 +1,25 @@
+package cc.iotkit.web.domain.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 登录租户对象
+ *
+ * @author Michelle.Chung
+ */
+@Data
+public class LoginTenantVo {
+
+    /**
+     * 租户开关
+     */
+    private Boolean tenantEnabled;
+
+    /**
+     * 租户对象列表
+     */
+    private List<TenantListVo> voList;
+
+}

+ 15 - 0
iot-starter/src/main/java/cc/iotkit/web/domain/vo/LoginVo.java

@@ -0,0 +1,15 @@
+package cc.iotkit.web.domain.vo;
+
+import lombok.Data;
+
+/**
+ * 登录验证信息
+ *
+ * @author Michelle.Chung
+ */
+@Data
+public class LoginVo {
+
+    private String token;
+
+}

+ 23 - 0
iot-starter/src/main/java/cc/iotkit/web/domain/vo/TenantListVo.java

@@ -0,0 +1,23 @@
+package cc.iotkit.web.domain.vo;
+
+import cc.iotkit.system.dto.vo.SysTenantVo;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+
+
+/**
+ * 租户列表
+ *
+ * @author Lion Li
+ */
+@Data
+@AutoMapper(target = SysTenantVo.class)
+public class TenantListVo {
+
+    private String tenantId;
+
+    private String companyName;
+
+    private String domain;
+
+}

+ 403 - 0
iot-starter/src/main/java/cc/iotkit/web/service/SysLoginService.java

@@ -0,0 +1,403 @@
+package cc.iotkit.web.service;
+
+import cc.iotkit.common.constant.Constants;
+import cc.iotkit.common.constant.GlobalConstants;
+import cc.iotkit.common.enums.DeviceType;
+import cc.iotkit.common.enums.LoginType;
+import cc.iotkit.common.enums.UserStatus;
+import cc.iotkit.common.exception.BizException;
+import cc.iotkit.common.exception.user.UserException;
+import cc.iotkit.common.redis.utils.RedisUtils;
+import cc.iotkit.common.satoken.utils.LoginHelper;
+import cc.iotkit.common.tenant.helper.TenantHelper;
+import cc.iotkit.common.undefined.LoginUser;
+import cc.iotkit.common.undefined.RoleDTO;
+import cc.iotkit.common.undefined.XcxLoginUser;
+import cc.iotkit.common.utils.DateUtils;
+import cc.iotkit.common.utils.MapstructUtils;
+import cc.iotkit.common.utils.MessageUtils;
+import cc.iotkit.common.utils.StringUtils;
+import cc.iotkit.common.web.config.properties.CaptchaProperties;
+import cc.iotkit.common.web.utils.ServletUtils;
+import cc.iotkit.data.system.ISysUserData;
+import cc.iotkit.model.system.SysUser;
+import cc.iotkit.system.dto.vo.SysUserVo;
+import cc.iotkit.system.service.ISysPermissionService;
+import cc.iotkit.system.service.ISysTenantService;
+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.util.ObjectUtil;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.time.Duration;
+import java.util.Date;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * 登录校验方法
+ *
+ * @author Lion Li
+ */
+@RequiredArgsConstructor
+@Slf4j
+@Service
+public class SysLoginService {
+
+    private final ISysUserData userData;
+    private final CaptchaProperties captchaProperties;
+    private final ISysPermissionService permissionService;
+    private final ISysTenantService tenantService;
+
+    @Value("${user.password.maxRetryCount}")
+    private Integer maxRetryCount;
+
+    @Value("${user.password.lockTime}")
+    private Integer lockTime;
+
+    /**
+     * 登录验证
+     *
+     * @param username 用户名
+     * @param password 密码
+     * @param code     验证码
+     * @param uuid     唯一标识
+     * @return 结果
+     */
+    public String login(String tenantId, String username, String password, String code, String uuid) {
+        boolean captchaEnabled = captchaProperties.getEnable();
+        // 验证码开关
+        if (captchaEnabled) {
+            validateCaptcha(tenantId, username, code, uuid);
+        }
+        // 校验租户
+        checkTenant(tenantId);
+
+        SysUserVo user = loadUserByUsername(tenantId, username);
+        checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
+        // 此处可根据登录用户的数据不同 自行创建 loginUser
+        LoginUser loginUser = buildLoginUser(user);
+        // 生成token
+        LoginHelper.loginByDevice(loginUser, DeviceType.PC);
+
+        recordLogininfor(loginUser.getTenantId(), username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
+        recordLoginInfo(user.getId());
+        return StpUtil.getTokenValue();
+    }
+
+    public String smsLogin(String tenantId, String phonenumber, String smsCode) {
+        // 校验租户
+        checkTenant(tenantId);
+        // 通过手机号查找用户
+        SysUserVo user = loadUserByPhonenumber(tenantId, phonenumber);
+
+        checkLogin(LoginType.SMS, tenantId, user.getUserName(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
+        // 此处可根据登录用户的数据不同 自行创建 loginUser
+        LoginUser loginUser = buildLoginUser(user);
+        // 生成token
+        LoginHelper.loginByDevice(loginUser, DeviceType.APP);
+
+        recordLogininfor(loginUser.getTenantId(), user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
+        recordLoginInfo(user.getId());
+        return StpUtil.getTokenValue();
+    }
+
+    public String emailLogin(String tenantId, String email, String emailCode) {
+        // 校验租户
+        checkTenant(tenantId);
+        // 通过手机号查找用户
+        SysUserVo user = loadUserByEmail(tenantId, email);
+
+        checkLogin(LoginType.EMAIL, tenantId, user.getUserName(), () -> !validateEmailCode(tenantId, email, emailCode));
+        // 此处可根据登录用户的数据不同 自行创建 loginUser
+        LoginUser loginUser = buildLoginUser(user);
+        // 生成token
+        LoginHelper.loginByDevice(loginUser, DeviceType.APP);
+
+        recordLogininfor(loginUser.getTenantId(), user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
+        recordLoginInfo(user.getId());
+        return StpUtil.getTokenValue();
+    }
+
+
+    public String xcxLogin(String xcxCode) {
+        // xcxCode 为 小程序调用 wx.login 授权后获取
+        // todo 以下自行实现
+        // 校验 appid + appsrcret + xcxCode 调用登录凭证校验接口 获取 session_key 与 openid
+        String openid = "";
+        SysUserVo user = loadUserByOpenid(openid);
+        // 校验租户
+        checkTenant(user.getTenantId());
+
+        // 此处可根据登录用户的数据不同 自行创建 loginUser
+        XcxLoginUser loginUser = new XcxLoginUser();
+        loginUser.setTenantId(user.getTenantId());
+        loginUser.setUserId(user.getId());
+        loginUser.setUsername(user.getUserName());
+        loginUser.setUserType(user.getUserType());
+        loginUser.setOpenid(openid);
+        // 生成token
+        LoginHelper.loginByDevice(loginUser, DeviceType.XCX);
+
+        recordLogininfor(loginUser.getTenantId(), user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
+        recordLoginInfo(user.getId());
+        return StpUtil.getTokenValue();
+    }
+
+    /**
+     * 退出登录
+     */
+    public void logout() {
+        try {
+            LoginUser loginUser = LoginHelper.getLoginUser();
+            if (TenantHelper.isEnable() && LoginHelper.isSuperAdmin()) {
+                // 超级管理员 登出清除动态租户
+                TenantHelper.clearDynamic();
+            }
+            StpUtil.logout();
+            recordLogininfor(loginUser.getTenantId(), loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success"));
+        } catch (NotLoginException ignored) {
+        }
+    }
+
+    /**
+     * 记录登录信息
+     *
+     * @param tenantId 租户ID
+     * @param username 用户名
+     * @param status   状态
+     * @param message  消息内容
+     */
+    private void recordLogininfor(String tenantId, String username, String status, String message) {
+
+    }
+
+    /**
+     * 校验短信验证码
+     */
+    private boolean validateSmsCode(String tenantId, String phonenumber, String smsCode) {
+        String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + phonenumber);
+        if (StringUtils.isBlank(code)) {
+            recordLogininfor(tenantId, phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
+            throw new BizException(MessageUtils.message("user.jcaptcha.expire"));
+        }
+        return code.equals(smsCode);
+    }
+
+    /**
+     * 校验邮箱验证码
+     */
+    private boolean validateEmailCode(String tenantId, String email, String emailCode) {
+        String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + email);
+        if (StringUtils.isBlank(code)) {
+            recordLogininfor(tenantId, email, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
+            throw new BizException("验证码过期");
+        }
+        return code.equals(emailCode);
+    }
+
+    /**
+     * 校验验证码
+     *
+     * @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.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
+            throw new BizException("验证码过期");
+        }
+        if (!code.equalsIgnoreCase(captcha)) {
+            recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
+            throw new BizException("验证码错误");
+        }
+    }
+
+    private SysUserVo loadUserByUsername(String tenantId, String username) {
+        SysUser query = new SysUser();
+        query.setUserName(username);
+        if(TenantHelper.isEnable()){
+            query.setTenantId(tenantId);
+        }
+
+        SysUser user = userData.findOneByCondition(query);
+
+        if (ObjectUtil.isNull(user)) {
+            log.info("登录用户:{} 不存在.", username);
+            throw new UserException("user.not.exists", username);
+        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+            log.info("登录用户:{} 已被停用.", username);
+            throw new UserException("user.blocked", username);
+        }
+        if (TenantHelper.isEnable()) {
+            SysUser sysUser = userData.selectTenantUserByUserName(username, tenantId);
+            return MapstructUtils.convert(sysUser, SysUserVo.class);
+
+        }
+        SysUser sysUser = userData.selectUserByUserName(username);
+        return MapstructUtils.convert(sysUser, SysUserVo.class);
+    }
+
+    private SysUserVo loadUserByPhonenumber(String tenantId, String phonenumber) {
+        SysUser query = new SysUser();
+        query.setPhonenumber(phonenumber);
+        if(TenantHelper.isEnable()){
+            query.setTenantId(tenantId);
+        }
+
+        SysUser user = userData.findOneByCondition(query);
+        if (ObjectUtil.isNull(user)) {
+            log.info("登录用户:{} 不存在.", phonenumber);
+            throw new UserException("user.not.exists", phonenumber);
+        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+            log.info("登录用户:{} 已被停用.", phonenumber);
+            throw new UserException("user.blocked", phonenumber);
+        }
+
+        if (TenantHelper.isEnable()) {
+            SysUser sysUser = userData.selectTenantUserByPhonenumber(phonenumber, tenantId);
+            return MapstructUtils.convert(sysUser, SysUserVo.class);
+        }
+        SysUser userFind =  userData.selectByPhonenumber(phonenumber);
+        return MapstructUtils.convert(userFind, SysUserVo.class);
+    }
+
+    private SysUserVo loadUserByEmail(String tenantId, String email) {
+        SysUser query = new SysUser();
+        query.setEmail(email);
+        if(TenantHelper.isEnable()){
+            query.setTenantId(tenantId);
+        }
+
+        SysUser user = userData.findOneByCondition(query);
+
+        if (ObjectUtil.isNull(user)) {
+            log.info("登录用户:{} 不存在.", email);
+            throw new UserException("user.not.exists", email);
+        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+            log.info("登录用户:{} 已被停用.", email);
+            throw new UserException("user.blocked", email);
+        }
+        if (TenantHelper.isEnable()) {
+            SysUser sysUser = userData.selectTenantUserByEmail(email, tenantId);
+            return MapstructUtils.convert(sysUser, SysUserVo.class);
+        }
+        SysUser sysUser = userData.selectUserByEmail(email);
+        return MapstructUtils.convert(sysUser, SysUserVo.class);
+
+    }
+
+    private SysUserVo loadUserByOpenid(String openid) {
+        // 使用 openid 查询绑定用户 如未绑定用户 则根据业务自行处理 例如 创建默认用户
+        // todo 自行实现 userService.selectUserByOpenid(openid);
+        SysUserVo user = new SysUserVo();
+        if (ObjectUtil.isNull(user)) {
+            log.info("登录用户:{} 不存在.", openid);
+            // todo 用户不存在 业务逻辑自行实现
+        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
+            log.info("登录用户:{} 已被停用.", openid);
+            // todo 用户已被停用 业务逻辑自行实现
+        }
+        return user;
+    }
+
+    /**
+     * 构建登录用户
+     */
+    private LoginUser buildLoginUser(SysUserVo user) {
+        LoginUser loginUser = new LoginUser();
+        loginUser.setTenantId(user.getTenantId());
+        loginUser.setUserId(user.getId());
+        loginUser.setDeptId(user.getDeptId());
+        loginUser.setUsername(user.getUserName());
+        loginUser.setUserType(user.getUserType());
+        loginUser.setMenuPermission(permissionService.getMenuPermission(user.getId()));
+        loginUser.setRolePermission(permissionService.getRolePermission(user.getId()));
+        loginUser.setDeptName(ObjectUtil.isNull(user.getDept()) ? "" : user.getDept().getDeptName());
+        List<RoleDTO> roles = BeanUtil.copyToList(user.getRoles(), RoleDTO.class);
+        loginUser.setRoles(roles);
+        return loginUser;
+    }
+
+    /**
+     * 记录登录信息
+     *
+     * @param userId 用户ID
+     */
+    public void recordLoginInfo(Long userId) {
+        SysUser sysUser = new SysUser();
+        sysUser.setId(userId);
+        sysUser.setLoginIp(ServletUtils.getClientIP());
+        sysUser.setLoginDate(DateUtils.getNowDate());
+        sysUser.setUpdateBy(userId);
+        userData.save(sysUser);
+    }
+
+    /**
+     * 登录校验
+     */
+    private void checkLogin(LoginType loginType, String tenantId, String username, Supplier<Boolean> supplier) {
+        String errorKey = GlobalConstants.PWD_ERR_CNT_KEY + username;
+        String loginFail = Constants.LOGIN_FAIL;
+
+        // 获取用户登录错误次数(可自定义限制策略 例如: key + username + ip)
+        Integer errorNumber = RedisUtils.getCacheObject(errorKey);
+        // 锁定时间内登录 则踢出
+        if (ObjectUtil.isNotNull(errorNumber) && errorNumber.equals(maxRetryCount)) {
+            recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
+            throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
+        }
+
+        if (supplier.get()) {
+            // 是否第一次
+            errorNumber = ObjectUtil.isNull(errorNumber) ? 1 : errorNumber + 1;
+            // 达到规定错误次数 则锁定登录
+            if (errorNumber.equals(maxRetryCount)) {
+                RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
+                recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
+                throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
+            } else {
+                // 未达到规定错误次数 则递增
+                RedisUtils.setCacheObject(errorKey, errorNumber);
+                recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
+                throw new UserException(loginType.getRetryLimitCount(), errorNumber);
+            }
+        }
+
+        // 登录成功 清空错误次数
+        RedisUtils.deleteObject(errorKey);
+    }
+
+    private void checkTenant(String tenantId) {
+//        if (!TenantHelper.isEnable()) {
+//            return;
+//        }
+//        if (TenantConstants.DEFAULT_TENANT_ID.equals(tenantId)) {
+//            return;
+//        }
+//        SysTenantVo tenant = tenantService.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");
+//        }
+    }
+
+}

+ 106 - 0
iot-starter/src/main/java/cc/iotkit/web/service/SysRegisterService.java

@@ -0,0 +1,106 @@
+package cc.iotkit.web.service;
+
+import cc.iotkit.common.constant.Constants;
+import cc.iotkit.common.constant.GlobalConstants;
+import cc.iotkit.common.enums.UserType;
+import cc.iotkit.common.exception.BizException;
+import cc.iotkit.common.exception.user.UserException;
+import cc.iotkit.common.redis.utils.RedisUtils;
+import cc.iotkit.common.undefined.RegisterBody;
+import cc.iotkit.common.utils.MessageUtils;
+import cc.iotkit.common.utils.StringUtils;
+import cc.iotkit.common.web.config.properties.CaptchaProperties;
+import cc.iotkit.system.dto.bo.SysUserBo;
+import cc.iotkit.system.service.ISysUserService;
+import cn.dev33.satoken.secure.BCrypt;
+import lombok.RequiredArgsConstructor;
+
+import org.springframework.stereotype.Service;
+
+/**
+ * 注册校验方法
+ *
+ * @author Lion Li
+ */
+@RequiredArgsConstructor
+@Service
+public class SysRegisterService {
+
+    private final ISysUserService userService;
+    private final CaptchaProperties captchaProperties;
+
+    /**
+     * 注册
+     */
+    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.getEnable();
+        // 验证码开关
+        if (captchaEnabled) {
+            validateCaptcha(tenantId, username, registerBody.getCode(), registerBody.getUuid());
+        }
+        SysUserBo sysUser = new SysUserBo();
+        sysUser.setUserName(username);
+        sysUser.setNickName(username);
+        sysUser.setPassword(BCrypt.hashpw(password));
+        sysUser.setUserType(userType);
+
+        if (!userService.checkUserNameUnique(sysUser)) {
+            throw new UserException("user.register.save.error", username);
+        }
+        boolean regFlag = userService.registerUser(sysUser, tenantId);
+        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 BizException("验证码过期");
+
+        }
+        if (!code.equalsIgnoreCase(captcha)) {
+            recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.jcaptcha.error"));
+            throw new BizException("验证码错误");
+
+//            throw new CaptchaException();
+        }
+    }
+
+    /**
+     * 记录登录信息
+     *
+     * @param tenantId 租户ID
+     * @param username 用户名
+     * @param status   状态
+     * @param message  消息内容
+     * @return
+     */
+    private 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);
+    }
+
+}