luoyb
2024-05-18 ea53035f42734c08a16825ad77dc047a80781951
feat: 项目初始化
18个文件已添加
4个文件已修改
1654 ■■■■■ 已修改文件
ruoyi-admin/src/main/java/com/sunsail/SunSailApplication.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/sunsail/SunSailServletInitializer.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/sunsail/web/controller/AuthController.java 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/sunsail/web/controller/CaptchaController.java 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/sunsail/web/controller/IndexController.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/sunsail/web/domain/vo/CaptchaVo.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/sunsail/web/domain/vo/LoginTenantVo.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/sunsail/web/domain/vo/LoginVo.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/sunsail/web/domain/vo/TenantListVo.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/sunsail/web/listener/UserActionListener.java 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/sunsail/web/service/IAuthStrategy.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/sunsail/web/service/SysLoginService.java 232 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/sunsail/web/service/SysRegisterService.java 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/sunsail/web/service/impl/EmailAuthStrategy.java 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/sunsail/web/service/impl/PasswordAuthStrategy.java 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/sunsail/web/service/impl/SmsAuthStrategy.java 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/sunsail/web/service/impl/SocialAuthStrategy.java 131 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/sunsail/web/service/impl/XcxAuthStrategy.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/org/dromara/test/AssertUnitTest.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/org/dromara/test/DemoUnitTest.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/org/dromara/test/ParamUnitTest.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/org/dromara/test/TagUnitTest.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/sunsail/SunSailApplication.java
New file
@@ -0,0 +1,23 @@
package com.sunsail;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
/**
 * 启动程序
 *
 * @author Lion Li
 */
@SpringBootApplication
public class SunSailApplication {
    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(SunSailApplication.class);
        application.setApplicationStartup(new BufferingApplicationStartup(2048));
        application.run(args);
        System.out.println("(♥◠‿◠)ノ゙  RuoYi-Vue-Plus启动成功   ლ(´ڡ`ლ)゙");
    }
}
ruoyi-admin/src/main/java/com/sunsail/SunSailServletInitializer.java
New file
@@ -0,0 +1,18 @@
package com.sunsail;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
/**
 * web容器中进行部署
 *
 * @author Lion Li
 */
public class SunSailServletInitializer extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(SunSailApplication.class);
    }
}
ruoyi-admin/src/main/java/com/sunsail/web/controller/AuthController.java
New file
@@ -0,0 +1,209 @@
package com.sunsail.web.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.sunsail.common.core.utils.MapstructUtils;
import com.sunsail.common.core.utils.MessageUtils;
import com.sunsail.common.core.utils.StreamUtils;
import com.sunsail.common.core.utils.StringUtils;
import com.sunsail.common.core.utils.ValidatorUtils;
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 com.sunsail.common.core.constant.UserConstants;
import com.sunsail.common.core.domain.R;
import com.sunsail.common.core.domain.model.LoginBody;
import com.sunsail.common.core.domain.model.RegisterBody;
import com.sunsail.common.core.domain.model.SocialLoginBody;
import com.sunsail.common.encrypt.annotation.ApiEncrypt;
import com.sunsail.common.json.utils.JsonUtils;
import com.sunsail.common.satoken.utils.LoginHelper;
import com.sunsail.common.social.config.properties.SocialLoginConfigProperties;
import com.sunsail.common.social.config.properties.SocialProperties;
import com.sunsail.common.social.utils.SocialUtils;
import com.sunsail.common.tenant.helper.TenantHelper;
import com.sunsail.common.websocket.utils.WebSocketUtils;
import com.sunsail.system.domain.SysClient;
import com.sunsail.system.domain.bo.SysTenantBo;
import com.sunsail.system.domain.vo.SysTenantVo;
import com.sunsail.system.service.ISysClientService;
import com.sunsail.system.service.ISysConfigService;
import com.sunsail.system.service.ISysSocialService;
import com.sunsail.system.service.ISysTenantService;
import com.sunsail.web.domain.vo.LoginTenantVo;
import com.sunsail.web.domain.vo.LoginVo;
import com.sunsail.web.domain.vo.TenantListVo;
import com.sunsail.web.service.IAuthStrategy;
import com.sunsail.web.service.SysLoginService;
import com.sunsail.web.service.SysRegisterService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.net.URL;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
 * 认证
 *
 * @author Lion Li
 */
