1、系统登录改造:首页移除机构、新用户用手机号登录(机构新增组织管理员移除账号密码、新增用户移除账号密码)、需要验证码的部分(首页忘记密码、修改密保手机);2、组织权限菜单树增加visiable属性过滤;3、支持钉钉、阿里云短信配置加密、对接系统发送短信;

This commit is contained in:
gaoqr
2024-08-28 09:28:52 +08:00
parent 2248f89e70
commit 156ff82411
67 changed files with 973 additions and 316 deletions
@@ -7,7 +7,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@Schema(description = "RPC 服务 - 短信验证码的发送 Request DTO")
@Data
@@ -19,12 +18,10 @@ public class SmsCodeSendReqDTO {
private String mobile;
@Schema(description = "发送场景", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "发送场景不能为空")
@InEnum(SmsSceneEnum.class)
private Integer scene;
@Schema(description = "发送 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "10.20.30.40")
@NotEmpty(message = "发送 IP 不能为空")
private String createIp;
}
@@ -4,6 +4,7 @@ import com.cf.imes.framework.common.validation.InEnum;
import com.cf.imes.framework.common.validation.Mobile;
import com.cf.imes.module.system.enums.sms.SmsSceneEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
@@ -11,6 +12,7 @@ import javax.validation.constraints.NotNull;
@Schema(description = "RPC 服务 - 短信验证码的使用 Request DTO")
@Data
@Builder
public class SmsCodeUseReqDTO {
@Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300")
@@ -28,7 +30,6 @@ public class SmsCodeUseReqDTO {
private String code;
@Schema(description = "发送 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "10.20.30.40")
@NotEmpty(message = "使用 IP 不能为空")
private String usedIp;
}
@@ -4,6 +4,7 @@ import com.cf.imes.framework.common.validation.InEnum;
import com.cf.imes.framework.common.validation.Mobile;
import com.cf.imes.module.system.enums.sms.SmsSceneEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
@@ -11,6 +12,7 @@ import javax.validation.constraints.NotNull;
@Schema(description = "RPC 服务 - 短信验证码的校验 Request DTO")
@Data
@Builder
public class SmsCodeValidateReqDTO {
@Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300")
@@ -16,6 +16,7 @@ public interface ErrorCodeConstants {
ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1_002_000_005, "未绑定账号,需要进行绑定");
ErrorCode AUTH_TOKEN_EXPIRED = new ErrorCode(1_002_000_006, "Token 已经过期");
ErrorCode AUTH_MOBILE_NOT_EXISTS = new ErrorCode(1_002_000_007, "手机号不存在");
ErrorCode AUTH_LOGIN_FIRST_LOGIN = new ErrorCode(1_002_000_008, "请通过验证码登录");
// ========== 菜单模块 1-002-001-000 ==========
ErrorCode MENU_NAME_DUPLICATE = new ErrorCode(1_002_001_000, "已经存在该名字的菜单");
@@ -45,6 +46,7 @@ public interface ErrorCodeConstants {
ErrorCode USER_IS_DISABLE = new ErrorCode(1_002_003_006, "名字为【{}】的用户已被禁用");
ErrorCode USER_COUNT_MAX = new ErrorCode(1_002_003_008, "创建用户失败,原因:超过组织最大组织配额({})!");
ErrorCode USER_ME_ERROR = new ErrorCode(1_002_003_009, "不可操作用户自身");
ErrorCode USERNAME_MULTIPLE_ERROR = new ErrorCode(1_002_003_010,"存在名字为【{}】的重复用户,请联系系统管理员");
ErrorCode THE_PASSWORD_LENGTH_MUST_BE_AT_LEAST_6_CHARACTERS = new ErrorCode(1_002_003_010, "密码长度必须至少为6个字符");
@@ -105,14 +107,15 @@ public interface ErrorCodeConstants {
ErrorCode SMS_SEND_TEMPLATE_NOT_EXISTS = new ErrorCode(1_002_013_002, "短信模板不存在");
// ========== 短信验证码 1-002-014-000 ==========
ErrorCode SMS_CODE_NOT_FOUND = new ErrorCode(1_002_014_000, "验证码不存在");
ErrorCode SMS_CODE_EXPIRED = new ErrorCode(1_002_014_001, "验证码已过期");
ErrorCode SMS_CODE_USED = new ErrorCode(1_002_014_002, "验证码已使用");
ErrorCode SMS_CODE_NOT_FOUND = new ErrorCode(1_002_014_000, "验证码不存在,请点击获取");
ErrorCode SMS_CODE_EXPIRED = new ErrorCode(1_002_014_001, "验证码已过期,请重新获取");
ErrorCode SMS_CODE_USED = new ErrorCode(1_002_014_002, "验证码已使用,请重新获取");
ErrorCode SMS_CODE_NOT_CORRECT = new ErrorCode(1_002_014_003, "验证码不正确");
ErrorCode SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY = new ErrorCode(1_002_014_004, "超过每日短信发送数量");
ErrorCode SMS_CODE_SEND_TOO_FAST = new ErrorCode(1_002_014_005, "短信发送过于频率");
ErrorCode SMS_CODE_IS_EXISTS = new ErrorCode(1_002_014_006, "手机号已被使用");
ErrorCode SMS_CODE_IS_UNUSED = new ErrorCode(1_002_014_007, "验证码未被使用");
ErrorCode SMS_CODE_IS_VALID = new ErrorCode(1_002_014_008, "验证码在有效期内,5分钟内请勿重复发送!");
// ========== 组织信息 1-002-015-000 ==========
ErrorCode ORGAN_NOT_EXISTS = new ErrorCode(1_002_015_000, "组织不存在");
@@ -14,6 +14,7 @@ public enum LoginLogTypeEnum {
LOGIN_SOCIAL(101), // 使用社交登录
LOGIN_MOBILE(103), // 使用手机登陆
LOGIN_SMS(104), // 使用短信登陆
LOGIN_FORGET_PWD(105), // 登录忘记密码
LOGOUT_SELF(200), // 自己主动登出
LOGOUT_DELETE(202), // 强制退出
@@ -15,8 +15,7 @@ public enum LoginResultEnum {
USER_DISABLED(20), // 用户被禁用
CAPTCHA_NOT_FOUND(30), // 图片验证码不存在
CAPTCHA_CODE_ERROR(31), // 图片验证码不正确
;
FIRST_LOGIN(32); // 首次登录
/**
* 结果
@@ -16,12 +16,13 @@ import java.util.Arrays;
@AllArgsConstructor
public enum SmsSceneEnum implements IntArrayValuable {
MEMBER_LOGIN(1, "user-sms-login", "会员用户 - 手机号登陆"),
MEMBER_UPDATE_MOBILE(2, "user-update-mobile", "会员用户 - 修改手机"),
MEMBER_UPDATE_PASSWORD(3, "user-update-password", "会员用户 - 修改密码"),
MEMBER_RESET_PASSWORD(4, "user-reset-password", "会员用户 - 忘记密"),
ADMIN_MEMBER_LOGIN(21, "admin-sms-login", "后台用户 - 手机号登录");
USER_UPDATE_MOBILE(1, "user-update-mobile", "修改手机"),
USER_RESET_PASSWORD(2, "user-reset-password", "忘记密码"),
USER_UPDATE_PASSWORD(3, "user-update-password", "修改密码"),
USER_LOGIN_CAPTCHAVERIFICATION(4, "user-login-captchaverification", "登录验证"),
ADMIN_LOGIN(20, "admin-sms-login", "后台用户 - 手机号登录"),
ADMIN_MEMBER_LOGIN(21, "admin-sms-login", "后台用户 - 手机号登录"),
MEMBER_LOGIN(22, "user-sms-login", "会员用户 - 手机号登陆");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SmsSceneEnum::getScene).toArray();
@@ -22,4 +22,17 @@ public enum SmsTemplateTypeEnum {
*/
private final int type;
public static SmsTemplateTypeEnum valueOf(Integer type) {
if (type == null) {
return null;
}
for (SmsTemplateTypeEnum value : values()) {
if (type == value.getType()) {
return value;
}
}
return null;
}
}
@@ -2,12 +2,14 @@ package com.cf.imes.module.system;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.context.config.annotation.RefreshScope;
/**
* 项目的启动类
* @author 晨丰科技
*/
@SpringBootApplication
@RefreshScope
public class SystemServerApplication {
public static void main(String[] args) {
@@ -1,13 +1,13 @@
package com.cf.imes.module.system.controller.admin.auth;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.cf.imes.framework.common.enums.CommonStatusEnum;
import com.cf.imes.framework.common.enums.UserTypeEnum;
import com.cf.imes.framework.common.pojo.CommonResult;
import com.cf.imes.framework.operatelog.core.annotations.OperateLog;
import com.cf.imes.framework.security.config.SecurityProperties;
import com.cf.imes.framework.security.core.util.SecurityFrameworkUtils;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginReqVO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthPermissionInfoRespVO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthSocialLoginReqVO;
@@ -20,6 +20,7 @@ import com.cf.imes.module.system.service.auth.AdminAuthService;
import com.cf.imes.module.system.service.permission.MenuService;
import com.cf.imes.module.system.service.permission.PermissionService;
import com.cf.imes.module.system.service.permission.RoleService;
import com.cf.imes.module.system.service.sms.SmsCodeService;
import com.cf.imes.module.system.service.social.SocialClientService;
import com.cf.imes.module.system.service.user.AdminUserService;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginRespVO;
@@ -30,6 +31,7 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -43,6 +45,7 @@ import java.util.Set;
import static com.cf.imes.framework.common.pojo.CommonResult.success;
import static com.cf.imes.framework.common.util.collection.CollectionUtils.convertSet;
import static com.cf.imes.framework.common.util.servlet.ServletUtils.getClientIP;
import static com.cf.imes.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - 认证")
@@ -68,11 +71,14 @@ public class AuthController {
@Resource
private SecurityProperties securityProperties;
@Resource
private SmsCodeService smsCodeService;
@PostMapping("/login")
@PermitAll
@Operation(summary = "使用账号密码登录")
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
public CommonResult<AuthLoginRespVO> login(@RequestBody @Valid AuthLoginReqVO reqVO) {
public CommonResult<AuthLoginRespVO> login(@Valid @RequestBody AuthLoginReqVO reqVO) {
return success(authService.login(reqVO));
}
@@ -83,7 +89,7 @@ public class AuthController {
public CommonResult<Boolean> logout(HttpServletRequest request) {
String token = SecurityFrameworkUtils.obtainAuthorization(request,
securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
if (StrUtil.isNotBlank(token)) {
if (StringUtils.isNotBlank(token)) {
authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType());
}
return success(true);
@@ -112,7 +118,6 @@ public class AuthController {
if (CollUtil.isEmpty(roleIds)) {
return success(AuthConvert.INSTANCE.convert(user, Collections.emptyList(), Collections.emptyList()));
}
//List<RoleDO> roles = roleService.getRoleList(roleIds);
List<RoleDO> roles = roleService.getRoleList1(roleIds);
roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); // 移除禁用的角色
@@ -144,6 +149,18 @@ public class AuthController {
return success(true);
}
@PutMapping("/sms-code")
@Operation(summary = "发送短信验证码")
@PermitAll
@OperateLog(enable = false)
public CommonResult<Boolean> sendSmsCode(@Valid @RequestBody SmsCodeSendReqDTO reqDTO) {
// 获取请求ip
reqDTO.setCreateIp(getClientIP());
// 发送验证码
smsCodeService.sendSmsCode(reqDTO);
return success(true);
}
// ========== 社交登录相关 ==========
@GetMapping("/social-auth-redirect")
@@ -1,6 +1,6 @@
package com.cf.imes.module.system.controller.admin.auth.vo;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.ObjectUtil;
import com.cf.imes.framework.common.validation.InEnum;
import com.cf.imes.module.system.enums.social.SocialTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -8,10 +8,10 @@ import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
@@ -29,12 +29,10 @@ public class AuthLoginReqVO {
private String username;
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao")
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
@Schema(description = "组织名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "晨丰科技")
@NotBlank(message = "组织名称不能空")
private String organName;
// ========== 图片验证码相关 ==========
@@ -44,6 +42,10 @@ public class AuthLoginReqVO {
@NotEmpty(message = "验证码不能为空", groups = CodeEnableGroup.class)
private String captchaVerification;
@Schema(description = "短信验证码", example = "123456")
@Length(min = 6, max = 6, message = "短信验证码长度为 6 位")
private String smsCaptchaVerification;
// ========== 绑定社交登录时,需要传递如下参数 ==========
@Schema(description = "社交平台的类型,参见 SocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@@ -63,12 +65,17 @@ public class AuthLoginReqVO {
@AssertTrue(message = "授权码不能为空")
public boolean isSocialCodeValid() {
return socialType == null || StrUtil.isNotEmpty(socialCode);
return socialType == null || StringUtils.isNotEmpty(socialCode);
}
@AssertTrue(message = "授权 state 不能为空")
public boolean isSocialState() {
return socialType == null || StrUtil.isNotEmpty(socialState);
return socialType == null || StringUtils.isNotEmpty(socialState);
}
@AssertTrue(message = "请输入密码后登陆")
public boolean isPasswordValid() {
// 要么是密码登录、要么是验证码登录
return ObjectUtil.isNotNull(password) || ObjectUtil.isNotNull(captchaVerification);
}
}
@@ -46,6 +46,9 @@ public class AuthPermissionInfoRespVO {
@Schema(description = "组织id")
private Long organId;
@Schema(description = "是否需要设置密码")
private boolean needSetPwd;
}
@Schema(description = "管理后台 - 登录用户的菜单信息 Response VO")
@@ -1,13 +1,10 @@
package com.cf.imes.module.system.controller.admin.organ.vo.organ;
import cn.hutool.core.util.ObjectUtil;
import com.cf.imes.framework.common.validation.Mobile;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
@@ -30,6 +27,7 @@ public class OrganSaveReqVO {
@Schema(description = "联系手机", example = "15601691300")
@Mobile
@NotNull(message = "联系人手机不能为空")
private String contactMobile;
@Schema(description = "组织状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@@ -62,13 +60,6 @@ public class OrganSaveReqVO {
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
@AssertTrue(message = "用户账号、密码不能为空")
@JsonIgnore
public boolean isUsernameValid() {
return id != null // 修改时,不需要传递
|| (ObjectUtil.isAllNotEmpty(username, password)); // 新增时,必须都传递 username、password
}
@Schema(description = "是否大型组织")
private Boolean large;
@@ -5,12 +5,10 @@ import com.cf.imes.framework.common.enums.CommonStatusEnum;
import com.cf.imes.framework.common.pojo.CommonResult;
import com.cf.imes.framework.common.util.object.BeanUtils;
import com.cf.imes.framework.security.core.LoginUser;
import com.cf.imes.framework.security.core.util.SecurityFrameworkUtils;
import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuListReqVO;
import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuRespVO;
import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuSaveVO;
import com.cf.imes.module.system.controller.admin.permission.vo.menu.MenuSimpleRespVO;
import com.cf.imes.module.system.convert.auth.AuthConvert;
import com.cf.imes.module.system.dal.dataobject.permission.MenuDO;
import com.cf.imes.module.system.dal.dataobject.permission.RoleDO;
import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO;
@@ -111,7 +109,7 @@ public class MenuController {
"在多组织的场景下,会只返回组织所在套餐有的菜单")
public CommonResult<List<MenuSimpleRespVO>> getSimpleMenuList() {
List<MenuDO> list = menuService.getMenuListByOrgan(
new MenuListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus()));
new MenuListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus()).setVisiable(CommonStatusEnum.ENABLE.getStatus()));
list.sort(Comparator.comparing(MenuDO::getSort));
return success(BeanUtils.toBean(list, MenuSimpleRespVO.class));
}
@@ -10,7 +10,9 @@ public class MenuListReqVO {
@Schema(description = "菜单名称,模糊匹配", example = "晨丰")
private String name;
@Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1")
@Schema(description = "菜单状态,参见 CommonStatusEnum 枚举类", example = "1")
private Integer status;
@Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1")
private Integer visiable;
}
@@ -38,6 +38,7 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
@@ -100,6 +101,15 @@ public class UserController {
return success(true);
}
@PutMapping("/reset-password")
@Operation(summary = "忘记密码重置")
@PermitAll
@OperateLog(enable = false)
public CommonResult<Boolean> resetUserPassword(@Valid @RequestBody UserResetPasswordReqVO reqVO) {
userService.resetUserPassword(reqVO);
return success(true);
}
@PutMapping("/update-status")
@Operation(summary = "修改用户状态")
// @PreAuthorize("@ss.hasPermission('system:user:update')")
@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import com.cf.imes.framework.common.enums.UserTypeEnum;
import com.cf.imes.framework.common.pojo.CommonResult;
import com.cf.imes.framework.datapermission.core.annotation.DataPermission;
import com.cf.imes.module.system.controller.admin.user.vo.profile.UserMobileUpdateReqVO;
import com.cf.imes.module.system.controller.admin.user.vo.profile.UserProfileRespVO;
import com.cf.imes.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO;
import com.cf.imes.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO;
@@ -97,4 +98,10 @@ public class UserProfileController {
return success(avatar);
}
@PostMapping("/security/phone")
@Operation(summary = "修改密保手机")
public CommonResult<Boolean> updateUserSecurityPhone(@Valid @RequestBody UserMobileUpdateReqVO reqVO) {
userService.updateUserSecurityPhone(getLoginUserId(), reqVO);
return success(true);
}
}
@@ -0,0 +1,26 @@
package com.cf.imes.module.system.controller.admin.user.vo.profile;
import com.cf.imes.framework.common.validation.Mobile;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
@Schema(description = "管理后台 - 用户密保手机更新 Request VO")
@Data
public class UserMobileUpdateReqVO {
@Schema(description = "手机号码", example = "15601691300")
@Length(min = 11, max = 11, message = "手机号长度必须 11 位")
@Mobile
@NotEmpty(message = "手机号不能为空")
private String mobile;
@Schema(description = "验证码", example = "123456")
@Length(min = 6, max = 6, message = "验证码长度必须 6 位")
@NotEmpty(message = "短信验证码不能为空")
private String smsCaptchaVerification;
}
@@ -1,5 +1,6 @@
package com.cf.imes.module.system.controller.admin.user.vo.profile;
import com.cf.imes.framework.common.validation.Mobile;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
@@ -23,6 +24,7 @@ public class UserProfileUpdateReqVO {
@Schema(description = "手机号码", example = "15601691300")
@Length(min = 11, max = 11, message = "手机号长度必须 11 位")
@Mobile
private String mobile;
@Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1")
@@ -0,0 +1,28 @@
package com.cf.imes.module.system.controller.admin.user.vo.user;
import com.cf.imes.framework.common.validation.Mobile;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
@Schema(description = "管理后台 - 用户重置密码 Request VO")
@Data
public class UserResetPasswordReqVO {
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
@Schema(description = "短信验证码", example = "123456")
@Length(min = 6, max = 6, message = "短信验证码长度为 6 位")
@NotEmpty(message = "短信验证码不能为空")
private String smsCaptchaVerification;
@Schema(description = "手机号", example = "15601691300")
@Mobile
@NotEmpty(message = "手机号不能为空")
private String mobile;
}
@@ -18,7 +18,6 @@ public class UserSaveReqVO {
private Long id;
@Schema(description = "a", requiredMode = Schema.RequiredMode.REQUIRED, example = "chenfeng")
@NotBlank(message = "用户名不能为空")
@Pattern(regexp = "^[a-zA-Z0-9]{4,30}$", message = "用户名由 数字、字母 组成")
@Size(min = 4, max = 30, message = "用户名长度为 4-30 个字符")
private String username;
@@ -43,6 +42,7 @@ public class UserSaveReqVO {
@Schema(description = "手机号码", example = "15601691300")
@Mobile
@NotBlank(message = "手机号码不能为空")
private String mobile;
@Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1")
@@ -57,13 +57,6 @@ public class UserSaveReqVO {
@Length(min = 4, max = 16, message = "密码长度为 4-16 位")
private String password;
@AssertTrue(message = "密码不能为空")
@JsonIgnore
public boolean isPasswordValid() {
return id != null // 修改时,不需要传递
|| (ObjectUtil.isAllNotEmpty(password)); // 新增时,必须都传递 password
}
@Schema(description = "帐号状态(0正常 1停用)")
@NotNull(message = "账号状态不能空")
private Integer status;
@@ -4,7 +4,6 @@ import cn.hutool.core.collection.CollUtil;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import com.cf.imes.module.system.api.social.dto.SocialUserBindReqDTO;
import com.cf.imes.module.system.controller.admin.auth.vo.*;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginRespVO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthPermissionInfoRespVO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthSmsLoginReqVO;
@@ -15,6 +14,7 @@ import com.cf.imes.module.system.dal.dataobject.permission.MenuDO;
import com.cf.imes.module.system.dal.dataobject.permission.RoleDO;
import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO;
import com.cf.imes.module.system.enums.permission.MenuTypeEnum;
import org.apache.commons.lang3.StringUtils;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import org.slf4j.LoggerFactory;
@@ -33,7 +33,8 @@ public interface AuthConvert {
default AuthPermissionInfoRespVO convert(AdminUserDO user, List<RoleDO> roleList, List<MenuDO> menuList) {
return AuthPermissionInfoRespVO.builder()
.user(AuthPermissionInfoRespVO.UserVO.builder().id(user.getId()).nickname(user.getNickname()).avatar(user.getAvatar()).organId(user.getOrganId()).build())
.user(AuthPermissionInfoRespVO.UserVO.builder()
.id(user.getId()).nickname(user.getNickname()).avatar(user.getAvatar()).organId(user.getOrganId()).needSetPwd(StringUtils.isEmpty(user.getPassword())).build())
.roles(convertSet(roleList, RoleDO::getCode))
// 权限标识信息
.permissions(convertSet(menuList, MenuDO::getPermission))
@@ -17,9 +17,8 @@ public interface OrganConvert {
default UserSaveReqVO convert02(OrganSaveReqVO bean) {
UserSaveReqVO reqVO = new UserSaveReqVO();
reqVO.setUsername(bean.getUsername());
reqVO.setPassword(bean.getPassword());
reqVO.setNickname(bean.getContactName()).setMobile(bean.getContactMobile());
reqVO.setMobile(bean.getContactMobile());
reqVO.setNickname(bean.getContactName());
return reqVO;
}
@@ -22,7 +22,8 @@ public interface MenuMapper extends BaseMapperX<MenuDO> {
default List<MenuDO> selectList(MenuListReqVO reqVO) {
return selectList(new LambdaQueryWrapperX<MenuDO>()
.likeIfPresent(MenuDO::getName, reqVO.getName())
.eqIfPresent(MenuDO::getStatus, reqVO.getStatus()));
.eqIfPresent(MenuDO::getStatus, reqVO.getStatus())
.eqIfPresent(MenuDO::getVisible, reqVO.getVisiable()));
}
default List<MenuDO> selectListByPermission(String permission) {
@@ -1,8 +1,9 @@
package com.cf.imes.module.system.dal.mysql.user;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cf.imes.framework.common.enums.CommonStatusEnum;
import com.cf.imes.framework.common.exception.util.ServiceExceptionUtil;
import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.framework.mybatis.core.mapper.BaseMapperX;
import com.cf.imes.framework.mybatis.core.query.LambdaQueryWrapperX;
@@ -11,7 +12,6 @@ import com.cf.imes.framework.security.core.LoginUser;
import com.cf.imes.framework.security.core.util.SecurityFrameworkUtils;
import com.cf.imes.module.executor.enums.OrderDeletedEnum;
import com.cf.imes.module.system.controller.admin.user.vo.user.UserPageReqVO;
import com.cf.imes.module.system.dal.dataobject.organ.OrganizationDO;
import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -22,7 +22,8 @@ import java.time.LocalTime;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.USERNAME_MULTIPLE_ERROR;
@Mapper
public interface AdminUserMapper extends BaseMapperX<AdminUserDO> {
@@ -35,6 +36,26 @@ public interface AdminUserMapper extends BaseMapperX<AdminUserDO> {
);
}
/**
* 用户名查询唯一用户,存在多个用户抛出业务异常
*
* @param userName
* @return
*/
@OrganIgnore
default AdminUserDO selectByUsernameUnique(String userName) {
AdminUserDO adminUserDO = null;
List<AdminUserDO> adminUserDOS = selectList(new LambdaQueryWrapperX<AdminUserDO>()
.eq(AdminUserDO::getUsername, userName));
if (CollectionUtil.isNotEmpty(adminUserDOS)) {
if (adminUserDOS.size() > 1) {
ServiceExceptionUtil.exception(USERNAME_MULTIPLE_ERROR, userName);
}
adminUserDO = adminUserDOS.get(0);
}
return adminUserDO;
}
default AdminUserDO selectByEmail(String email) {
return selectOne(AdminUserDO::getEmail, email);
}
@@ -98,4 +98,14 @@ public interface RedisKeyConstants {
* VALUE 数据格式:String 模版信息
*/
String SMS_TEMPLATE = "sms_template";
/**
* 短信验证码的缓存
*/
String SMS_CAPTCHA_VERIFICATION = "sms_captcha_verification:%s";
/**
* 短信验证码次数上限的缓存
*/
String SMS_CAPTCHA_VERIFICATION_LIMIT = "sms_captcha_verification_limit:%s";
}
@@ -38,4 +38,15 @@ public class SmsCodeProperties {
@NotNull(message = "验证码最大值不能为空")
private Integer endCode;
/**
* 窗口时间
*/
@NotNull(message = "窗口时间不能为空")
private Duration sendWindowSizeMin;
/**
* 窗口最大请求次数
*/
@NotNull(message = "窗口最大请求次数不能为空")
private Integer sendMaxSendTimesPerWindow;
}
@@ -39,4 +39,9 @@ public class SmsSendMessage {
*/
private List<KeyValue<String, Object>> templateParams;
/**
* 短信消息模板类型:system_sms_template.type
* 不同的消息模板类型有不同的消息发送后处理器
*/
private Integer templateType;
}
@@ -1,10 +1,12 @@
package com.cf.imes.module.system.mq.producer.sms;
import com.cf.imes.framework.common.core.KeyValue;
import com.cf.imes.module.system.dal.dataobject.sms.SmsTemplateDO;
import com.cf.imes.module.system.mq.message.sms.SmsSendMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import javax.annotation.Resource;
import java.util.List;
@@ -25,16 +27,16 @@ public class SmsProducer {
/**
* 发送 {@link SmsSendMessage} 消息
*
* @param logId 短信日志编号
* @param mobile 手机号
* @param channelId 渠道编号
* @param apiTemplateId 短信模板编号
* @param logId 短信日志编号
* @param mobile 手机号
* @param template 模板信息
* @param templateParams 短信模板参数
*/
public void sendSmsSendMessage(Long logId, String mobile,
Long channelId, String apiTemplateId, List<KeyValue<String, Object>> templateParams) {
public void sendSmsSendMessage(Long logId, String mobile, SmsTemplateDO template, List<KeyValue<String, Object>> templateParams) {
SmsSendMessage message = new SmsSendMessage().setLogId(logId).setMobile(mobile);
message.setChannelId(channelId).setApiTemplateId(apiTemplateId).setTemplateParams(templateParams);
message.setChannelId(template.getChannelId()).setApiTemplateId(template.getApiTemplateId()).setTemplateType(template.getType()).setTemplateParams(templateParams);
// event异步发送短信,保证子线程内部request不为空
RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
applicationContext.publishEvent(message);
}
@@ -1,6 +1,5 @@
package com.cf.imes.module.system.service.auth;
import com.cf.imes.module.system.controller.admin.auth.vo.*;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginReqVO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginRespVO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthSmsLoginReqVO;
@@ -22,11 +21,10 @@ public interface AdminAuthService {
/**
* 验证账号 + 密码。如果通过,则返回用户
*
* @param username 账号
* @param password 密码
* @param reqVO
* @return 用户
*/
AdminUserDO authenticate(String username, String password, String organName);
AdminUserDO authenticate(AuthLoginReqVO reqVO);
/**
* 账号登录
@@ -9,6 +9,7 @@ import com.cf.imes.framework.common.util.servlet.ServletUtils;
import com.cf.imes.framework.common.util.validation.ValidationUtils;
import com.cf.imes.module.system.api.logger.dto.LoginLogCreateReqDTO;
import com.cf.imes.module.system.api.sms.SmsCodeApi;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import com.cf.imes.module.system.api.social.dto.SocialUserBindReqDTO;
import com.cf.imes.module.system.api.social.dto.SocialUserRespDTO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginReqVO;
@@ -27,6 +28,7 @@ import com.cf.imes.module.system.enums.sms.SmsSceneEnum;
import com.cf.imes.module.system.service.logger.LoginLogService;
import com.cf.imes.module.system.service.member.MemberService;
import com.cf.imes.module.system.service.oauth2.OAuth2TokenService;
import com.cf.imes.module.system.service.sms.SmsCodeService;
import com.cf.imes.module.system.service.social.SocialUserService;
import com.cf.imes.module.system.service.organ.OrganService;
import com.cf.imes.module.system.service.user.AdminUserService;
@@ -47,7 +49,6 @@ import java.util.Objects;
import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.cf.imes.framework.common.util.servlet.ServletUtils.getClientIP;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.ORGAN_DATA_CODE_NOT_EXISTS;
import static com.cf.imes.module.system.enums.ErrorCodeConstants.ORGAN_NOT_EXISTS;
/**
* Auth Service 实现类
@@ -77,6 +78,9 @@ public class AdminAuthServiceImpl implements AdminAuthService {
@Resource
private OrganService organService;
@Resource
private SmsCodeService smsCodeService;
/**
* 验证码的开关,默认为 true
*/
@@ -84,19 +88,23 @@ public class AdminAuthServiceImpl implements AdminAuthService {
private Boolean captchaEnable;
@Override
public AdminUserDO authenticate(String username, String password, String organName) {
public AdminUserDO authenticate(AuthLoginReqVO reqVO) {
String username = reqVO.getUsername();
String password = reqVO.getPassword();
String smsCaptchaVerification = reqVO.getSmsCaptchaVerification();
final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME;
OrganizationDO organ = organService.getOrganByName(organName);
if(Objects.isNull(organ)) {
throw ServiceExceptionUtil.exception(ORGAN_NOT_EXISTS);
}
// 校验账号是否存在
AdminUserDO user = userService.getUserByUsernameAndOrganId(username, organ.getId());
// 查询账号
AdminUserDO user = userService.getUserUniqueByUserName(username);
// 校验用户是否存在
if (user == null) {
createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
throw ServiceExceptionUtil.exception(ErrorCodeConstants.AUTH_LOGIN_BAD_CREDENTIALS);
}
if (!userService.isPasswordMatch(password, user.getPassword())) {
// 校验机构有效性
organService.validOrgan(user.getOrganId());
// 校验密码
String userPassword = user.getPassword();
if (StringUtils.isNotEmpty(userPassword) && !userService.isPasswordMatch(password, userPassword)) {
createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
throw ServiceExceptionUtil.exception(ErrorCodeConstants.AUTH_LOGIN_BAD_CREDENTIALS);
}
@@ -105,6 +113,11 @@ public class AdminAuthServiceImpl implements AdminAuthService {
createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED);
throw ServiceExceptionUtil.exception(ErrorCodeConstants.AUTH_LOGIN_USER_DISABLED);
}
// 校验没有密码并且没有传验证码,告诉前端需要验证码登录
if (StringUtils.isEmpty(userPassword) && StringUtils.isEmpty(smsCaptchaVerification)) {
createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.FIRST_LOGIN);
throw ServiceExceptionUtil.exception(ErrorCodeConstants.AUTH_LOGIN_FIRST_LOGIN);
}
return user;
}
@@ -114,7 +127,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
validateCaptcha(reqVO);
// 使用账号密码,进行登录
AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword(), reqVO.getOrganName());
AdminUserDO user = authenticate(reqVO);
// 如果 socialType 非空,说明需要绑定社交用户
if (reqVO.getSocialType() != null) {
@@ -122,14 +135,17 @@ public class AdminAuthServiceImpl implements AdminAuthService {
reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()));
}
Long organId = user.getOrganId();
// 校验机构有效性
organService.validOrgan(organId);
OrganizationDO organ = organService.getOrgan(organId);
String dataSourceCode = organ.getDataSourceCode();
// 校验机构数据源
if (StringUtils.isBlank(dataSourceCode)) {
throw exception(ORGAN_DATA_CODE_NOT_EXISTS);
}
// 验证并消耗验证码
String smsCaptchaVerification = reqVO.getSmsCaptchaVerification();
if (StringUtils.isNotEmpty(smsCaptchaVerification)) {
smsCodeService.useSmsCode(SmsCodeUseReqDTO.builder().mobile(user.getUsername()).code(smsCaptchaVerification).scene(SmsSceneEnum.USER_LOGIN_CAPTCHAVERIFICATION.getScene()).build());
}
// 创建 Token 令牌,记录登录日志
return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME,organ.getLarge(), dataSourceCode, user.getOrganId(), user.getNickname());
}
@@ -1,14 +1,12 @@
package com.cf.imes.module.system.service.oauth2;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.cf.imes.framework.common.enums.UserTypeEnum;
import com.cf.imes.framework.common.exception.ServerException;
import com.cf.imes.framework.organ.core.context.OrganContextHolder;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginReqVO;
import com.cf.imes.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import com.cf.imes.module.system.dal.dataobject.oauth2.OAuth2CodeDO;
import com.cf.imes.module.system.dal.dataobject.organ.OrganizationDO;
import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO;
import com.cf.imes.module.system.enums.ErrorCodeConstants;
import com.cf.imes.module.system.service.auth.AdminAuthService;
@@ -17,7 +15,6 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -64,16 +61,16 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService {
OAuth2CodeDO codeDO = oauth2CodeService.consumeAuthorizationCode(code);
Assert.notNull(codeDO, "授权码不能为空"); // 防御性编程
// 校验 clientId 是否匹配
if (!StrUtil.equals(clientId, codeDO.getClientId())) {
if (!CharSequenceUtil.equals(clientId, codeDO.getClientId())) {
throw exception(ErrorCodeConstants.OAUTH2_GRANT_CLIENT_ID_MISMATCH);
}
// 校验 redirectUri 是否匹配
if (!StrUtil.equals(redirectUri, codeDO.getRedirectUri())) {
if (!CharSequenceUtil.equals(redirectUri, codeDO.getRedirectUri())) {
throw exception(ErrorCodeConstants.OAUTH2_GRANT_REDIRECT_URI_MISMATCH);
}
// 校验 state 是否匹配
state = StrUtil.nullToDefault(state, ""); // 数据库 state 为 null 时,会设置为 "" 空串
if (!StrUtil.equals(state, codeDO.getState())) {
state = CharSequenceUtil.nullToDefault(state, ""); // 数据库 state 为 null 时,会设置为 "" 空串
if (!CharSequenceUtil.equals(state, codeDO.getState())) {
throw exception(ErrorCodeConstants.OAUTH2_GRANT_STATE_MISMATCH);
}
@@ -91,7 +88,7 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService {
@Override
public OAuth2AccessTokenDO grantPassword(String username, String password, String clientId, List<String> scopes) {
// 使用账号 + 密码进行登录
AdminUserDO user = adminAuthService.authenticate(username, password, null);
AdminUserDO user = adminAuthService.authenticate(AuthLoginReqVO.builder().username(username).password(password).build());
Assert.notNull(user, "用户不能为空!"); // 防御性编程
/*Long organId = OrganContextHolder.getOrganId();
OrganizationDO organ = organService.getOrgan(organId);
@@ -78,4 +78,11 @@ public interface SmsChannelService {
*/
SmsClient getSmsClient(String code);
/**
* 根据配置获得短信客户端
*
* @param
* @return 短信客户端
*/
SmsClient getSmsClient();
}
@@ -6,7 +6,7 @@ import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.framework.common.util.object.BeanUtils;
import com.cf.imes.framework.sms.core.client.SmsClient;
import com.cf.imes.framework.sms.core.client.SmsClientFactory;
import com.cf.imes.framework.sms.core.property.SmsChannelProperties;
import com.cf.imes.framework.sms.core.property.SmsProperties;
import com.cf.imes.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO;
import com.cf.imes.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsChannelDO;
@@ -22,7 +22,6 @@ import javax.annotation.Resource;
import java.time.Duration;
import java.util.List;
import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.cf.imes.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
/**
@@ -34,22 +33,28 @@ import static com.cf.imes.framework.common.util.cache.CacheUtils.buildAsyncReloa
@Slf4j
public class SmsChannelServiceImpl implements SmsChannelService {
@Resource
private SmsClientFactory smsClientFactory;
@Resource
private SmsChannelMapper smsChannelMapper;
@Resource
private SmsTemplateService smsTemplateService;
@Resource
private SmsProperties smsProperties;
/**
* {@link SmsClient} 缓存,通过它异步刷新 smsClientFactory
*/
@Getter
private final LoadingCache<Long, SmsClient> idClientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L),
new CacheLoader<Long, SmsClient>() {
new CacheLoader<>() {
@Override
public SmsClient load(Long id) {
// 查询,然后尝试刷新
SmsChannelDO channel = smsChannelMapper.selectById(id);
if (channel != null) {
SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);
smsClientFactory.createOrUpdateSmsClient(properties);
}
return smsClientFactory.getSmsClient(id);
return smsClientFactory.getSmsClient(smsProperties);
}
});
@@ -59,30 +64,15 @@ public class SmsChannelServiceImpl implements SmsChannelService {
*/
@Getter
private final LoadingCache<String, SmsClient> codeClientCache = buildAsyncReloadingCache(Duration.ofSeconds(60L),
new CacheLoader<String, SmsClient>() {
new CacheLoader<>() {
@Override
public SmsClient load(String code) {
// 查询,然后尝试刷新
SmsChannelDO channel = smsChannelMapper.selectByCode(code);
if (channel != null) {
SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);
smsClientFactory.createOrUpdateSmsClient(properties);
}
return smsClientFactory.getSmsClient(code);
return smsClientFactory.getSmsClient(smsProperties);
}
});
@Resource
private SmsClientFactory smsClientFactory;
@Resource
private SmsChannelMapper smsChannelMapper;
@Resource
private SmsTemplateService smsTemplateService;
@Override
public Long createSmsChannel(SmsChannelSaveReqVO createReqVO) {
SmsChannelDO channel = BeanUtils.toBean(createReqVO, SmsChannelDO.class);
@@ -160,7 +150,12 @@ public class SmsChannelServiceImpl implements SmsChannelService {
@Override
public SmsClient getSmsClient(String code) {
return codeClientCache.getUnchecked(code);
return smsClientFactory.getSmsClient(smsProperties);
}
@Override
public SmsClient getSmsClient() {
return smsClientFactory.getSmsClient(smsProperties);
}
}
@@ -3,24 +3,29 @@ package com.cf.imes.module.system.service.sms;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeSendReqDTO;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeValidateReqDTO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsCodeDO;
import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO;
import com.cf.imes.module.system.dal.mysql.sms.SmsCodeMapper;
import com.cf.imes.module.system.dal.redis.RedisKeyConstants;
import com.cf.imes.module.system.enums.sms.SmsSceneEnum;
import com.cf.imes.module.system.framework.sms.SmsCodeProperties;
import com.cf.imes.framework.common.exception.util.ServiceExceptionUtil;
import com.cf.imes.module.system.enums.ErrorCodeConstants;
import com.cf.imes.module.system.service.user.AdminUserService;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.HashMap;
import static cn.hutool.core.util.RandomUtil.randomInt;
import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.cf.imes.framework.common.util.date.DateUtils.isToday;
/**
* 短信验证码 Service 实现类
@@ -40,49 +45,118 @@ public class SmsCodeServiceImpl implements SmsCodeService {
@Resource
private SmsSendService smsSendService;
@Resource
private RedisTemplate redisTemplate;
@Resource
private AdminUserService userService;
@Override
public void sendSmsCode(SmsCodeSendReqDTO reqDTO) {
// 非修改手机号的场景时校验手机号是否存在
AdminUserDO user = userService.getUserByUsernameAndOrganId(reqDTO.getMobile(), null);
if (ObjectUtil.isNull(user) && ObjectUtil.notEqual(SmsSceneEnum.USER_UPDATE_MOBILE.getScene(), reqDTO.getScene())) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.AUTH_MOBILE_NOT_EXISTS);
}
SmsSceneEnum sceneEnum = SmsSceneEnum.getCodeByScene(reqDTO.getScene());
Assert.notNull(sceneEnum, "验证码场景({}) 查找不到配置", reqDTO.getScene());
// 创建验证码
String code = createSmsCode(reqDTO.getMobile(), reqDTO.getScene(), reqDTO.getCreateIp());
String code = createSmsCode(reqDTO.getMobile(), reqDTO.getCreateIp());
// 发送验证码
HashMap<String, Object> map = MapUtil.of("code", code);
map.put("product","晨丰科技");
smsSendService.sendSingleSms(reqDTO.getMobile(), null, null,
sceneEnum.getTemplateCode(), MapUtil.of("code", code));
sceneEnum.getTemplateCode(), map);
}
private String createSmsCode(String mobile, Integer scene, String ip) {
// 校验是否可以发送验证码,不用筛选场景
SmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile, null, null);
if (lastSmsCode != null) {
if (LocalDateTimeUtil.between(lastSmsCode.getCreateTime(), LocalDateTime.now()).toMillis()
< smsCodeProperties.getSendFrequency().toMillis()) { // 发送过于频繁
/**
* 生成验证码
*
* @param mobile 手机号
* @param ip 请求来源ip
* @return
*/
private String createSmsCode(String mobile, String ip) {
String redisMobileKey = String.format(RedisKeyConstants.SMS_CAPTCHA_VERIFICATION, mobile);
String redisMobileCountKey = String.format(RedisKeyConstants.SMS_CAPTCHA_VERIFICATION_LIMIT, mobile);
String redisIpCountKey = String.format(RedisKeyConstants.SMS_CAPTCHA_VERIFICATION_LIMIT, ip);
boolean hasCode = redisTemplate.hasKey(redisMobileKey);
if (!hasCode) {
// 验证码不存在
Duration sendWindowSizeMin = smsCodeProperties.getSendWindowSizeMin();
Integer sendMaxSendTimesPerWindow = smsCodeProperties.getSendMaxSendTimesPerWindow();
// 限制手机号请求次数:10分钟5次
boolean mobileCountRateLimit = slidingWindowRateLimit(redisMobileCountKey, sendWindowSizeMin.toMinutesPart(), sendMaxSendTimesPerWindow);
if (!mobileCountRateLimit) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CODE_SEND_TOO_FAST);
}
if (isToday(lastSmsCode.getCreateTime()) && // 必须是今天,才能计算超过当天的上限
lastSmsCode.getTodayIndex() >= smsCodeProperties.getSendMaximumQuantityPerDay()) { // 超过当天发送的上限。
throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY);
// 限制ip请求次数
boolean ipCountRateLimit = slidingWindowRateLimit(redisIpCountKey, sendWindowSizeMin.toMinutesPart(), sendMaxSendTimesPerWindow);
if (!ipCountRateLimit) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CODE_SEND_TOO_FAST);
}
// TODO 晨丰:提升,每个 IP 每天可发送数量
// TODO 晨丰:提升,每个 IP 每小时可发送数量
} else {
// 验证码存在提示请勿重复发送
throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CODE_IS_VALID);
}
// 创建验证码记录
String code = String.valueOf(randomInt(smsCodeProperties.getBeginCode(), smsCodeProperties.getEndCode() + 1));
SmsCodeDO newSmsCode = SmsCodeDO.builder().mobile(mobile).code(code).scene(scene)
.todayIndex(lastSmsCode != null && isToday(lastSmsCode.getCreateTime()) ? lastSmsCode.getTodayIndex() + 1 : 1)
.createIp(ip).used(false).build();
smsCodeMapper.insert(newSmsCode);
return code;
// 生成6位随机验证码
StringBuffer code = new StringBuffer(6);
for (int i = 0; i < 6; i++) {
code.append(randomInt(0, 9));
}
return code.toString();
}
/**
* 滑动窗口限制发送次数
*
* @param key
* @param winSizeMin 窗口时间大小
* @param maxSendTimesPerWin 窗口内最大请求次数
* @return false没有达到发送频繁告警要求
*/
public boolean slidingWindowRateLimit(String key, int winSizeMin, int maxSendTimesPerWin) {
// 获取当前时间戳
long currentTime = System.currentTimeMillis();
// 清除过期的时间片
redisTemplate.opsForZSet().removeRangeByScore(key, 0L, currentTime - (winSizeMin * 60 * 1000));
// 检查过去 winSizeMin 分钟内的请求次数
long requestCount = redisTemplate.opsForZSet().zCard(key);
if (requestCount >= maxSendTimesPerWin) {
return false;
}
// 窗口足够添加当前请求的时间戳
redisTemplate.opsForZSet().add(key, currentTime, currentTime);
return true;
}
@Override
public void useSmsCode(SmsCodeUseReqDTO reqDTO) {
// 检测验证码是否有效
SmsCodeDO lastSmsCode = validateSmsCode0(reqDTO.getMobile(), reqDTO.getCode(), reqDTO.getScene());
// 使用验证码
smsCodeMapper.updateById(SmsCodeDO.builder().id(lastSmsCode.getId())
.used(true).usedTime(LocalDateTime.now()).usedIp(reqDTO.getUsedIp()).build());
// 非修改手机号的场景时校验手机号是否存在
AdminUserDO user = userService.getUserByUsernameAndOrganId(reqDTO.getMobile(), null);
if (ObjectUtil.isNull(user) && ObjectUtil.notEqual(SmsSceneEnum.USER_UPDATE_MOBILE.getScene(), reqDTO.getScene())) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.AUTH_MOBILE_NOT_EXISTS);
}
String smsCodeKey = String.format(RedisKeyConstants.SMS_CAPTCHA_VERIFICATION, reqDTO.getMobile());
Object keyObj = redisTemplate.opsForValue().get(smsCodeKey);
if (ObjectUtil.isNotNull(keyObj)) {
// 校验码存在
if (ObjectUtil.notEqual(String.valueOf(keyObj), reqDTO.getCode())) {
// 验证码不正确
throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CODE_NOT_CORRECT);
} else {
// 消耗验证码
redisTemplate.delete(smsCodeKey);
}
} else {
// 提示验证码不存在
throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CODE_NOT_FOUND);
}
}
@Override
@@ -40,10 +40,11 @@ public interface SmsLogService {
* @param apiSendMsg 短信 API 发送失败的提示
* @param apiRequestId 短信 API 发送返回的唯一请求 ID
* @param apiSerialNo 短信 API 发送返回的序号
* @param channelCode 短信 API 渠道编码
*/
void updateSmsSendResult(Long id, Boolean success,
String apiSendCode, String apiSendMsg,
String apiRequestId, String apiSerialNo);
String apiRequestId, String apiSerialNo, String channelCode);
/**
* 更新日志的接收结果
@@ -40,8 +40,6 @@ public class SmsLogServiceImpl implements SmsLogService {
logBuilder.templateId(template.getId()).templateCode(template.getCode()).templateType(template.getType());
logBuilder.templateContent(templateContent).templateParams(templateParams)
.apiTemplateId(template.getApiTemplateId());
// 设置渠道相关字段
logBuilder.channelId(template.getChannelId()).channelCode(template.getChannelCode());
// 设置接收相关字段
logBuilder.receiveStatus(SmsReceiveStatusEnum.INIT.getStatus());
@@ -54,12 +52,12 @@ public class SmsLogServiceImpl implements SmsLogService {
@Override
public void updateSmsSendResult(Long id, Boolean success,
String apiSendCode, String apiSendMsg,
String apiRequestId, String apiSerialNo) {
String apiRequestId, String apiSerialNo, String channelCode) {
SmsSendStatusEnum sendStatus = success ? SmsSendStatusEnum.SUCCESS : SmsSendStatusEnum.FAILURE;
smsLogMapper.updateById(SmsLogDO.builder().id(id)
.sendStatus(sendStatus.getStatus()).sendTime(LocalDateTime.now())
.apiSendCode(apiSendCode).apiSendMsg(apiSendMsg)
.apiRequestId(apiRequestId).apiSerialNo(apiSerialNo).build());
.apiRequestId(apiRequestId).apiSerialNo(apiSerialNo).channelCode(channelCode).build());
}
@Override
@@ -3,7 +3,6 @@ package com.cf.imes.module.system.service.sms;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.cf.imes.framework.common.core.KeyValue;
import com.cf.imes.framework.common.enums.CommonStatusEnum;
import com.cf.imes.framework.common.enums.UserTypeEnum;
@@ -12,16 +11,17 @@ import com.cf.imes.framework.datapermission.core.annotation.DataPermission;
import com.cf.imes.framework.sms.core.client.SmsClient;
import com.cf.imes.framework.sms.core.client.dto.SmsReceiveRespDTO;
import com.cf.imes.framework.sms.core.client.dto.SmsSendRespDTO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsChannelDO;
import com.cf.imes.module.system.dal.dataobject.sms.SmsTemplateDO;
import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO;
import com.cf.imes.module.system.mq.message.sms.SmsSendMessage;
import com.cf.imes.module.system.mq.producer.sms.SmsProducer;
import com.cf.imes.module.system.service.member.MemberService;
import com.cf.imes.module.system.service.sms.handler.SmsSendAfterSendHandler;
import com.cf.imes.module.system.service.user.AdminUserService;
import com.cf.imes.module.system.enums.ErrorCodeConstants;
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@@ -29,8 +29,6 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static com.cf.imes.framework.common.exception.util.ServiceExceptionUtil.exception;
/**
* 短信发送 Service 发送的实现
*
@@ -54,11 +52,14 @@ public class SmsSendServiceImpl implements SmsSendService {
@Resource
private SmsProducer smsProducer;
@Resource
private List<SmsSendAfterSendHandler> smsSendAfterSendHandlers;
@Override
@DataPermission(enable = false) // 发送短信时,无需考虑数据权限
public Long sendSingleSmsToAdmin(String mobile, Long userId, String templateCode, Map<String, Object> templateParams) {
// 如果 mobile 为空,则加载用户编号对应的手机号
if (StrUtil.isEmpty(mobile)) {
if (StringUtils.isEmpty(mobile)) {
AdminUserDO user = adminUserService.getUser(userId);
if (user != null) {
mobile = user.getMobile();
@@ -71,7 +72,7 @@ public class SmsSendServiceImpl implements SmsSendService {
@Override
public Long sendSingleSmsToMember(String mobile, Long userId, String templateCode, Map<String, Object> templateParams) {
// 如果 mobile 为空,则加载用户编号对应的手机号
if (StrUtil.isEmpty(mobile)) {
if (StringUtils.isEmpty(mobile)) {
mobile = memberService.getMemberUserMobile(userId);
}
// 执行发送
@@ -83,8 +84,6 @@ public class SmsSendServiceImpl implements SmsSendService {
String templateCode, Map<String, Object> templateParams) {
// 校验短信模板是否合法
SmsTemplateDO template = validateSmsTemplate(templateCode);
// 校验短信渠道是否合法
SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId());
// 校验手机号码是否存在
mobile = validateMobile(mobile);
@@ -92,30 +91,17 @@ public class SmsSendServiceImpl implements SmsSendService {
List<KeyValue<String, Object>> newTemplateParams = buildTemplateParams(template, templateParams);
// 创建发送日志。如果模板被禁用,则不发送短信,只记录日志
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus())
&& CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus());
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus());
String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType, isSend, template, content, templateParams);
// 发送 MQ 消息,异步执行发送短信
if (isSend) {
smsProducer.sendSmsSendMessage(sendLogId, mobile, template.getChannelId(),
template.getApiTemplateId(), newTemplateParams);
smsProducer.sendSmsSendMessage(sendLogId, mobile, template, newTemplateParams);
}
return sendLogId;
}
@VisibleForTesting
SmsChannelDO validateSmsChannel(Long channelId) {
// 获得短信模板。考虑到效率,从缓存中获取
SmsChannelDO channelDO = smsChannelService.getSmsChannel(channelId);
// 短信模板不存在
if (channelDO == null) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS);
}
return channelDO;
}
@VisibleForTesting
SmsTemplateDO validateSmsTemplate(String templateCode) {
// 获得短信模板。考虑到效率,从缓存中获取
@@ -149,7 +135,7 @@ public class SmsSendServiceImpl implements SmsSendService {
@VisibleForTesting
public String validateMobile(String mobile) {
if (StrUtil.isEmpty(mobile)) {
if (StringUtils.isEmpty(mobile)) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.SMS_SEND_MOBILE_NOT_EXISTS);
}
return mobile;
@@ -158,19 +144,29 @@ public class SmsSendServiceImpl implements SmsSendService {
@Override
public void doSendSms(SmsSendMessage message) {
// 获得渠道对应的 SmsClient 客户端
SmsClient smsClient = smsChannelService.getSmsClient(message.getChannelId());
SmsClient smsClient = smsChannelService.getSmsClient();
Assert.notNull(smsClient, "短信客户端({}) 不存在", message.getChannelId());
List<KeyValue<String, Object>> templateParams = message.getTemplateParams();
String channelCode = null;
// 发送短信
try {
SmsSendRespDTO sendResponse = smsClient.sendSms(message.getLogId(), message.getMobile(),
message.getApiTemplateId(), message.getTemplateParams());
message.getApiTemplateId(), templateParams);
channelCode = sendResponse.getChannelCode();
// 发送成功后操作
for (SmsSendAfterSendHandler afterSendHandler : smsSendAfterSendHandlers) {
if (afterSendHandler.checkTemplateType(message.getTemplateType())) {
afterSendHandler.afterSend(sendResponse, templateParams);
}
}
smsLogService.updateSmsSendResult(message.getLogId(), sendResponse.getSuccess(),
sendResponse.getApiCode(), sendResponse.getApiMsg(),
sendResponse.getApiRequestId(), sendResponse.getSerialNo());
sendResponse.getApiRequestId(), sendResponse.getSerialNo(), channelCode);
} catch (Throwable ex) {
log.error("[doSendSms][发送短信异常,日志编号({})]", message.getLogId(), ex);
smsLogService.updateSmsSendResult(message.getLogId(), false,
"EXCEPTION", ExceptionUtil.getRootCauseMessage(ex), null, null);
"EXCEPTION", ExceptionUtil.getRootCauseMessage(ex), null, null, channelCode);
}
}
@@ -165,7 +165,7 @@ public class SmsTemplateServiceImpl implements SmsTemplateService {
@VisibleForTesting
void validateApiTemplate(Long channelId, String apiTemplateId) {
// 获得短信模板
SmsClient smsClient = smsChannelService.getSmsClient(channelId);
SmsClient smsClient = smsChannelService.getSmsClient();
Assert.notNull(smsClient, String.format("短信客户端(%d) 不存在", channelId));
SmsTemplateRespDTO template;
try {
@@ -0,0 +1,25 @@
package com.cf.imes.module.system.service.sms.handler;
import com.cf.imes.framework.common.core.KeyValue;
import com.cf.imes.framework.sms.core.client.dto.SmsSendRespDTO;
import java.util.List;
/**
* 短信发送后处理器接口
*
* @author Gqr
* @since 2024/8/12 18:42
*/
public interface SmsSendAfterSendHandler {
/**
* 校验消息模板类型
*
* @param templateType
* @return
*/
boolean checkTemplateType(Integer templateType);
void afterSend(SmsSendRespDTO sendResponse, List<KeyValue<String, Object>> params);
}
@@ -0,0 +1,46 @@
package com.cf.imes.module.system.service.sms.handler.impl;
import cn.hutool.core.util.ObjectUtil;
import com.cf.imes.framework.common.core.KeyValue;
import com.cf.imes.framework.sms.core.client.dto.SmsSendRespDTO;
import com.cf.imes.module.system.dal.redis.RedisKeyConstants;
import com.cf.imes.module.system.enums.sms.SmsTemplateTypeEnum;
import com.cf.imes.module.system.framework.sms.SmsCodeProperties;
import com.cf.imes.module.system.service.sms.handler.SmsSendAfterSendHandler;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
/**
* @author Gqr
* @since 2024/8/12 18:49
*/
@Service
public class SmsSendAfterSendHandlerImpl implements SmsSendAfterSendHandler {
@Resource
private RedisTemplate redisTemplate;
@Resource
private SmsCodeProperties smsCodeProperties;
@Override
public boolean checkTemplateType(Integer templateType) {
SmsTemplateTypeEnum smsTemplateTypeEnum = SmsTemplateTypeEnum.valueOf(templateType);
return ObjectUtil.isNotNull(smsTemplateTypeEnum);
}
@Override
public void afterSend(SmsSendRespDTO sendResponse, List<KeyValue<String, Object>> params) {
// 发送成功把验证码存入redis
if (sendResponse.getSuccess()) {
for (KeyValue<String, Object> keyValue : params) {
if ("code".equals(keyValue.getKey())) {
redisTemplate.opsForValue().set(String.format(RedisKeyConstants.SMS_CAPTCHA_VERIFICATION, sendResponse.getMobile()), keyValue.getValue(), smsCodeProperties.getExpireTimes());
}
}
}
}
}
@@ -4,11 +4,13 @@ import cn.hutool.core.collection.CollUtil;
import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.framework.common.util.collection.CollectionUtils;
import com.cf.imes.module.system.api.user.dto.OrganAdminUserRespDTO;
import com.cf.imes.module.system.controller.admin.user.vo.profile.UserMobileUpdateReqVO;
import com.cf.imes.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO;
import com.cf.imes.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO;
import com.cf.imes.module.system.controller.admin.user.vo.user.UserImportExcelVO;
import com.cf.imes.module.system.controller.admin.user.vo.user.UserImportRespVO;
import com.cf.imes.module.system.controller.admin.user.vo.user.UserPageReqVO;
import com.cf.imes.module.system.controller.admin.user.vo.user.UserResetPasswordReqVO;
import com.cf.imes.module.system.controller.admin.user.vo.user.UserSaveReqVO;
import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO;
@@ -71,7 +73,7 @@ public interface AdminUserService {
* @param id 用户 id
* @param avatarFile 头像文件
*/
String updateUserAvatar(Long id, InputStream avatarFile) throws Exception;
String updateUserAvatar(Long id, InputStream avatarFile);
/**
* 修改密码
@@ -81,6 +83,13 @@ public interface AdminUserService {
*/
void updateUserPassword(Long id, String password);
/**
* 重置密码
*
* @param reqVO
*/
void resetUserPassword(UserResetPasswordReqVO reqVO);
/**
* 修改状态
*
@@ -113,6 +122,14 @@ public interface AdminUserService {
*/
AdminUserDO getUserByUsernameAndOrganId(String username, Long organId);
/**
* 用户名查询唯一用户
*
* @param userName
* @return
*/
AdminUserDO getUserUniqueByUserName(String userName);
/**
* 通过手机号获取用户
*
@@ -242,4 +259,12 @@ public interface AdminUserService {
* 用户活跃数统计
*/
Map<String, Integer> userActTotal();
/**
* 修改密保手机
*
* @param id 用户编号
* @param reqVO 用户个人信息
*/
void updateUserSecurityPhone(Long id, @Valid UserMobileUpdateReqVO reqVO);
}
@@ -4,13 +4,17 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.cf.imes.framework.common.enums.CommonStatusEnum;
import com.cf.imes.framework.common.enums.UserTypeEnum;
import com.cf.imes.framework.common.exception.ServiceException;
import com.cf.imes.framework.common.exception.util.ServiceExceptionUtil;
import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.framework.common.util.collection.CollectionUtils;
import com.cf.imes.framework.common.util.monitor.TracerUtils;
import com.cf.imes.framework.common.util.object.BeanUtils;
import com.cf.imes.framework.common.util.pinyin.PinYinUtils;
import com.cf.imes.framework.common.util.servlet.ServletUtils;
import com.cf.imes.framework.datapermission.core.util.DataPermissionUtils;
import com.cf.imes.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.cf.imes.framework.mybatis.core.query.MPJLambdaWrapperX;
@@ -18,13 +22,17 @@ import com.cf.imes.framework.organ.core.aop.OrganIgnore;
import com.cf.imes.framework.organ.core.context.OrganContextHolder;
import com.cf.imes.framework.security.core.util.SecurityFrameworkUtils;
import com.cf.imes.module.infra.api.file.FileApi;
import com.cf.imes.module.system.api.logger.dto.LoginLogCreateReqDTO;
import com.cf.imes.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import com.cf.imes.module.system.api.user.dto.OrganAdminUserRespDTO;
import com.cf.imes.module.system.constants.permission.InternalRoleConstants;
import com.cf.imes.module.system.controller.admin.user.vo.profile.UserMobileUpdateReqVO;
import com.cf.imes.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO;
import com.cf.imes.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO;
import com.cf.imes.module.system.controller.admin.user.vo.user.UserImportExcelVO;
import com.cf.imes.module.system.controller.admin.user.vo.user.UserImportRespVO;
import com.cf.imes.module.system.controller.admin.user.vo.user.UserPageReqVO;
import com.cf.imes.module.system.controller.admin.user.vo.user.UserResetPasswordReqVO;
import com.cf.imes.module.system.controller.admin.user.vo.user.UserSaveReqVO;
import com.cf.imes.module.system.dal.dataobject.dept.DeptDO;
import com.cf.imes.module.system.dal.dataobject.dept.UserPostDO;
@@ -32,11 +40,16 @@ import com.cf.imes.module.system.dal.dataobject.permission.UserRoleDO;
import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO;
import com.cf.imes.module.system.dal.mysql.dept.UserPostMapper;
import com.cf.imes.module.system.dal.mysql.user.AdminUserMapper;
import com.cf.imes.module.system.enums.logger.LoginLogTypeEnum;
import com.cf.imes.module.system.enums.logger.LoginResultEnum;
import com.cf.imes.module.system.enums.sms.SmsSceneEnum;
import com.cf.imes.module.system.service.dept.DeptService;
import com.cf.imes.module.system.service.dept.PostService;
import com.cf.imes.module.system.service.logger.LoginLogService;
import com.cf.imes.module.system.service.permission.PermissionService;
import com.cf.imes.module.system.service.organ.OrganService;
import com.cf.imes.module.system.enums.ErrorCodeConstants;
import com.cf.imes.module.system.service.sms.SmsCodeService;
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -92,6 +105,12 @@ public class AdminUserServiceImpl implements AdminUserService {
@Resource
private FileApi fileApi;
@Resource
private SmsCodeService smsCodeService;
@Resource
private LoginLogService loginLogService;
// 定义密码校验的正则表达式
private static final String LENGTH_PATTERN = ".{6,}";
@@ -110,15 +129,14 @@ public class AdminUserServiceImpl implements AdminUserService {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.USER_COUNT_MAX, organ.getAccountCount());
}
});
// 改造:手机号直接当做用户名
createReqVO.setUsername(createReqVO.getMobile());
// 校验正确性
validateUserForCreateOrUpdate(null, createReqVO.getUsername(),
createReqVO.getMobile(), createReqVO.getEmail(), createReqVO.getDeptId(), createReqVO.getPostIds(),createReqVO.getPassword(), organId);
createReqVO.getEmail(), createReqVO.getDeptId(), createReqVO.getPostIds(), null, organId);
// 插入用户
AdminUserDO user = BeanUtils.toBean(createReqVO, AdminUserDO.class);
user.setOrganId(createReqVO.getOrganId());
//user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启
user.setPassword(encodePassword(createReqVO.getPassword())); // 加密密码
String pinyinFull = PinYinUtils.convertToPinyin(user.getNickname());
if (StringUtils.isNotBlank(pinyinFull)) {
pinyinFull = pinyinFull.replace(" ", "");
@@ -138,12 +156,11 @@ public class AdminUserServiceImpl implements AdminUserService {
@Transactional(rollbackFor = Exception.class)
public void updateUser(UserSaveReqVO updateReqVO) {
Long organId = updateReqVO.getOrganId() == null ? OrganContextHolder.getOrganId(): updateReqVO.getOrganId();
updateReqVO.setPassword(null); // 特殊:此处不更新密码
// 不允许自身操作
checkCurrentWhenOperate(updateReqVO.getId());
// 校验正确性
validateUserForCreateOrUpdate(updateReqVO.getId(), updateReqVO.getUsername(),
updateReqVO.getMobile(), updateReqVO.getEmail(), updateReqVO.getDeptId(), updateReqVO.getPostIds(),null, organId);
updateReqVO.getEmail(), updateReqVO.getDeptId(), updateReqVO.getPostIds(), null, organId);
// 更新用户
AdminUserDO updateObj = BeanUtils.toBean(updateReqVO, AdminUserDO.class);
@@ -190,7 +207,7 @@ public class AdminUserServiceImpl implements AdminUserService {
// 校验正确性
validateUserExists(id);
validateEmailUnique(id, reqVO.getEmail(), organId);
validateMobileUnique(id, reqVO.getMobile(), organId);
validateMobileUnique(id, reqVO.getMobile());
// 执行更新
userMapper.updateById(BeanUtils.toBean(reqVO, AdminUserDO.class).setId(id));
}
@@ -231,6 +248,46 @@ public class AdminUserServiceImpl implements AdminUserService {
userMapper.updateById(updateObj);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void resetUserPassword(UserResetPasswordReqVO reqVO) {
String mobile = reqVO.getMobile();
String password = reqVO.getPassword();
// 校验用户名
AdminUserDO adminUserDO = validateUserNameExists(mobile);
Long userId = adminUserDO.getId();
String username = adminUserDO.getUsername();
// 校验密码
validatePassword(password);
// 忘记密码消耗验证码
smsCodeService.useSmsCode(SmsCodeUseReqDTO.builder()
.mobile(mobile).code(reqVO.getSmsCaptchaVerification()).scene(SmsSceneEnum.USER_RESET_PASSWORD.getScene())
.build());
// 更新密码
AdminUserDO updateObj = new AdminUserDO();
updateObj.setId(userId);
updateObj.setPassword(encodePassword(password)); // 加密密码
userMapper.updateById(updateObj);
// 记录日志
createLoginLog(userId, username, LoginLogTypeEnum.LOGIN_FORGET_PWD, LoginResultEnum.SUCCESS);
}
private void createLoginLog(Long userId, String username,
LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) {
// 插入登录日志
LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
reqDTO.setLogType(logTypeEnum.getType());
reqDTO.setTraceId(TracerUtils.getTraceId());
reqDTO.setUserId(userId);
reqDTO.setUserType(UserTypeEnum.ADMIN.getValue());
reqDTO.setUsername(username);
reqDTO.setUserAgent(ServletUtils.getUserAgent());
reqDTO.setUserIp(ServletUtils.getClientIP());
reqDTO.setResult(loginResult.getResult());
loginLogService.createLoginLog(reqDTO);
}
@Override
public void updateUserStatus(Long id, Integer status) {
// 不允许自身操作
@@ -269,10 +326,15 @@ public class AdminUserServiceImpl implements AdminUserService {
public AdminUserDO getUserByUsernameAndOrganId(String username, Long organId) {
return userMapper.selectOne(new LambdaQueryWrapperX<AdminUserDO>()
.eq(AdminUserDO::getUsername, username)
.eq(AdminUserDO::getOrganId, organId)
.eqIfPresent(AdminUserDO::getOrganId, organId)
);
}
@Override
public AdminUserDO getUserUniqueByUserName(String userName) {
return userMapper.selectByUsernameUnique(userName);
}
@Override
public AdminUserDO getUserByMobile(String mobile) {
return userMapper.selectByMobile(mobile);
@@ -363,7 +425,7 @@ public class AdminUserServiceImpl implements AdminUserService {
return deptIds;
}
private void validateUserForCreateOrUpdate(Long id, String username, String mobile, String email,
private void validateUserForCreateOrUpdate(Long id, String username, String email,
Long deptId, Set<Long> postIds,String password ,Long organId) {
// 关闭数据权限,避免因为没有数据权限,查询不到数据,进而导致唯一校验不正确
DataPermissionUtils.executeIgnore(() -> {
@@ -375,8 +437,6 @@ public class AdminUserServiceImpl implements AdminUserService {
validateUserExists(id);
// 校验用户名唯一
validateUsernameUnique(id, username, organId);
// 校验手机号唯一
validateMobileUnique(id, mobile, organId);
// 校验邮箱唯一
validateEmailUnique(id, email, organId);
// 校验部门处于开启状态
@@ -420,11 +480,21 @@ public class AdminUserServiceImpl implements AdminUserService {
}
@VisibleForTesting
private AdminUserDO validateUserNameExists(String userName) {
AdminUserDO user = userMapper.selectByUsernameUnique(userName);
if (user == null) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.USER_NOT_EXISTS);
}
return user;
}
@VisibleForTesting
@OrganIgnore
void validateUsernameUnique(Long id, String username, Long organId) {
if (StringUtils.isBlank(username)) {
return;
}
AdminUserDO user = userMapper.selectOne(AdminUserDO::getUsername, username, AdminUserDO::getOrganId, organId);
AdminUserDO user = userMapper.selectOne(AdminUserDO::getUsername, username);
if (user == null) {
return;
}
@@ -456,11 +526,12 @@ public class AdminUserServiceImpl implements AdminUserService {
}
@VisibleForTesting
void validateMobileUnique(Long id, String mobile, Long organId) {
@OrganIgnore
void validateMobileUnique(Long id, String mobile) {
if (StringUtils.isBlank(mobile)) {
return;
}
AdminUserDO user = userMapper.selectOne(AdminUserDO::getOrganId, organId, AdminUserDO::getMobile, mobile);
AdminUserDO user = userMapper.selectOne(AdminUserDO::getUsername, mobile);
if (user == null) {
return;
}
@@ -501,7 +572,7 @@ public class AdminUserServiceImpl implements AdminUserService {
importUsers.forEach(importUser -> {
// 校验,判断是否有不符合的原因
try {
validateUserForCreateOrUpdate(null, null, importUser.getMobile(), importUser.getEmail(),
validateUserForCreateOrUpdate(null, null, importUser.getEmail(),
importUser.getDeptId(), null,null, organId);
} catch (ServiceException ex) {
respVO.getFailureUsernames().put(importUser.getUsername(), ex.getMessage());
@@ -510,6 +581,8 @@ public class AdminUserServiceImpl implements AdminUserService {
// 判断如果不存在,在进行插入
AdminUserDO existUser = userMapper.selectByUsername(importUser.getUsername(), organId);
if (existUser == null) {
// 改造:手机号直接当做用户名
importUser.setUsername(importUser.getMobile());
userMapper.insert(BeanUtils.toBean(importUser, AdminUserDO.class)
.setPassword(encodePassword(userInitPassword)).setPostIds(new HashSet<>())); // 设置默认密码及空岗位编号数组
respVO.getCreateUsernames().add(importUser.getUsername());
@@ -563,6 +636,24 @@ public class AdminUserServiceImpl implements AdminUserService {
return organAdminUserRespDTOS;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateUserSecurityPhone(Long id, UserMobileUpdateReqVO reqVO) {
String mobile = reqVO.getMobile();
// 校验手机号唯一
validateMobileUnique(id, mobile);
// 校验和消耗验证码
smsCodeService.useSmsCode(SmsCodeUseReqDTO.builder()
.mobile(reqVO.getMobile()).code(reqVO.getSmsCaptchaVerification()).scene(SmsSceneEnum.USER_UPDATE_MOBILE.getScene())
.build());
// 新手机号入库 -> setUsername
userMapper.update(new LambdaUpdateWrapper<AdminUserDO>()
.set(AdminUserDO::getUsername, mobile)
.eq(AdminUserDO::getId, id));
}
/**
* 对密码进行加密
*
@@ -159,6 +159,8 @@ chenfeng:
- /admin-api/system/data-source/*
- /admin-api/system/auth/login
- /admin-api/system/auth/logout
- /admin-api/system/auth/sms-code
- /admin-api/system/user/reset-password
- /rpc-api/**
#- /admin-api/system/**
#- /rpc-api/system/organ/valid # 防止递归。避免调用 /rpc-api/system/organ/valid 接口时,又去触发 /rpc-api/system/organ/valid 去校验
@@ -193,10 +195,19 @@ chenfeng:
use-data-code: imes_prod #动态数据源标识,后期添加的数据源需要修改此值
sms-code: # 短信验证码相关的配置项
expire-times: 10m
expire-times: 5m
send-frequency: 1m
send-maximum-quantity-per-day: 10
send-max-send-times-per-window: 5 #窗口最大请求次数
send-window-size-min: 10m #窗口时间
begin-code: 9999 # 这里配置 9999 的原因是,测试方便。
end-code: 9999 # 这里配置 9999 的原因是,测试方便。
sms:
channel: DEBUG_DING_TALK
signature: AKhcjogF4mzWTn5G0I0sfQ==
apiKey: Bw9mvVZZ8cLBLoPFl9yY/AI1xOUCEALEtftakrgiv9BT/dOuKUhWsXuFr9UvxRlMSRporf8Wcd7C6IFB9ukUR0IWgKTelx2vv2Lhzdnz4ac=
apiSecret: Um956u5zFSl0tu8imFB/TOZyK7xysT5JMLY17mZIwXH+eh4BjZEmsjmFRfkcXin0A/GNhRzcUurJfm+N/S42uJZ9G9aCCIoPSjpnF9SXwt0=
encrypt:
enable: true
publicKey: cfimes
debug: false
@@ -5,20 +5,14 @@ import com.cf.imes.framework.common.enums.CommonStatusEnum;
import com.cf.imes.framework.common.enums.UserTypeEnum;
import com.cf.imes.framework.test.core.ut.BaseDbUnitTest;
import com.cf.imes.module.system.api.sms.SmsCodeApi;
import com.cf.imes.module.system.api.social.dto.SocialUserBindReqDTO;
import com.cf.imes.module.system.api.social.dto.SocialUserRespDTO;
import com.cf.imes.module.system.controller.admin.auth.vo.*;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginReqVO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthLoginRespVO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthSmsLoginReqVO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthSmsSendReqVO;
import com.cf.imes.module.system.controller.admin.auth.vo.AuthSocialLoginReqVO;
import com.cf.imes.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import com.cf.imes.module.system.dal.dataobject.user.AdminUserDO;
import com.cf.imes.module.system.enums.logger.LoginLogTypeEnum;
import com.cf.imes.module.system.enums.logger.LoginResultEnum;
import com.cf.imes.module.system.enums.sms.SmsSceneEnum;
import com.cf.imes.module.system.enums.social.SocialTypeEnum;
import com.cf.imes.module.system.service.logger.LoginLogService;
import com.cf.imes.module.system.service.member.MemberService;
import com.cf.imes.module.system.service.oauth2.OAuth2TokenService;
@@ -91,7 +85,7 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest {
when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true);
// 调用
AdminUserDO loginUser = authService.authenticate(username, password, null);
AdminUserDO loginUser = authService.authenticate(AuthLoginReqVO.builder().username(username).password(password).build());
// 校验
assertPojoEquals(user, loginUser);
}
@@ -103,7 +97,7 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest {
String password = randomString();
// 调用, 并断言异常
assertServiceException(() -> authService.authenticate(username, password, null),
assertServiceException(() -> authService.authenticate(AuthLoginReqVO.builder().username(username).password(password).build()),
AUTH_LOGIN_BAD_CREDENTIALS);
verify(loginLogService).createLoginLog(
argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
@@ -123,7 +117,7 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest {
when(userService.getUserByUsername(eq(username), null)).thenReturn(user);
// 调用, 并断言异常
assertServiceException(() -> authService.authenticate(username, password, null),
assertServiceException(() -> authService.authenticate(AuthLoginReqVO.builder().username(username).password(password).build()),
AUTH_LOGIN_BAD_CREDENTIALS);
verify(loginLogService).createLoginLog(
argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
@@ -145,7 +139,7 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest {
when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true);
// 调用, 并断言异常
assertServiceException(() -> authService.authenticate(username, password, null),
assertServiceException(() -> authService.authenticate(AuthLoginReqVO.builder().username(username).password(password).build()),
AUTH_LOGIN_USER_DISABLED);
verify(loginLogService).createLoginLog(
argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType())
@@ -5,6 +5,7 @@ import com.cf.imes.framework.common.pojo.PageResult;
import com.cf.imes.framework.common.util.object.BeanUtils;
import com.cf.imes.framework.sms.core.client.SmsClient;
import com.cf.imes.framework.sms.core.client.SmsClientFactory;
import com.cf.imes.framework.sms.core.enums.SmsChannelEnum;
import com.cf.imes.framework.sms.core.property.SmsChannelProperties;
import com.cf.imes.framework.test.core.ut.BaseDbUnitTest;
import com.cf.imes.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO;
@@ -200,16 +201,12 @@ public class SmsChannelServiceTest extends BaseDbUnitTest {
Long id = channel.getId();
// mock 方法
SmsClient mockClient = mock(SmsClient.class);
when(smsClientFactory.getSmsClient(eq(id))).thenReturn(mockClient);
when(smsClientFactory.getSmsClient(eq(SmsChannelEnum.ALIYUN.getCode()))).thenReturn(mockClient);
// 调用
SmsClient client = smsChannelService.getSmsClient(id);
// 断言
assertSame(client, mockClient);
verify(smsClientFactory).createOrUpdateSmsClient(argThat(arg -> {
SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);
return properties.equals(arg);
}));
}
@Test
@@ -227,10 +224,6 @@ public class SmsChannelServiceTest extends BaseDbUnitTest {
SmsClient client = smsChannelService.getSmsClient(code);
// 断言
assertSame(client, mockClient);
verify(smsClientFactory).createOrUpdateSmsClient(argThat(arg -> {
SmsChannelProperties properties = BeanUtils.toBean(channel, SmsChannelProperties.class);
return properties.equals(arg);
}));
}
}
@@ -130,10 +130,11 @@ public class SmsLogServiceImplTest extends BaseDbUnitTest {
String apiSendMsg = randomString();
String apiRequestId = randomString();
String apiSerialNo = randomString();
String channelCode = randomString();
// 调用
smsLogService.updateSmsSendResult(id, success,
apiSendCode, apiSendMsg, apiRequestId, apiSerialNo);
apiSendCode, apiSendMsg, apiRequestId, apiSerialNo, channelCode);
// 断言
dbSmsLog = smsLogMapper.selectById(id);
assertEquals(success ? SmsSendStatusEnum.SUCCESS.getStatus() : SmsSendStatusEnum.FAILURE.getStatus(),
@@ -85,8 +85,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
// 断言
assertEquals(smsLogId, resultSmsLogId);
// 断言调用
verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(user.getMobile()),
eq(template.getChannelId()), eq(template.getApiTemplateId()),
verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(user.getMobile()), eq(template),
eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login"))));
}
@@ -124,8 +123,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
// 断言
assertEquals(smsLogId, resultSmsLogId);
// 断言调用
verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile),
eq(template.getChannelId()), eq(template.getApiTemplateId()),
verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile), eq(template),
eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login"))));
}
@@ -164,8 +162,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
// 断言
assertEquals(smsLogId, resultSmsLogId);
// 断言调用
verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile),
eq(template.getChannelId()), eq(template.getApiTemplateId()),
verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile), eq(template),
eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login"))));
}
@@ -204,8 +201,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
// 断言
assertEquals(smsLogId, resultSmsLogId);
// 断言调用
verify(smsProducer, times(0)).sendSmsSendMessage(anyLong(), anyString(),
anyLong(), any(), anyList());
verify(smsProducer, times(0)).sendSmsSendMessage(anyLong(), any(), any(), anyList());
}
@Test
@@ -274,7 +270,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest {
// 断言
verify(smsLogService).updateSmsSendResult(eq(message.getLogId()),
eq(sendResult.getSuccess()), eq(sendResult.getApiCode()),
eq(sendResult.getApiMsg()), eq(sendResult.getApiRequestId()), eq(sendResult.getSerialNo()));
eq(sendResult.getApiMsg()), eq(sendResult.getApiRequestId()), eq(sendResult.getSerialNo()), eq(sendResult.getChannelCode()));
}
@Test
@@ -588,7 +588,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest {
userMapper.insert(randomAdminUserDO(o -> o.setMobile(mobile)));
// 调用,校验异常
assertServiceException(() -> userService.validateMobileUnique(null, mobile, 1L),
assertServiceException(() -> userService.validateMobileUnique(null, mobile),
USER_MOBILE_EXISTS);
}
@@ -601,7 +601,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest {
userMapper.insert(randomAdminUserDO(o -> o.setMobile(mobile)));
// 调用,校验异常
assertServiceException(() -> userService.validateMobileUnique(id, mobile, 1L),
assertServiceException(() -> userService.validateMobileUnique(id, mobile),
USER_MOBILE_EXISTS);
}