网站建设的技术有哪些方面北京发生大事了
分组接口(Group Interface)是 Bean Validation(JSR 380) 提供的一种机制,用于在参数校验时动态选择需要校验的字段或规则。通过分组接口,可以将校验逻辑按业务场景划分,从而实现更灵活的参数校验。
以下是分组接口的技术细节和实现原理:
1. 分组接口的定义
分组接口是一个普通的 Java 接口,没有任何方法。它的作用仅仅是作为一个标记(Marker Interface),用于标识不同的校验场景。
public interface GroupA {}public interface GroupB {}
2. 在字段上指定分组
在校验注解(如 @NotEmpty
、@Size
等)中,可以通过 groups
属性指定该注解所属的分组。
public class User {@NotEmpty(message = "用户名不能为空", groups = GroupA.class)private String username;@Size(min = 6, max = 20, message = "密码长度必须在6到20之间", groups = GroupB.class)private String password;
}
username
字段的@NotEmpty
校验规则属于GroupA
分组。password
字段的@Size
校验规则属于GroupB
分组。
3. 在 Controller 中使用分组校验
在 Spring MVC 中,可以通过 @Validated
注解指定校验分组。
@RestController
public class UserController {@PostMapping("/createUser")public ResponseEntity<?> createUser(@Validated(GroupA.class) @RequestBody User user) {// 只校验属于 GroupA 分组的字段return ResponseEntity.ok("用户创建成功");}@PostMapping("/updateUser")public ResponseEntity<?> updateUser(@Validated({GroupA.class, GroupB.class}) @RequestBody User user) {// 校验属于 GroupA 和 GroupB 分组的字段return ResponseEntity.ok("用户更新成功");}
}
- 在
createUser
方法中,只会校验username
字段(因为username
属于GroupA
分组)。 - 在
updateUser
方法中,会同时校验username
和password
字段(因为它们分别属于GroupA
和GroupB
分组)。
4. 默认分组
如果没有指定 groups
属性,校验注解默认属于 javax.validation.groups.Default
分组。
@NotEmpty(message = "用户名不能为空") // 默认属于 Default 分组
private String username;
5. 分组继承
分组接口可以继承其他接口,从而实现分组的组合。
public interface GroupA {}public interface GroupB extends GroupA {}
- 如果一个字段的校验注解属于
GroupB
分组,那么它也会被GroupA
分组校验。
6. 动态分组
分组校验可以根据业务逻辑动态选择。例如,根据系统配置决定是否启用某些校验规则。
@PostMapping("/login")
public ResponseEntity<?> login(@Validated({Default.class, CodeEnableGroup.class}) @RequestBody AuthLoginReqVO reqVO) {// 根据系统配置动态选择分组if (enableCaptcha) {// 启用验证码校验validator.validate(reqVO, CodeEnableGroup.class);}return ResponseEntity.ok("登录成功");
}
7. 自定义校验器与分组
如果你实现了自定义校验器(通过 ConstraintValidator
),可以在校验器中获取当前的分组信息。
public class CustomValidator implements ConstraintValidator<CustomAnnotation, String> {@Overridepublic boolean isValid(String value, ConstraintValidatorContext context) {// 获取当前的分组信息Set<Class<?>> groups = context.getConstraintDescriptor().getGroups();if (groups.contains(GroupA.class)) {// 执行 GroupA 分组的校验逻辑}return true;}
}
8. 分组校验的实现原理
Spring Validation 是基于 Bean Validation(Hibernate Validator)实现的。分组校验的核心原理如下:
-
注解解析:
- Spring 会解析
@Validated
注解中指定的分组。 - 根据分组信息,筛选出需要校验的字段和规则。
- Spring 会解析
-
校验器调用:
- Spring 调用 Bean Validation 的校验器(如 Hibernate Validator),并传入分组信息。
- 校验器根据分组信息执行相应的校验逻辑。
-
校验结果处理:
- 如果校验失败,Spring 会抛出
MethodArgumentNotValidException
异常,并返回错误信息。
- 如果校验失败,Spring 会抛出
9. 分组校验的优缺点
优点:
- 灵活性:可以根据业务场景动态选择校验规则。
- 解耦:将校验逻辑与业务逻辑分离,代码更清晰。
- 复用性:同一个字段可以在不同的分组中复用,避免重复定义。
缺点:
- 复杂度增加:如果分组过多,可能会导致代码复杂度增加。
- 维护成本:需要仔细管理分组接口,避免分组冲突或遗漏。
10. 最佳实践
-
按业务场景划分分组:
- 将分组接口与具体的业务场景对应,例如
CreateGroup
、UpdateGroup
、LoginGroup
等。
- 将分组接口与具体的业务场景对应,例如
-
避免过度分组:
- 不要为每个字段都创建单独的分组,而是将相关的字段划分到同一个分组中。
-
使用默认分组:
- 对于通用的校验规则,可以使用
Default
分组,避免重复定义。
- 对于通用的校验规则,可以使用
-
分组继承:
- 通过分组继承实现分组的组合,减少重复代码。
总结
分组接口是 Bean Validation 提供的一种强大机制,用于实现动态、灵活的参数校验。通过分组接口,可以根据业务场景选择需要校验的字段和规则,从而提高代码的可维护性和复用性。在实际开发中,合理使用分组接口可以显著提升参数校验的灵活性和可读性。
package cn.iocoder.yudao.module.system.controller.admin.auth.vo;import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;@Schema(description = "管理后台 - 账号密码登录 Request VO,如果登录并绑定社交用户,需要传递 social 开头的参数")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AuthLoginReqVO {@Schema(description = "账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudaoyuanma")@NotEmpty(message = "登录账号不能为空")@Length(min = 4, max = 16, message = "账号长度为 4-16 位")@Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")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 = "PfcH6mgr8tpXuMWFjvW6YVaqrswIuwmWI5dsVZSg7sGpWtDCUbHuDEXl3cFB1+VvCC/rAkSwK8Fad52FSuncVg==")@NotEmpty(message = "验证码不能为空", groups = CodeEnableGroup.class)private String captchaVerification;// ========== 绑定社交登录时,需要传递如下参数 ==========@Schema(description = "社交平台的类型,参见 SocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")@InEnum(SocialTypeEnum.class)private Integer socialType;@Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")private String socialCode;@Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62")private String socialState;/*** 开启验证码的 Group*/public interface CodeEnableGroup {}@AssertTrue(message = "授权码不能为空")public boolean isSocialCodeValid() {return socialType == null || StrUtil.isNotEmpty(socialCode);}@AssertTrue(message = "授权 state 不能为空")public boolean isSocialState() {return socialType == null || StrUtil.isNotEmpty(socialState);}}
package cn.iocoder.yudao.module.system.service.auth;import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*;
import cn.iocoder.yudao.module.system.convert.auth.AuthConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
import cn.iocoder.yudao.module.system.enums.oauth2.OAuth2ClientConstants;
import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
import cn.iocoder.yudao.module.system.service.logger.LoginLogService;
import cn.iocoder.yudao.module.system.service.member.MemberService;
import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService;
import cn.iocoder.yudao.module.system.service.social.SocialUserService;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import com.google.common.annotations.VisibleForTesting;
import com.xingyuv.captcha.model.common.ResponseModel;
import com.xingyuv.captcha.model.vo.CaptchaVO;
import com.xingyuv.captcha.service.CaptchaService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import javax.validation.Validator;
import java.util.Objects;import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;/*** Auth Service 实现类** @author 芋道源码*/
@Service
@Slf4j
public class AdminAuthServiceImpl implements AdminAuthService {@Resourceprivate AdminUserService userService;@Resourceprivate LoginLogService loginLogService;@Resourceprivate OAuth2TokenService oauth2TokenService;@Resourceprivate SocialUserService socialUserService;@Resourceprivate MemberService memberService;@Resourceprivate Validator validator;@Resourceprivate CaptchaService captchaService;@Resourceprivate SmsCodeApi smsCodeApi;/*** 验证码的开关,默认为 true*/@Value("${yudao.captcha.enable:true}")private Boolean captchaEnable;@Overridepublic AdminUserDO authenticate(String username, String password) {final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME;// 校验账号是否存在AdminUserDO user = userService.getUserByUsername(username);if (user == null) {createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);throw exception(AUTH_LOGIN_BAD_CREDENTIALS);}if (!userService.isPasswordMatch(password, user.getPassword())) {createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);throw exception(AUTH_LOGIN_BAD_CREDENTIALS);}// 校验是否禁用if (CommonStatusEnum.isDisable(user.getStatus())) {createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED);throw exception(AUTH_LOGIN_USER_DISABLED);}return user;}@Overridepublic AuthLoginRespVO login(AuthLoginReqVO reqVO) {// 校验验证码validateCaptcha(reqVO);// 使用账号密码,进行登录AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword());// 如果 socialType 非空,说明需要绑定社交用户if (reqVO.getSocialType() != null) {socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()));}// 创建 Token 令牌,记录登录日志return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME);}@Overridepublic void sendSmsCode(AuthSmsSendReqVO reqVO) {// 登录场景,验证是否存在if (userService.getUserByMobile(reqVO.getMobile()) == null) {throw exception(AUTH_MOBILE_NOT_EXISTS);}// 发送验证码smsCodeApi.sendSmsCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP()));}@Overridepublic AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO) {// 校验验证码smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.ADMIN_MEMBER_LOGIN.getScene(), getClientIP()));// 获得用户信息AdminUserDO user = userService.getUserByMobile(reqVO.getMobile());if (user == null) {throw exception(USER_NOT_EXISTS);}// 创建 Token 令牌,记录登录日志return createTokenAfterLoginSuccess(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE);}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(getUserType().getValue());reqDTO.setUsername(username);reqDTO.setUserAgent(ServletUtils.getUserAgent());reqDTO.setUserIp(ServletUtils.getClientIP());reqDTO.setResult(loginResult.getResult());loginLogService.createLoginLog(reqDTO);// 更新最后登录时间if (userId != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) {userService.updateUserLogin(userId, ServletUtils.getClientIP());}}@Overridepublic AuthLoginRespVO socialLogin(AuthSocialLoginReqVO reqVO) {// 使用 code 授权码,进行登录。然后,获得到绑定的用户编号SocialUserRespDTO socialUser = socialUserService.getSocialUserByCode(UserTypeEnum.ADMIN.getValue(), reqVO.getType(),reqVO.getCode(), reqVO.getState());if (socialUser == null || socialUser.getUserId() == null) {throw exception(AUTH_THIRD_LOGIN_NOT_BIND);}// 获得用户AdminUserDO user = userService.getUser(socialUser.getUserId());if (user == null) {throw exception(USER_NOT_EXISTS);}// 创建 Token 令牌,记录登录日志return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL);}@VisibleForTestingvoid validateCaptcha(AuthLoginReqVO reqVO) {// 如果验证码关闭,则不进行校验if (!captchaEnable) {return;}// 校验验证码ValidationUtils.validate(validator, reqVO, AuthLoginReqVO.CodeEnableGroup.class);CaptchaVO captchaVO = new CaptchaVO();captchaVO.setCaptchaVerification(reqVO.getCaptchaVerification());ResponseModel response = captchaService.verification(captchaVO);// 验证不通过if (!response.isSuccess()) {// 创建登录失败日志(验证码不正确)createLoginLog(null, reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME, LoginResultEnum.CAPTCHA_CODE_ERROR);throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR, response.getRepMsg());}}private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) {// 插入登陆日志createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS);// 创建访问令牌OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(),OAuth2ClientConstants.CLIENT_ID_DEFAULT, null);// 构建返回结果return AuthConvert.INSTANCE.convert(accessTokenDO);}@Overridepublic AuthLoginRespVO refreshToken(String refreshToken) {OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT);return AuthConvert.INSTANCE.convert(accessTokenDO);}@Overridepublic void logout(String token, Integer logType) {// 删除访问令牌OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.removeAccessToken(token);if (accessTokenDO == null) {return;}// 删除成功,则记录登出日志createLogoutLog(accessTokenDO.getUserId(), accessTokenDO.getUserType(), logType);}private void createLogoutLog(Long userId, Integer userType, Integer logType) {LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();reqDTO.setLogType(logType);reqDTO.setTraceId(TracerUtils.getTraceId());reqDTO.setUserId(userId);reqDTO.setUserType(userType);if (ObjectUtil.equal(getUserType().getValue(), userType)) {reqDTO.setUsername(getUsername(userId));} else {reqDTO.setUsername(memberService.getMemberUserMobile(userId));}reqDTO.setUserAgent(ServletUtils.getUserAgent());reqDTO.setUserIp(ServletUtils.getClientIP());reqDTO.setResult(LoginResultEnum.SUCCESS.getResult());loginLogService.createLoginLog(reqDTO);}private String getUsername(Long userId) {if (userId == null) {return null;}AdminUserDO user = userService.getUser(userId);return user != null ? user.getUsername() : null;}private UserTypeEnum getUserType() {return UserTypeEnum.ADMIN;}@Overridepublic AuthLoginRespVO register(AuthRegisterReqVO registerReqVO) {// 1. 校验验证码validateCaptcha(registerReqVO);// 2. 校验用户名是否已存在Long userId = userService.registerUser(registerReqVO);// 3. 创建 Token 令牌,记录登录日志return createTokenAfterLoginSuccess(userId, registerReqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME);}@VisibleForTestingvoid validateCaptcha(AuthRegisterReqVO reqVO) {// 如果验证码关闭,则不进行校验if (!captchaEnable) {return;}// 校验验证码ValidationUtils.validate(validator, reqVO, AuthLoginReqVO.CodeEnableGroup.class);CaptchaVO captchaVO = new CaptchaVO();captchaVO.setCaptchaVerification(reqVO.getCaptchaVerification());ResponseModel response = captchaService.verification(captchaVO);// 验证不通过if (!response.isSuccess()) {throw exception(AUTH_REGISTER_CAPTCHA_CODE_ERROR, response.getRepMsg());}}}