@Slf4j
@SaIgnore
@RequiredArgsConstructor
@RestController
@RequestMapping("/auth")
public class AuthController {
    private final SocialProperties socialProperties;
    private final SysLoginService loginService;
    private final SysRegisterService registerService;
    private final ISysConfigService configService;
    private final ISysTenantService tenantService;
    private final ISysSocialService socialUserService;
    private final ISysClientService clientService;
    private final ScheduledExecutorService scheduledExecutorService;
    /**
     * 登录方法
     *
     * @param body 登录信息
     * @return 结果
     */
    @ApiEncrypt
    @PostMapping("/login")
    public R<LoginVo> login(@RequestBody String body) {
        LoginBody loginBody = JsonUtils.parseObject(body, LoginBody.class);
        ValidatorUtils.validate(loginBody);
        // 授权类型和客户端id
        String clientId = loginBody.getClientId();
        String grantType = loginBody.getGrantType();
        SysClient client = clientService.queryByClientId(clientId);
        // 查询不到 client 或 client 内不包含 grantType
        if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), grantType)) {
            log.info("客户端id: {} 认证类型:{} 异常!.", clientId, grantType);
            return R.fail(MessageUtils.message("auth.grant.type.error"));
        } else if (!UserConstants.NORMAL.equals(client.getStatus())) {
            return R.fail(MessageUtils.message("auth.grant.type.blocked"));
        }
        // 校验租户
        loginService.checkTenant(loginBody.getTenantId());
        // 登录
        LoginVo loginVo = IAuthStrategy.login(body, client, grantType);
        Long userId = LoginHelper.getUserId();
        scheduledExecutorService.schedule(() -> {
            WebSocketUtils.sendMessage(userId, "欢迎登录RuoYi-Vue-Plus后台管理系统");
        }, 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());
        }
        loginService.socialRegister(authUserData);
        return R.ok();
    }
    /**
     * 取消授权
     *
     * @param socialId socialId
     */
    @DeleteMapping(value = "/unlock/{socialId}")
    public R<Void> unlockSocial(@PathVariable Long socialId) {
        Boolean rows = socialUserService.deleteWithValidById(socialId);
        return rows ? R.ok() : R.fail("取消授权失败");
    }
    /**
     * 退出登录
     */
    @PostMapping("/logout")
    public R<Void> logout() {
        loginService.logout();
        return R.ok("退出成功");
    }
    /**
     * 用户注册
     */
    @ApiEncrypt
    @PostMapping("/register")
    public R<Void> register(@Validated @RequestBody RegisterBody user) {
        if (!configService.selectRegisterEnabled(user.getTenantId())) {
            return R.fail("当前系统没有开启注册功能!");
        }
        registerService.register(user);
        return R.ok();
    }
    /**
     * 登录页面租户下拉框
     *
     * @return 租户列表
     */
    @GetMapping("/tenant/list")
    public R<LoginTenantVo> tenantList(HttpServletRequest request) throws Exception {
        List<SysTenantVo> tenantList = tenantService.queryList(new SysTenantBo());
        List<TenantListVo> voList = MapstructUtils.convert(tenantList, TenantListVo.class);
        // 获取域名
        String host;
        String referer = request.getHeader("referer");
        if (StringUtils.isNotBlank(referer)) {
            // 这里从referer中取值是为了本地使用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);
    }
}
ruoyi-admin/src/main/java/com/sunsail/web/controller/CaptchaController.java
New file
@@ -0,0 +1,136 @@
package com.sunsail.web.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import com.sunsail.common.core.constant.Constants;
import com.sunsail.common.core.constant.GlobalConstants;
import com.sunsail.common.core.domain.R;
import com.sunsail.common.core.utils.SpringUtils;
import com.sunsail.common.core.utils.StringUtils;
import com.sunsail.common.core.utils.reflect.ReflectUtils;
import com.sunsail.common.mail.config.properties.MailProperties;
import com.sunsail.common.mail.utils.MailUtils;
import com.sunsail.common.ratelimiter.annotation.RateLimiter;
import com.sunsail.common.ratelimiter.enums.LimitType;
import com.sunsail.common.redis.utils.RedisUtils;
import com.sunsail.common.web.config.properties.CaptchaProperties;
import com.sunsail.common.web.enums.CaptchaType;
import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.core.factory.SmsFactory;
import org.dromara.sms4j.provider.enumerate.SupplierType;
import com.sunsail.web.domain.vo.CaptchaVo;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
import java.util.LinkedHashMap;
/**
 * 验证码操作处理
 *
 * @author Lion Li
 */
