|
|
@@ -1,30 +1,58 @@
|
|
|
package com.jtzx.crm.module.api.web.auth;
|
|
|
|
|
|
|
|
|
-import com.jtzx.crm.aspect.extension.SysLogAspectExtensionForSysUserLogin;
|
|
|
+import cc.uncarbon.framework.core.constant.HelioConstant;
|
|
|
import cc.uncarbon.framework.core.context.TenantContextHolder;
|
|
|
import cc.uncarbon.framework.core.context.UserContext;
|
|
|
import cc.uncarbon.framework.core.context.UserContextHolder;
|
|
|
+import cc.uncarbon.framework.core.exception.BusinessException;
|
|
|
+import cc.uncarbon.framework.ratelimit.annotation.UseRateLimit;
|
|
|
import cc.uncarbon.framework.web.model.response.ApiResult;
|
|
|
+import cn.dev33.satoken.annotation.SaCheckLogin;
|
|
|
+import cn.dev33.satoken.spring.SpringMVCUtil;
|
|
|
+import cn.hutool.core.collection.CollUtil;
|
|
|
+import cn.hutool.core.date.DateTime;
|
|
|
+import cn.hutool.core.date.DateUtil;
|
|
|
+import cn.hutool.core.lang.Dict;
|
|
|
+import cn.hutool.core.util.StrUtil;
|
|
|
+import com.google.common.collect.Maps;
|
|
|
+import com.jtzx.crm.aspect.extension.SysLogAspectExtensionForSmsCodeLogin;
|
|
|
+import com.jtzx.crm.aspect.extension.SysLogAspectExtensionForSysUserAccountLogin;
|
|
|
+import com.jtzx.crm.aspect.extension.SysLogAspectExtensionForSysUserSmsCodeLogin;
|
|
|
import com.jtzx.crm.helper.CaptchaHelper;
|
|
|
import com.jtzx.crm.helper.RolePermissionCacheHelper;
|
|
|
import com.jtzx.crm.module.api.constant.ApiConstant;
|
|
|
+import com.jtzx.crm.module.api.constant.LimitConstant;
|
|
|
+import com.jtzx.crm.module.api.constant.RedisConstant;
|
|
|
import com.jtzx.crm.module.api.model.interior.AdminCaptchaContainer;
|
|
|
import com.jtzx.crm.module.api.model.response.AdminCaptchaVO;
|
|
|
+import com.jtzx.crm.module.api.model.response.LoginRespDataVO;
|
|
|
+import com.jtzx.crm.module.api.ratelimit.AccountRateLimitStrategy;
|
|
|
+import com.jtzx.crm.module.api.ratelimit.SmsCodeLoginRateLimitStrategy;
|
|
|
+import com.jtzx.crm.module.api.ratelimit.SmsCodeRateLimitStrategy;
|
|
|
import com.jtzx.crm.module.api.util.CrmAdminStpUtil;
|
|
|
+import com.jtzx.crm.module.api.util.ReqIpUtil;
|
|
|
+import com.jtzx.crm.module.sms.facade.SmsFacade;
|
|
|
import com.jtzx.crm.module.sys.annotation.SysLog;
|
|
|
import com.jtzx.crm.module.sys.model.request.SysUserLoginDTO;
|
|
|
import com.jtzx.crm.module.sys.model.response.SysUserLoginBO;
|
|
|
import com.jtzx.crm.module.sys.model.response.SysUserLoginVO;
|
|
|
import com.jtzx.crm.module.sys.service.SysUserService;
|
|
|
-import cn.dev33.satoken.annotation.SaCheckLogin;
|
|
|
import io.swagger.v3.oas.annotations.Operation;
|
|
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
|
import jakarta.validation.Valid;
|
|
|
+import jakarta.validation.constraints.Pattern;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.dromara.sms4j.api.entity.SmsResponse;
|
|
|
+import org.dromara.sms4j.comm.utils.SmsUtils;
|
|
|
+import org.dromara.sms4j.core.factory.SmsFactory;
|
|
|
+import org.springframework.data.redis.core.RedisTemplate;
|
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+
|
|
|
|
|
|
@Tag(name = "后台管理-鉴权接口")
|
|
|
@RequestMapping(value = ApiConstant.HTTP_API_URL_PREFIX + "/api/v1")
|
|
|
@@ -39,35 +67,160 @@ public class AdminAuthController {
|
|
|
|
|
|
private final CaptchaHelper captchaHelper;
|
|
|
|
|
|
+ private final RedisTemplate<String, Object> stringSetRedisTemplate;
|
|
|
|
|
|
- @SysLog(value = "登录后台用户", syncSave = true, extension = SysLogAspectExtensionForSysUserLogin.class, queryIPLocation = true)
|
|
|
- @Operation(summary = "登录")
|
|
|
- @PostMapping(value = "/auth/login")
|
|
|
- public ApiResult<SysUserLoginVO> login(@RequestBody @Valid SysUserLoginDTO dto) {
|
|
|
- // 登录验证码核验;前端项目搜索关键词「 Helio: 登录验证码」
|
|
|
- // AdminApiErrorEnum.CAPTCHA_VALIDATE_FAILED.assertTrue(captchaHelper.validate(dto.getCaptchaId(), dto.getCaptchaAnswer()))
|
|
|
+ private final SmsFacade smsFacade;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 账号密码登陆,输错5次,账号封禁 1800秒(半个小时),目前测试用 300 秒-5分钟,输错超3次,需要填写动态验证码
|
|
|
+ * 这一步的登陆,不会返回token,登陆信息会存入redis 中 半个小时,半小时之内没有后续操作,就自动失效,后续其它操作都不能进行
|
|
|
+ * @param dto
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ @SysLog(value = "第一步后台用户账号密码登录", extension = SysLogAspectExtensionForSysUserAccountLogin.class, queryIPLocation = true)
|
|
|
+ @Operation(summary = "第一步账号密码登录")
|
|
|
+ @UseRateLimit(duration = 300, max = 5, strategy = AccountRateLimitStrategy.class, mark = "AccountLogin")
|
|
|
+ @PostMapping(value = "/auth/accountlogin")
|
|
|
+ public ApiResult<LoginRespDataVO> login(@RequestBody @Valid SysUserLoginDTO dto) {
|
|
|
+ //限流时redis中的key
|
|
|
+ String key = RedisConstant.ACCOUNT_REDIS_KEY_PREFIX + "account:" + dto.getUsername() + ":AccountLogin";
|
|
|
+ LoginRespDataVO resultData = this.getResultData(null, key);
|
|
|
+ resultData.setCommitTopCount(LimitConstant.login_max);
|
|
|
+ //计算封禁结束时间
|
|
|
+ DateTime dateTime = DateUtil.offsetSecond(DateUtil.date(), LimitConstant.login_duration);
|
|
|
+ resultData.setEndDateTime(DateUtil.formatDateTime(dateTime));
|
|
|
+ //封禁结束时间存入redis中
|
|
|
+ stringSetRedisTemplate.opsForValue()
|
|
|
+ .set(RedisConstant.ACCOUNT_LOGIN_FAILED_END_DATETIME_PREFIX + dto.getUsername(),
|
|
|
+ resultData.getEndDateTime(),
|
|
|
+ LimitConstant.login_duration,
|
|
|
+ TimeUnit.SECONDS);
|
|
|
|
|
|
// RPC调用, 失败抛异常, 成功返回用户信息
|
|
|
SysUserLoginBO userInfo = sysUserService.adminLogin(dto);
|
|
|
+ if (userInfo == null) {
|
|
|
+ resultData.setStatus(false);
|
|
|
+ //前两次不返回图形验证码,如果第三次还失败,就返回图形验证码
|
|
|
+ if (LimitConstant.login_fail_count > 0 && resultData.getCommitCount() >= LimitConstant.login_fail_count) {
|
|
|
+ resultData.setShowPicCode(true);
|
|
|
+ //返回图形验证码
|
|
|
+ resultData.setCaptchaVO(new AdminCaptchaVO(captchaHelper.generate()));
|
|
|
+ if (resultData.getCommitCount() > LimitConstant.login_fail_count) {
|
|
|
+ if (StrUtil.isEmpty(dto.getCaptchaId()) || StrUtil.isEmpty(dto.getCaptchaAnswer())) {
|
|
|
+ return ApiResult.data("图形验证码未填写", resultData).setCode(406);
|
|
|
+ }
|
|
|
+ // 登录验证码核验
|
|
|
+ if (!captchaHelper.validate(dto.getCaptchaId(), dto.getCaptchaAnswer())) {
|
|
|
+ return ApiResult.data("图形验证码错误", resultData).setCode(406);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return ApiResult.data("账号或密码错误", resultData).setCode(406);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (StrUtil.isEmpty(userInfo.getPhoneNo())) {
|
|
|
+ resultData.setStatus(false);
|
|
|
+ return ApiResult.data("账号未设置手机号", resultData).setCode(406);
|
|
|
+ }
|
|
|
+ //设置手机号
|
|
|
+ resultData.setMobile(userInfo.getPhoneNo());
|
|
|
+ //记住我
|
|
|
+ userInfo.setRememberMe(dto.getRememberMe());
|
|
|
+
|
|
|
+ //用户数据存入redis,有效期为 5 分钟,超时未进行 发送手机验证码,将无效
|
|
|
+ stringSetRedisTemplate.opsForValue()
|
|
|
+ .set(RedisConstant.LOGIN_USER_INFO_PREFIX + userInfo.getPhoneNo(), userInfo, ApiConstant.LOGIN_USER_INFO, TimeUnit.SECONDS);
|
|
|
+ return ApiResult.data("登录成功", resultData);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送登陆验证码,5分钟最多发3次
|
|
|
+ * @param mobile
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ @SysLog(value = "第二步后台用户登录发送登录短信验证码", extension = SysLogAspectExtensionForSmsCodeLogin.class, queryIPLocation = true)
|
|
|
+ @Operation(summary = "第二步发送登录短信验证码")
|
|
|
+ @GetMapping(value = "/auth/sms/smsCode")
|
|
|
+ @UseRateLimit(duration = 300, max = 3, strategy = SmsCodeRateLimitStrategy.class)
|
|
|
+ public ApiResult<String> smsCode(@RequestParam String username,
|
|
|
+ @Pattern(message = "手机号格式有误", regexp = HelioConstant.Regex.CHINA_MAINLAND_PHONE_NO)
|
|
|
+ @RequestParam(name = "mobile") String mobile) {
|
|
|
+
|
|
|
+ //计算封禁结束时间
|
|
|
+ DateTime dateTime = DateUtil.offsetSecond(DateUtil.date(), LimitConstant.smsCode_duration);
|
|
|
+ String formatDateTime = DateUtil.formatDateTime(dateTime);
|
|
|
+ //封禁结束时间存入redis中
|
|
|
+ stringSetRedisTemplate.opsForValue()
|
|
|
+ .set(RedisConstant.SMS_CODE_FAILED_END_DATETIME_PREFIX + mobile,
|
|
|
+ formatDateTime,
|
|
|
+ LimitConstant.smsCode_duration,
|
|
|
+ TimeUnit.SECONDS);
|
|
|
+ //账号密码redis中存储的用户信息
|
|
|
+ SysUserLoginBO userInfo = this.getLoginUserInfo(mobile);
|
|
|
+ String smsCode = SmsUtils.getRandomInt(RedisConstant.SMS_CODE_LENGTH);
|
|
|
+ log.info("smsCode--------->{}", smsCode);
|
|
|
+ LinkedHashMap<String, String> messages = Maps.newLinkedHashMap();
|
|
|
+ messages.put("code", smsCode);
|
|
|
+ SmsResponse smsResponse = SmsFactory.getSmsBlend().sendMessage(mobile, messages);
|
|
|
+ if (smsResponse.isSuccess()) {
|
|
|
+ smsFacade.set(RedisConstant.SMS_CODE_PREFIX + mobile, smsCode, RedisConstant.SMS_CODE_DURATION);
|
|
|
+ return ApiResult.data("发送成功");
|
|
|
+ }
|
|
|
+ return ApiResult.fail(406, "发送失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ @SysLog(value = "第三步后台用户手机验证码登录", extension = SysLogAspectExtensionForSysUserSmsCodeLogin.class, queryIPLocation = true)
|
|
|
+ @Operation(summary = "第三步验证码登录")
|
|
|
+ @UseRateLimit(duration = 600, max = 3, strategy = SmsCodeLoginRateLimitStrategy.class, mark = "smsCodeLogin")
|
|
|
+ @GetMapping(value = "/auth/smsCodeLogin")
|
|
|
+ public ApiResult<LoginRespDataVO> smsCodeLogin(@RequestParam String username,
|
|
|
+ @Pattern(message = "手机号格式有误", regexp = HelioConstant.Regex.CHINA_MAINLAND_PHONE_NO) @RequestParam String mobile,
|
|
|
+ @RequestParam String smsCode) {
|
|
|
+
|
|
|
+ //账号密码redis中存储的用户信息
|
|
|
+ SysUserLoginBO userInfo = this.getLoginUserInfo(mobile);
|
|
|
+
|
|
|
+ //限流时redis中的key
|
|
|
+ String key = RedisConstant.SMS_CODE_REDIS_KEY_PREFIX + "smsCodeLogin:" + mobile + ":smsCodeLogin";
|
|
|
+ LoginRespDataVO resultData = this.getResultData(null, key);
|
|
|
+ resultData.setCommitTopCount(LimitConstant.smsCode_login_max);
|
|
|
+ //计算封禁结束时间
|
|
|
+ DateTime dateTime = DateUtil.offsetSecond(DateUtil.date(), LimitConstant.smsCode_login_duration);
|
|
|
+ resultData.setEndDateTime(DateUtil.formatDateTime(dateTime));
|
|
|
+ //封禁结束时间存入redis中
|
|
|
+ stringSetRedisTemplate.opsForValue()
|
|
|
+ .set(RedisConstant.SMS_CODE_LOGIN_FAILED_END_DATETIME_PREFIX + mobile,
|
|
|
+ resultData.getEndDateTime(),
|
|
|
+ LimitConstant.smsCode_login_duration,
|
|
|
+ TimeUnit.SECONDS);
|
|
|
+ Object cacheData = smsFacade.get(RedisConstant.SMS_CODE_PREFIX + userInfo.getPhoneNo());
|
|
|
+ if (cacheData == null) {
|
|
|
+ resultData.setStatus(false);
|
|
|
+ return ApiResult.data("验证码错误", resultData).setCode(406);
|
|
|
+ }
|
|
|
+ String code = (String) cacheData;
|
|
|
+ if (!code.equals(smsCode)) {
|
|
|
+ resultData.setStatus(false);
|
|
|
+ return ApiResult.data("验证码错误", resultData).setCode(406);
|
|
|
+ }
|
|
|
|
|
|
// 构造用户上下文
|
|
|
UserContext userContext = UserContext.builder()
|
|
|
.userId(userInfo.getId())
|
|
|
.userName(userInfo.getUsername())
|
|
|
.userPhoneNo(userInfo.getPhoneNo())
|
|
|
- .userTypeStr("ADMIN_USER")
|
|
|
- .extraData(null)
|
|
|
+ .userTypeStr("GroupManager")//分组组长
|
|
|
+ .extraData(Dict.parse(userInfo))//用户信息保存在扩展数据中
|
|
|
.rolesIds(userInfo.getRoleIds())
|
|
|
.roles(userInfo.getRoles())
|
|
|
+ .clientIP(ReqIpUtil.getIpAddr(SpringMVCUtil.getRequest()))//获取用户登陆id
|
|
|
.build();
|
|
|
-
|
|
|
+ if (CollUtil.isNotEmpty(userInfo.getRoles()) && userInfo.getRoles().contains("Admin")) {
|
|
|
+ userContext.setUserTypeStr("Admin");//管理员
|
|
|
+ }
|
|
|
// 将用户ID注册到 SA-Token ,并附加一些业务字段
|
|
|
- CrmAdminStpUtil.login(userInfo.getId(), dto.getRememberMe());
|
|
|
+ CrmAdminStpUtil.login(userInfo.getId(), userInfo.getRememberMe());
|
|
|
CrmAdminStpUtil.getSession().set(UserContext.CAMEL_NAME, userContext);
|
|
|
-
|
|
|
- // 更新角色-权限缓存
|
|
|
- rolePermissionCacheHelper.putCache(userInfo.getRoleIdPermissionMap());
|
|
|
-
|
|
|
// 返回登录token
|
|
|
SysUserLoginVO tokenInfo = SysUserLoginVO.builder()
|
|
|
.tokenName(CrmAdminStpUtil.getTokenName())
|
|
|
@@ -75,8 +228,8 @@ public class AdminAuthController {
|
|
|
.roles(userInfo.getRoles())
|
|
|
.permissions(userInfo.getPermissions())
|
|
|
.build();
|
|
|
-
|
|
|
- return ApiResult.data("登录成功", tokenInfo);
|
|
|
+ resultData.setTokenInfo(tokenInfo);
|
|
|
+ return ApiResult.data("登录成功", resultData);
|
|
|
}
|
|
|
|
|
|
@SaCheckLogin(type = CrmAdminStpUtil.TYPE)
|
|
|
@@ -90,7 +243,38 @@ public class AdminAuthController {
|
|
|
return ApiResult.success();
|
|
|
}
|
|
|
|
|
|
- @Operation(summary = "获取验证码")
|
|
|
+
|
|
|
+ public SysUserLoginBO getLoginUserInfo(String mobile) {
|
|
|
+ //获取redis中的用户信息
|
|
|
+ Object object = stringSetRedisTemplate.opsForValue().get(RedisConstant.LOGIN_USER_INFO_PREFIX + mobile);
|
|
|
+ if (object == null) {
|
|
|
+ throw new BusinessException(407, "登录已超时,需重新登陆");
|
|
|
+ }
|
|
|
+ SysUserLoginBO userInfo = (SysUserLoginBO) object;
|
|
|
+ if (!userInfo.getPhoneNo().equals(mobile)) {
|
|
|
+ throw new BusinessException(406, "手机号错误");
|
|
|
+ }
|
|
|
+ return userInfo;
|
|
|
+ }
|
|
|
+
|
|
|
+ public LoginRespDataVO getResultData(String mobile, String key) {
|
|
|
+ LoginRespDataVO resultData = LoginRespDataVO.builder().build();
|
|
|
+ resultData.setMobile(mobile);
|
|
|
+ resultData.setShowPicCode(false);
|
|
|
+ resultData.setStatus(true);
|
|
|
+ resultData.setCommitCount(0);
|
|
|
+ if (key != null) {
|
|
|
+ Object object = stringSetRedisTemplate.opsForValue().get(key);
|
|
|
+ if (object != null) {
|
|
|
+ Integer count = (Integer) object;
|
|
|
+ resultData.setCommitCount(count);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return resultData;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Operation(summary = "重新获取图形验证码(不是短信验证码)")
|
|
|
@GetMapping(value = "/auth/captcha")
|
|
|
public ApiResult<AdminCaptchaVO> captcha() {
|
|
|
// 核验方法:captchaHelper.validate
|