@SaIgnore
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
public class CaptchaController {
    private final CaptchaProperties captchaProperties;
    private final MailProperties mailProperties;
    /**
     * 短信验证码
     *
     * @param phonenumber 用户手机号
     */
    @RateLimiter(key = "#phonenumber", time = 60, count = 1)
    @GetMapping("/resource/sms/code")
    public R<Void> smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) {
        String key = GlobalConstants.CAPTCHA_CODE_KEY + phonenumber;
        String code = RandomUtil.randomNumbers(4);
        RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
        // 验证码模板id 自行处理 (查数据库或写死均可)
        String templateId = "";
        LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
        map.put("code", code);
        SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.ALIBABA);
        SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, templateId, map);
        if (!"OK".equals(smsResponse.getCode())) {
            log.error("验证码短信发送异常 => {}", smsResponse);
            return R.fail(smsResponse.getMessage());
        }
        return R.ok();
    }
    /**
     * 邮箱验证码
     *
     * @param email 邮箱
     */
    @RateLimiter(key = "#email", time = 60, count = 1)
    @GetMapping("/resource/email/code")
    public R<Void> emailCode(@NotBlank(message = "{user.email.not.blank}") String email) {
        if (!mailProperties.getEnabled()) {
            return R.fail("当前系统没有开启邮箱功能!");
        }
        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());
            return R.fail(e.getMessage());
        }
        return R.ok();
    }
    /**
     * 生成验证码
     */
    @RateLimiter(time = 60, count = 10, limitType = LimitType.IP)
    @GetMapping("/auth/code")
    public R<CaptchaVo> getCode() {
        CaptchaVo captchaVo = new CaptchaVo();
        boolean captchaEnabled = captchaProperties.getEnable();
        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);
    }
}
ruoyi-admin/src/main/java/com/sunsail/web/controller/IndexController.java
New file
@@ -0,0 +1,32 @@
package com.sunsail.web.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import com.sunsail.common.core.config.RuoYiConfig;
import com.sunsail.common.core.utils.StringUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * 首页
 *
 * @author Lion Li
 */
@SaIgnore
@RequiredArgsConstructor
@RestController
public class IndexController {
    /**
     * 系统基础配置
     */
    private final RuoYiConfig ruoyiConfig;
    /**
     * 访问首页,提示语
     */
    @GetMapping("/")
    public String index() {
        return StringUtils.format("欢迎使用{}后台管理框架,当前版本:v{},请通过前端地址访问。", ruoyiConfig.getName(), ruoyiConfig.getVersion());
    }
}
ruoyi-admin/src/main/java/com/sunsail/web/domain/vo/CaptchaVo.java
New file
@@ -0,0 +1,25 @@
package com.sunsail.web.domain.vo;
import lombok.Data;
/**
 * 验证码信息
 *
 * @author Michelle.Chung
 */
@Data
public class CaptchaVo {
    /**
     * 是否开启验证码
     */
    private Boolean captchaEnabled = true;
    private String uuid;
    /**
     * 验证码图片
     */
    private String img;
}
ruoyi-admin/src/main/java/com/sunsail/web/domain/vo/LoginTenantVo.java
New file
@@ -0,0 +1,25 @@
package com.sunsail.web.domain.vo;
import lombok.Data;
import java.util.List;
/**
 * 登录租户对象
 *
 * @author Michelle.Chung
 */
@Data
public class LoginTenantVo {
    /**
     * 租户开关
     */
    private Boolean tenantEnabled;
    /**
     * 租户对象列表
     */
    private List<TenantListVo> voList;
}
ruoyi-admin/src/main/java/com/sunsail/web/domain/vo/LoginVo.java
New file
@@ -0,0 +1,54 @@
package com.sunsail.web.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;
}
ruoyi-admin/src/main/java/com/sunsail/web/domain/vo/TenantListVo.java
New file
@@ -0,0 +1,22 @@
package com.sunsail.web.domain.vo;
import com.sunsail.system.domain.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;
}
ruoyi-admin/src/main/java/com/sunsail/web/listener/UserActionListener.java
New file
@@ -0,0 +1,151 @@
package com.sunsail.web.listener;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.listener.SaTokenListener;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.sunsail.common.core.constant.CacheConstants;
import com.sunsail.common.core.constant.Constants;
import com.sunsail.common.core.domain.dto.UserOnlineDTO;
import com.sunsail.common.core.domain.model.LoginUser;
import com.sunsail.common.core.utils.MessageUtils;
import com.sunsail.common.core.utils.ServletUtils;
import com.sunsail.common.core.utils.SpringUtils;
import com.sunsail.common.core.utils.ip.AddressUtils;
import com.sunsail.common.log.event.LogininforEvent;
import com.sunsail.common.redis.utils.RedisUtils;
import com.sunsail.common.satoken.utils.LoginHelper;
import com.sunsail.web.service.SysLoginService;
import org.springframework.stereotype.Component;
import java.time.Duration;
/**
 * 用户行为 侦听器的实现
 *
 * @author Lion Li
 */
@RequiredArgsConstructor
@Component
@Slf4j
public class UserActionListener implements SaTokenListener {
    private final SaTokenConfig tokenConfig;
    private final SysLoginService loginService;
    /**
     * 每次登录时触发
     */
    @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();
        UserOnlineDTO dto = new UserOnlineDTO();
        dto.setIpaddr(ip);
        dto.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
        dto.setBrowser(userAgent.getBrowser().getName());
        dto.setOs(userAgent.getOs().getName());
        dto.setLoginTime(System.currentTimeMillis());
        dto.setTokenId(tokenValue);
        dto.setUserName(user.getUsername());
        dto.setClientKey(user.getClientKey());
        dto.setDeviceType(user.getDeviceType());
        dto.setDeptName(user.getDeptName());
        if(tokenConfig.getTimeout() == -1) {
            RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto);
        } else {
            RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto, 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);
        // 更新登录信息
        loginService.recordLoginInfo(user.getUserId(), ip);
        log.info("user doLogin, userId:{}, token:{}", loginId, tokenValue);
    }
    /**
     * 每次注销时触发
     */
    @Override
    public void doLogout(String loginType, Object loginId, String tokenValue) {
        RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
        log.info("user doLogout, userId:{}, token:{}", loginId, tokenValue);
    }
    /**
     * 每次被踢下线时触发
     */
    @Override
    public void doKickout(String loginType, Object loginId, String tokenValue) {
        RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
        log.info("user doKickout, userId:{}, token:{}", loginId, tokenValue);
    }
    /**
     * 每次被顶下线时触发
     */
    @Override
    public void doReplaced(String loginType, Object loginId, String tokenValue) {
        RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
        log.info("user doReplaced, userId:{}, token:{}", loginId, tokenValue);
    }
    /**
     * 每次被封禁时触发
     */
    @Override
    public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {
    }
    /**
     * 每次被解封时触发
     */
    @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) {
    }
}
ruoyi-admin/src/main/java/com/sunsail/web/service/IAuthStrategy.java
New file
@@ -0,0 +1,36 @@
package com.sunsail.web.service;
import com.sunsail.common.core.exception.ServiceException;
import com.sunsail.common.core.utils.SpringUtils;
import com.sunsail.system.domain.SysClient;
import com.sunsail.web.domain.vo.LoginVo;
/**
 * 授权策略
 *
 * @author Michelle.Chung
 */
public interface IAuthStrategy {
    String BASE_NAME = "AuthStrategy";
    /**
     * 登录
     */
    static LoginVo login(String body, SysClient 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, SysClient client);
}
ruoyi-admin/src/main/java/com/sunsail/web/service/SysLoginService.java
New file
@@ -0,0 +1,232 @@
package com.sunsail.web.service;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthUser;
import com.sunsail.common.core.constant.Constants;
import com.sunsail.common.core.constant.GlobalConstants;
import com.sunsail.common.core.constant.TenantConstants;
import com.sunsail.common.core.domain.dto.RoleDTO;
import com.sunsail.common.core.domain.model.LoginUser;
import com.sunsail.common.core.enums.LoginType;
import com.sunsail.common.core.enums.TenantStatus;
import com.sunsail.common.core.exception.user.UserException;
import com.sunsail.common.core.utils.*;
import com.sunsail.common.log.event.LogininforEvent;
import com.sunsail.common.mybatis.helper.DataPermissionHelper;
import com.sunsail.common.redis.utils.RedisUtils;
import com.sunsail.common.satoken.utils.LoginHelper;
import com.sunsail.common.tenant.exception.TenantException;
import com.sunsail.common.tenant.helper.TenantHelper;
import com.sunsail.system.domain.SysUser;
import com.sunsail.system.domain.bo.SysSocialBo;
import com.sunsail.system.domain.vo.SysSocialVo;
import com.sunsail.system.domain.vo.SysTenantVo;
import com.sunsail.system.domain.vo.SysUserVo;
import com.sunsail.system.mapper.SysUserMapper;
import com.sunsail.system.service.ISysPermissionService;
import com.sunsail.system.service.ISysSocialService;
import com.sunsail.system.service.ISysTenantService;
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 {
    @Value("${user.password.maxRetryCount}")
    private Integer maxRetryCount;
    @Value("${user.password.lockTime}")
    private Integer lockTime;
    private final ISysTenantService tenantService;
    private final ISysPermissionService permissionService;
    private final ISysSocialService sysSocialService;
    private final SysUserMapper userMapper;
    /**
     * 绑定第三方用户
     *
     * @param authUserData 授权响应实体
     * @return 统一响应实体
     */
    public void socialRegister(AuthUser authUserData) {
        String authId = authUserData.getSource() + authUserData.getUuid();
        // 第三方用户信息
        SysSocialBo bo = BeanUtil.toBean(authUserData, SysSocialBo.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<SysSocialVo> list = sysSocialService.selectByAuthId(authId);
        if (CollUtil.isEmpty(list)) {
            // 没有绑定用户, 新增用户信息
            sysSocialService.insertByBo(bo);
        } else {
            // 更新用户信息
            bo.setId(list.get(0).getId());
            sysSocialService.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) {
            }
        }
    }
    /**
     * 记录登录信息
     *
     * @param tenantId 租户ID
     * @param username 用户名
     * @param status   状态
     * @param message  消息内容
     */
    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 LoginUser buildLoginUser(SysUserVo user) {
        LoginUser loginUser = new LoginUser();
        loginUser.setTenantId(user.getTenantId());
        loginUser.setUserId(user.getUserId());
        loginUser.setDeptId(user.getDeptId());
        loginUser.setUsername(user.getUserName());
        loginUser.setNickname(user.getNickName());
        loginUser.setUserType(user.getUserType());
        loginUser.setMenuPermission(permissionService.getMenuPermission(user.getUserId()));
        loginUser.setRolePermission(permissionService.getRolePermission(user.getUserId()));
        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, String ip) {
        SysUser sysUser = new SysUser();
        sysUser.setUserId(userId);
        sysUser.setLoginIp(ip);
        sysUser.setLoginDate(DateUtils.getNowDate());
        sysUser.setUpdateBy(userId);
        DataPermissionHelper.ignore(() -> userMapper.updateById(sysUser));
    }
    /**
     * 登录校验
     */
    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;
        // 获取用户登录错误次数,默认为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");
        }
        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");
        }
    }
}
ruoyi-admin/src/main/java/com/sunsail/web/service/SysRegisterService.java
New file
@@ -0,0 +1,117 @@
package com.sunsail.web.service;
import cn.dev33.satoken.secure.BCrypt;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import com.sunsail.common.core.constant.Constants;
import com.sunsail.common.core.constant.GlobalConstants;
import com.sunsail.common.core.domain.model.RegisterBody;
import com.sunsail.common.core.enums.UserType;
import com.sunsail.common.core.exception.user.CaptchaException;
import com.sunsail.common.core.exception.user.CaptchaExpireException;
import com.sunsail.common.core.exception.user.UserException;
import com.sunsail.common.core.utils.MessageUtils;
import com.sunsail.common.core.utils.ServletUtils;
import com.sunsail.common.core.utils.SpringUtils;
import com.sunsail.common.core.utils.StringUtils;
import com.sunsail.common.log.event.LogininforEvent;
import com.sunsail.common.redis.utils.RedisUtils;
import com.sunsail.common.tenant.helper.TenantHelper;
import com.sunsail.common.web.config.properties.CaptchaProperties;
import com.sunsail.system.domain.SysUser;
import com.sunsail.system.domain.bo.SysUserBo;
import com.sunsail.system.mapper.SysUserMapper;
import com.sunsail.system.service.ISysUserService;
import org.springframework.stereotype.Service;
/**
 * 注册校验方法
 *
 * @author Lion Li
 */
@RequiredArgsConstructor
@Service
public class SysRegisterService {
    private final ISysUserService userService;
    private final SysUserMapper userMapper;
    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);
        boolean exist = TenantHelper.dynamic(tenantId, () -> {
            return userMapper.exists(new LambdaQueryWrapper<SysUser>()
                .eq(SysUser::getUserName, sysUser.getUserName())
                .ne(ObjectUtil.isNotNull(sysUser.getUserId()), SysUser::getUserId, sysUser.getUserId()));
        });
        if (exist) {
            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 CaptchaExpireException();
        }
        if (!code.equalsIgnoreCase(captcha)) {
            recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.jcaptcha.error"));
            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);
    }
}
ruoyi-admin/src/main/java/com/sunsail/web/service/impl/EmailAuthStrategy.java
New file
@@ -0,0 +1,107 @@
package com.sunsail.web.service.impl;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.sunsail.common.core.constant.Constants;
import com.sunsail.common.core.constant.GlobalConstants;
import com.sunsail.common.core.domain.model.EmailLoginBody;
import com.sunsail.common.core.domain.model.LoginUser;
import com.sunsail.common.core.enums.LoginType;
import com.sunsail.common.core.enums.UserStatus;
import com.sunsail.common.core.exception.user.CaptchaExpireException;
import com.sunsail.common.core.exception.user.UserException;
import com.sunsail.common.core.utils.MessageUtils;
import com.sunsail.common.core.utils.StringUtils;
import com.sunsail.common.core.utils.ValidatorUtils;
import com.sunsail.common.json.utils.JsonUtils;
import com.sunsail.common.redis.utils.RedisUtils;
import com.sunsail.common.satoken.utils.LoginHelper;
import com.sunsail.common.tenant.helper.TenantHelper;
import com.sunsail.system.domain.SysClient;
import com.sunsail.system.domain.SysUser;
import com.sunsail.system.domain.vo.SysUserVo;
import com.sunsail.system.mapper.SysUserMapper;
import com.sunsail.web.domain.vo.LoginVo;
import com.sunsail.web.service.IAuthStrategy;
import com.sunsail.web.service.SysLoginService;
import org.springframework.stereotype.Service;
/**
 * 邮件认证策略
 *
 * @author Michelle.Chung
 */
@Slf4j
@Service("email" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class EmailAuthStrategy implements IAuthStrategy {
    private final SysLoginService loginService;
    private final SysUserMapper userMapper;
    @Override
    public LoginVo login(String body, SysClient client) {
        EmailLoginBody loginBody = JsonUtils.parseObject(body, EmailLoginBody.class);
        ValidatorUtils.validate(loginBody);
        String tenantId = loginBody.getTenantId();
        String email = loginBody.getEmail();
        String emailCode = loginBody.getEmailCode();
        // 通过邮箱查找用户
        SysUserVo user = loadUserByEmail(tenantId, email);
        loginService.checkLogin(LoginType.EMAIL, tenantId, user.getUserName(), () -> !validateEmailCode(tenantId, email, emailCode));
        // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
        LoginUser loginUser = loginService.buildLoginUser(user);
        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);
    }
    private SysUserVo loadUserByEmail(String tenantId, String email) {
        return TenantHelper.dynamic(tenantId, () -> {
            SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
                .select(SysUser::getEmail, SysUser::getStatus)
                .eq(SysUser::getEmail, email));
            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);
            }
            return userMapper.selectUserByEmail(email);
        });
    }
}
ruoyi-admin/src/main/java/com/sunsail/web/service/impl/PasswordAuthStrategy.java
New file
@@ -0,0 +1,126 @@
package com.sunsail.web.service.impl;
import cn.dev33.satoken.secure.BCrypt;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.sunsail.common.core.constant.Constants;
import com.sunsail.common.core.constant.GlobalConstants;
import com.sunsail.common.core.domain.model.LoginUser;
import com.sunsail.common.core.domain.model.PasswordLoginBody;
import com.sunsail.common.core.enums.LoginType;
import com.sunsail.common.core.enums.UserStatus;
import com.sunsail.common.core.exception.user.CaptchaException;
import com.sunsail.common.core.exception.user.CaptchaExpireException;
import com.sunsail.common.core.exception.user.UserException;
import com.sunsail.common.core.utils.MessageUtils;
import com.sunsail.common.core.utils.StringUtils;
import com.sunsail.common.core.utils.ValidatorUtils;
import com.sunsail.common.json.utils.JsonUtils;
import com.sunsail.common.redis.utils.RedisUtils;
import com.sunsail.common.satoken.utils.LoginHelper;
import com.sunsail.common.tenant.helper.TenantHelper;
import com.sunsail.common.web.config.properties.CaptchaProperties;
import com.sunsail.system.domain.SysClient;
import com.sunsail.system.domain.SysUser;
import com.sunsail.system.domain.vo.SysUserVo;
import com.sunsail.system.mapper.SysUserMapper;
import com.sunsail.web.domain.vo.LoginVo;
import com.sunsail.web.service.IAuthStrategy;
import com.sunsail.web.service.SysLoginService;
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;
    private final SysUserMapper userMapper;
    @Override
    public LoginVo login(String body, SysClient 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();
        boolean captchaEnabled = captchaProperties.getEnable();
        // 验证码开关
        if (captchaEnabled) {
            validateCaptcha(tenantId, username, code, uuid);
        }
        SysUserVo user = loadUserByUsername(tenantId, username);
        loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
        // 此处可根据登录用户的数据不同 自行创建 loginUser
        LoginUser loginUser = loginService.buildLoginUser(user);
        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();
        }
    }
    private SysUserVo loadUserByUsername(String tenantId, String username) {
        return TenantHelper.dynamic(tenantId, () -> {
            SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
                .select(SysUser::getUserName, SysUser::getStatus)
                .eq(SysUser::getUserName, username));
            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);
            }
            return userMapper.selectUserByUserName(username);
        });
    }
}
ruoyi-admin/src/main/java/com/sunsail/web/service/impl/SmsAuthStrategy.java
New file
@@ -0,0 +1,107 @@
package com.sunsail.web.service.impl;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.sunsail.common.core.constant.Constants;
import com.sunsail.common.core.constant.GlobalConstants;
import com.sunsail.common.core.domain.model.LoginUser;
import com.sunsail.common.core.domain.model.SmsLoginBody;
import com.sunsail.common.core.enums.LoginType;
import com.sunsail.common.core.enums.UserStatus;
import com.sunsail.common.core.exception.user.CaptchaExpireException;
import com.sunsail.common.core.exception.user.UserException;
import com.sunsail.common.core.utils.MessageUtils;
import com.sunsail.common.core.utils.StringUtils;
import com.sunsail.common.core.utils.ValidatorUtils;
import com.sunsail.common.json.utils.JsonUtils;
import com.sunsail.common.redis.utils.RedisUtils;
import com.sunsail.common.satoken.utils.LoginHelper;
import com.sunsail.common.tenant.helper.TenantHelper;
import com.sunsail.system.domain.SysClient;
import com.sunsail.system.domain.SysUser;
import com.sunsail.system.domain.vo.SysUserVo;
import com.sunsail.system.mapper.SysUserMapper;
import com.sunsail.web.domain.vo.LoginVo;
import com.sunsail.web.service.IAuthStrategy;
import com.sunsail.web.service.SysLoginService;
import org.springframework.stereotype.Service;
/**
 * 短信认证策略
 *
 * @author Michelle.Chung
 */
@Slf4j
@Service("sms" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class SmsAuthStrategy implements IAuthStrategy {
    private final SysLoginService loginService;
    private final SysUserMapper userMapper;
    @Override
    public LoginVo login(String body, SysClient client) {
        SmsLoginBody loginBody = JsonUtils.parseObject(body, SmsLoginBody.class);
        ValidatorUtils.validate(loginBody);
        String tenantId = loginBody.getTenantId();
        String phonenumber = loginBody.getPhonenumber();
        String smsCode = loginBody.getSmsCode();
        // 通过手机号查找用户
        SysUserVo user = loadUserByPhonenumber(tenantId, phonenumber);
        loginService.checkLogin(LoginType.SMS, tenantId, user.getUserName(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
        // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
        LoginUser loginUser = loginService.buildLoginUser(user);
        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);
    }
    private SysUserVo loadUserByPhonenumber(String tenantId, String phonenumber) {
        return TenantHelper.dynamic(tenantId, () -> {
            SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
                .select(SysUser::getPhonenumber, SysUser::getStatus)
                .eq(SysUser::getPhonenumber, phonenumber));
            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);
            }
            return userMapper.selectUserByPhonenumber(phonenumber);
        });
    }
}
ruoyi-admin/src/main/java/com/sunsail/web/service/impl/SocialAuthStrategy.java
New file
@@ -0,0 +1,131 @@
package com.sunsail.web.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.core.util.ObjectUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.http.Method;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser;
import com.sunsail.common.core.domain.model.LoginUser;
import com.sunsail.common.core.domain.model.SocialLoginBody;
import com.sunsail.common.core.enums.UserStatus;
import com.sunsail.common.core.exception.ServiceException;
import com.sunsail.common.core.exception.user.UserException;
import com.sunsail.common.core.utils.ValidatorUtils;
import com.sunsail.common.json.utils.JsonUtils;
import com.sunsail.common.satoken.utils.LoginHelper;
import com.sunsail.common.social.config.properties.SocialProperties;
import com.sunsail.common.social.utils.SocialUtils;
import com.sunsail.common.tenant.helper.TenantHelper;
import com.sunsail.system.domain.SysClient;
import com.sunsail.system.domain.SysUser;
import com.sunsail.system.domain.vo.SysSocialVo;
import com.sunsail.system.domain.vo.SysUserVo;
import com.sunsail.system.mapper.SysUserMapper;
import com.sunsail.system.service.ISysSocialService;
import com.sunsail.web.domain.vo.LoginVo;
import com.sunsail.web.service.IAuthStrategy;
import com.sunsail.web.service.SysLoginService;
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 ISysSocialService sysSocialService;
    private final SysUserMapper userMapper;
    private final SysLoginService loginService;
    /**
     * 登录-第三方授权登录
     *
     * @param body     登录信息
     * @param client   客户端信息
     */
    @Override
    public LoginVo login(String body, SysClient 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<SysSocialVo> list = sysSocialService.selectByAuthId(authUserData.getSource() + authUserData.getUuid());
        if (CollUtil.isEmpty(list)) {
            throw new ServiceException("你还没有绑定第三方账号,绑定后才可以登录!");
        }
        Optional<SysSocialVo> opt = list.stream().filter(x -> x.getTenantId().equals(loginBody.getTenantId())).findAny();
        if (opt.isEmpty()) {
            throw new ServiceException("对不起,你没有权限登录当前租户!");
        }
        SysSocialVo social = opt.get();
        // 查找用户
        SysUserVo user = loadUser(social.getTenantId(), social.getUserId());
        // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
        LoginUser loginUser = loginService.buildLoginUser(user);
        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 SysUserVo loadUser(String tenantId, Long userId) {
        return TenantHelper.dynamic(tenantId, () -> {
            SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
                .select(SysUser::getUserName, SysUser::getStatus)
                .eq(SysUser::getUserId, userId));
            if (ObjectUtil.isNull(user)) {
                log.info("登录用户:{} 不存在.", "");
                throw new UserException("user.not.exists", "");
            } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
                log.info("登录用户:{} 已被停用.", "");
                throw new UserException("user.blocked", "");
            }
            return userMapper.selectUserByUserName(user.getUserName());
        });
    }
}
ruoyi-admin/src/main/java/com/sunsail/web/service/impl/XcxAuthStrategy.java
New file
@@ -0,0 +1,91 @@
package com.sunsail.web.service.impl;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.sunsail.common.core.domain.model.XcxLoginBody;
import com.sunsail.common.core.domain.model.XcxLoginUser;
import com.sunsail.common.core.enums.UserStatus;
import com.sunsail.common.core.utils.ValidatorUtils;
import com.sunsail.common.json.utils.JsonUtils;
import com.sunsail.common.satoken.utils.LoginHelper;
import com.sunsail.system.domain.SysClient;
import com.sunsail.system.domain.vo.SysUserVo;
import com.sunsail.web.domain.vo.LoginVo;
import com.sunsail.web.service.IAuthStrategy;
import com.sunsail.web.service.SysLoginService;
import org.springframework.stereotype.Service;
/**
 * 邮件认证策略
 *
 * @author Michelle.Chung
 */
@Slf4j
@Service("xcx" + IAuthStrategy.BASE_NAME)
@RequiredArgsConstructor
public class XcxAuthStrategy implements IAuthStrategy {
    private final SysLoginService loginService;
    @Override
    public LoginVo login(String body, SysClient 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 = "";
        // 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
        SysUserVo user = loadUserByOpenid(openid);
        // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
        XcxLoginUser loginUser = new XcxLoginUser();
        loginUser.setTenantId(user.getTenantId());
        loginUser.setUserId(user.getUserId());
        loginUser.setUsername(user.getUserName());
        loginUser.setNickname(user.getNickName());
        loginUser.setUserType(user.getUserType());
        loginUser.setClientKey(client.getClientKey());
        loginUser.setDeviceType(client.getDeviceType());
        loginUser.setOpenid(openid);
        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;
    }
    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;
    }
}
ruoyi-admin/src/test/java/org/dromara/test/AssertUnitTest.java
@@ -1,4 +1,4 @@
package org.dromara.test;
package com.sunsail.test;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
ruoyi-admin/src/test/java/org/dromara/test/DemoUnitTest.java
@@ -1,6 +1,6 @@
package org.dromara.test;
package com.sunsail.test;
import org.dromara.common.core.config.RuoYiConfig;
import com.sunsail.common.core.config.RuoYiConfig;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
ruoyi-admin/src/test/java/org/dromara/test/ParamUnitTest.java
@@ -1,6 +1,6 @@
package org.dromara.test;
package com.sunsail.test;
import org.dromara.common.core.enums.UserType;
import com.sunsail.common.core.enums.UserType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
ruoyi-admin/src/test/java/org/dromara/test/TagUnitTest.java
@@ -1,4 +1,4 @@
package org.dromara.test;
package com.sunsail.test;
import org.junit.jupiter.api.*;
import org.springframework.boot.test.context.SpringBootTest;