diff --git a/pigx-app-server/pigx-app-server-biz/src/main/java/com/pig4cloud/pigx/app/service/impl/AppMobileServiceImpl.java b/pigx-app-server/pigx-app-server-biz/src/main/java/com/pig4cloud/pigx/app/service/impl/AppMobileServiceImpl.java index af31cf488..25c969c8d 100644 --- a/pigx-app-server/pigx-app-server-biz/src/main/java/com/pig4cloud/pigx/app/service/impl/AppMobileServiceImpl.java +++ b/pigx-app-server/pigx-app-server-biz/src/main/java/com/pig4cloud/pigx/app/service/impl/AppMobileServiceImpl.java @@ -59,7 +59,7 @@ public class AppMobileServiceImpl implements AppMobileService { */ @Override public R sendSmsCode(String mobile) { - String codeObj = RedisUtils.get(CacheConstants.DEFAULT_CODE_KEY + LoginTypeEnum.APPSMS.getType() + StringPool.AT + mobile); + String codeObj = RedisUtils.get(CacheConstants.SMS_CODE_KEY + LoginTypeEnum.APPSMS.getType() + StringPool.AT + mobile); if (StrUtil.isNotBlank(codeObj)) { log.info("手机号验证码未过期:{},{}", mobile, codeObj); @@ -68,7 +68,7 @@ public class AppMobileServiceImpl implements AppMobileService { String code = RandomUtil.randomNumbers(Integer.parseInt(SecurityConstants.CODE_SIZE)); log.debug("手机号生成验证码成功:{},{}", mobile, code); - RedisUtils.set(CacheConstants.DEFAULT_CODE_KEY + LoginTypeEnum.APPSMS.getType() + StringPool.AT + mobile, code, SecurityConstants.CODE_TIME); + RedisUtils.set(CacheConstants.SMS_CODE_KEY + LoginTypeEnum.APPSMS.getType() + StringPool.AT + mobile, code, SecurityConstants.CODE_TIME); return R.ok(Boolean.TRUE); } diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/ImageCodeEndpoint.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/ImageCodeEndpoint.java index 1bab2f344..3d8a90923 100644 --- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/ImageCodeEndpoint.java +++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/ImageCodeEndpoint.java @@ -50,7 +50,7 @@ public class ImageCodeEndpoint { return; } - RedisUtils.set(CacheConstants.DEFAULT_CODE_KEY + randomStr, result, SecurityConstants.CODE_TIME); + RedisUtils.set(CacheConstants.IMAGE_CODE_KEY + randomStr, result, SecurityConstants.CODE_TIME); // 转换流信息写出 captcha.out(response.getOutputStream()); } diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/core/AuthCaptchaSupport.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/core/AuthCaptchaSupport.java index 73c5aff84..72adf9591 100644 --- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/core/AuthCaptchaSupport.java +++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/core/AuthCaptchaSupport.java @@ -6,6 +6,7 @@ import cn.hutool.json.JSONUtil; import com.pig4cloud.pigx.common.core.constant.CacheConstants; import com.pig4cloud.pigx.common.core.constant.CommonConstants; import com.pig4cloud.pigx.common.core.constant.enums.CaptchaFlagTypeEnum; +import com.pig4cloud.pigx.common.core.constant.enums.LoginTypeEnum; import com.pig4cloud.pigx.common.core.exception.ValidateCodeException; import com.pig4cloud.pigx.common.core.util.MsgUtils; import com.pig4cloud.pigx.common.data.cache.RedisUtils; @@ -75,7 +76,7 @@ public class AuthCaptchaSupport { */ public boolean isCaptchaEnabled(String tenantId, String clientId) { if (StrUtil.isBlank(clientId)) { - return false; + return true; } String finalTenantId = StrUtil.isBlank(tenantId) ? CommonConstants.TENANT_ID_1.toString() : tenantId; @@ -103,12 +104,6 @@ public class AuthCaptchaSupport { } String randomStr = request.getParameter("randomStr"); - // https://gitee.com/log4j/pig/issues/IWA0D 手机号场景以 mobile 作为缓存键 - String mobile = request.getParameter("mobile"); - if (StrUtil.isNotBlank(mobile)) { - randomStr = mobile; - } - boolean isBehavior = StrUtil.equalsAnyIgnoreCase(randomStr, CommonConstants.IMAGE_CODE_BLOCK_PUZZLE, CommonConstants.IMAGE_CODE_CLICK_WORD); String captchaType = isBehavior ? randomStr : CommonConstants.IMAGE_CODE_MATH; @@ -122,6 +117,36 @@ public class AuthCaptchaSupport { } } + /** + * 按完整类型前缀判断是否使用短信登录,不校验手机号格式或验证码。 + * + * @param mobile 登录标识,格式为 {@code SMS@手机号} 或 {@code APP-SMS@手机号};允许为 null + * @return 匹配短信类型前缀时为 true;null、空串或其他类型返回 false + */ + public boolean isSmsLogin(String mobile) { + return StrUtil.startWith(mobile, LoginTypeEnum.SMS.getType() + StrUtil.AT) + || StrUtil.startWith(mobile, LoginTypeEnum.APPSMS.getType() + StrUtil.AT); + } + + /** + * 校验请求中的短信身份凭据,不接受图形或行为验证码作为替代。 + * 类型合法且验证码非空时委托缓存校验;读取缓存后无论匹配与否均消费该验证码。 + * + * @param request 非 null 请求,mobile 参数为 {@code SMS@手机号} 或 + * {@code APP-SMS@手机号},code 参数为短信验证码 + * @throws ValidateCodeException 验证码为空、登录类型不合法,或验证码不存在、过期、不匹配 + */ + public void validateSmsCode(HttpServletRequest request) { + String mobile = request.getParameter("mobile"); + String code = request.getParameter("code"); + if (StrUtil.isBlank(code)) { + throw new ValidateCodeException(MsgUtils.getMessage(AuthErrorCodes.AUTH_CAPTCHA_EMPTY)); + } + if (!isSmsLogin(mobile) || !captchaValidator.validateSms(mobile, code).isOk()) { + throw new ValidateCodeException(MsgUtils.getMessage(AuthErrorCodes.AUTH_CAPTCHA_INVALID)); + } + } + private String resolveAuthorizationParameter(HttpServletRequest request, HttpServletResponse response, String parameterName, boolean includeCurrentRequestId) { if (includeCurrentRequestId) { diff --git a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/filter/ValidateCodeFilter.java b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/filter/ValidateCodeFilter.java index ed46d6753..3c008d2b4 100644 --- a/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/filter/ValidateCodeFilter.java +++ b/pigx-auth/src/main/java/com/pig4cloud/pigx/auth/support/filter/ValidateCodeFilter.java @@ -12,20 +12,22 @@ import com.pig4cloud.pigx.auth.support.core.AuthCaptchaSupport; import com.pig4cloud.pigx.common.core.constant.CommonConstants; import com.pig4cloud.pigx.common.core.constant.SecurityConstants; import com.pig4cloud.pigx.common.core.exception.ValidateCodeException; -import com.pig4cloud.pigx.common.core.util.WebUtils; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; +import java.util.Objects; /** * @author lbw @@ -54,45 +56,30 @@ public class ValidateCodeFilter extends OncePerRequestFilter { // 如果登录URL 但是刷新token的请求,直接向下执行 String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE); - if (StrUtil.containsAny(grantType, SecurityConstants.REFRESH_TOKEN)) { + if (StrUtil.equals(grantType, SecurityConstants.REFRESH_TOKEN)) { filterChain.doFilter(request, response); return; } - // mobile模式, 如果请求不包含mobile 参数直接 - String mobile = request.getParameter(SecurityConstants.GRANT_MOBILE); - if (StrUtil.equals(SecurityConstants.GRANT_MOBILE, grantType) && StrUtil.isBlank(mobile)) { - throw new OAuth2AuthenticationException(SecurityConstants.GRANT_MOBILE); - } - - // mobile模式, 社交登录模式不校验验证码直接跳过 - if (StrUtil.equals(SecurityConstants.GRANT_MOBILE, grantType) && !StrUtil.contains(mobile, "SMS")) { - filterChain.doFilter(request, response); - return; - } - - // 判断客户端是否跳过检验 - if (!isCheckCaptchaClient(request)) { - filterChain.doFilter(request, response); - return; - } - - // 校验验证码 1. 客户端开启验证码 2. 短信模式 try { - checkCode(); + if (StrUtil.equals(SecurityConstants.GRANT_MOBILE, grantType)) { + String mobile = request.getParameter(SecurityConstants.GRANT_MOBILE); + if (StrUtil.isBlank(mobile)) { + throw new OAuth2AuthenticationException(SecurityConstants.GRANT_MOBILE); + } + // 短信是身份凭据,不能随客户端图形验证码开关关闭。 + if (authCaptchaSupport.isSmsLogin(mobile)) { + authCaptchaSupport.validateSmsCode(request); + } + } else if (isCheckCaptchaClient(request)) { + authCaptchaSupport.validateCode(request); + } filterChain.doFilter(request, response); } catch (ValidateCodeException validateCodeException) { throw new OAuth2AuthenticationException(validateCodeException.getMessage()); } } - /** - * 校验验证码 - */ - private void checkCode() throws ValidateCodeException { - authCaptchaSupport.validateCode(WebUtils.getRequest()); - } - /** * 是否需要校验客户端,根据client 查询客户端配置 * @@ -100,8 +87,12 @@ public class ValidateCodeFilter extends OncePerRequestFilter { * @return true 需要校验, false 不需要校验 */ private boolean isCheckCaptchaClient(HttpServletRequest request) { - String header = request.getHeader(HttpHeaders.AUTHORIZATION); - String clientId = WebUtils.extractClientId(header).orElse(null); + // 客户端认证过滤器在本过滤器之前执行,兼容 Basic 和 client_secret_post, + // 不信任额外传入的 client_id 来决定验证码策略。 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String clientId = authentication instanceof OAuth2ClientAuthenticationToken client + && client.isAuthenticated() && Objects.nonNull(client.getRegisteredClient()) + ? client.getRegisteredClient().getClientId() : null; // 获取租户拼接区分租户的key String tenantId = request.getHeader(CommonConstants.TENANT_ID); return authCaptchaSupport.isCaptchaEnabled(tenantId, clientId); diff --git a/pigx-auth/src/test/java/com/pig4cloud/pigx/auth/support/filter/LoginCaptchaSecurityTests.java b/pigx-auth/src/test/java/com/pig4cloud/pigx/auth/support/filter/LoginCaptchaSecurityTests.java new file mode 100644 index 000000000..b5a326b8f --- /dev/null +++ b/pigx-auth/src/test/java/com/pig4cloud/pigx/auth/support/filter/LoginCaptchaSecurityTests.java @@ -0,0 +1,139 @@ +package com.pig4cloud.pigx.auth.support.filter; + +import com.pig4cloud.pigx.auth.endpoint.ImageCodeEndpoint; +import com.pig4cloud.pigx.auth.support.core.AuthCaptchaSupport; +import com.pig4cloud.pigx.common.core.constant.CacheConstants; +import com.pig4cloud.pigx.common.core.util.SpringContextHolder; +import com.pig4cloud.pigx.common.security.captcha.CaptchaValidator; +import jakarta.servlet.FilterChain; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.support.StaticMessageSource; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +class LoginCaptchaSecurityTests { + + private final Map cache = new HashMap<>(); + private final RedisTemplate redis = mock(RedisTemplate.class); + private final ValueOperations values = mock(ValueOperations.class); + private final ValidateCodeFilter filter = new ValidateCodeFilter( + new AuthCaptchaSupport(new CaptchaValidator(null, redis))); + private final MockHttpServletRequest request = new MockHttpServletRequest("POST", "/oauth2/token"); + private final MockHttpServletResponse response = new MockHttpServletResponse(); + private final FilterChain chain = mock(FilterChain.class); + + @BeforeEach + void setup() { + when(redis.opsForValue()).thenReturn(values); + when(values.get(anyString())).thenAnswer(invocation -> cache.get(invocation.getArgument(0))); + when(values.getAndDelete(anyString())).thenAnswer(invocation -> cache.remove(invocation.getArgument(0))); + doAnswer(invocation -> { + cache.put(invocation.getArgument(0), invocation.getArgument(1)); + return null; + }).when(values).set(anyString(), any(), anyLong(), any(TimeUnit.class)); + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerSingleton("redisTemplate", redis); + StaticMessageSource messages = new StaticMessageSource(); + messages.setUseCodeAsDefaultMessage(true); + factory.registerSingleton("pigxMessageSource", messages); + new SpringContextHolder().postProcessBeanFactory(factory); + request.setServletPath("/oauth2/token"); + request.addHeader("TENANT-ID", "1"); + } + + @AfterEach + void cleanup() { + SecurityContextHolder.clearContext(); + new SpringContextHolder().postProcessBeanFactory(null); + SpringContextHolder.clearHolder(); + } + + static Stream validCases() { + return Stream.of(Arguments.of("SMS", false, false), Arguments.of("SMS", true, false), + Arguments.of("SMS", false, true), Arguments.of("SMS", true, true), + Arguments.of("APP-SMS", true, true), Arguments.of("image", false, true), + Arguments.of("image", true, true)); + } + + @ParameterizedTest + @MethodSource("validCases") + void acceptsOnlyTheExpectedCaptchaAndConsumesIt(String type, boolean post, boolean enabled) throws Exception { + configureClient(post, enabled); + String key; + if (type.equals("image")) { + request.setParameter("grant_type", "password"); + request.setParameter("randomStr", "image-test"); + new ImageCodeEndpoint().image("image-test", response); + key = CacheConstants.IMAGE_CODE_KEY + "image-test"; + } else { + request.setParameter("grant_type", "mobile"); + request.setParameter("mobile", type + "@13800138000"); + key = CacheConstants.SMS_CODE_KEY + type + "@13800138000"; + cache.put(key, "654321"); + } + request.setParameter("code", cache.get(key).toString()); + filter.doFilter(request, response, chain); + verify(chain).doFilter(request, response); + assertThat(cache).doesNotContainKey(key); + } + + static Stream invalidCases() { + Stream smsCases = Stream.of(false, true).flatMap(post -> Stream.of(false, true).flatMap(enabled -> + Stream.of("missing", "image", "legacy").map(kind -> Arguments.of(post, enabled, kind)))); + return Stream.concat(smsCases, Stream.of(Arguments.of(false, true, "password"), + Arguments.of(true, true, "password"))); + } + + @ParameterizedTest + @MethodSource("invalidCases") + void rejectsMissingOrSubstitutedIdentityProof(boolean post, boolean enabled, String kind) throws Exception { + configureClient(post, enabled); + String mobile = "SMS@13800138000"; + request.setParameter("grant_type", kind.equals("password") ? "password" : "mobile"); + request.setParameter("mobile", mobile); + if (kind.equals("image")) { + new ImageCodeEndpoint().image(mobile, response); + assertThat(cache).doesNotContainKey(CacheConstants.SMS_CODE_KEY + mobile); + request.setParameter("code", cache.get(CacheConstants.IMAGE_CODE_KEY + mobile).toString()); + } else if (kind.equals("legacy")) { + cache.put(CacheConstants.DEFAULT_CODE_KEY + mobile, "123456"); + request.setParameter("code", "123456"); + } + assertThatThrownBy(() -> filter.doFilter(request, response, chain)) + .isInstanceOf(OAuth2AuthenticationException.class); + verifyNoInteractions(chain); + } + + private void configureClient(boolean post, boolean enabled) { + RegisteredClient client = RegisteredClient.withId("test-client").clientId("test-client") + .authorizationGrantType(new AuthorizationGrantType("password")).build(); + SecurityContextHolder.getContext().setAuthentication(new OAuth2ClientAuthenticationToken(client, + post ? ClientAuthenticationMethod.CLIENT_SECRET_POST : ClientAuthenticationMethod.CLIENT_SECRET_BASIC, + "test-secret")); + // 策略只取已认证客户端,不使用请求额外指定的客户端。 + request.setParameter("client_id", "untrusted-client"); + cache.put("1:client_config_flag:test-client", "{\"captcha_flag\":\"" + (enabled ? "1" : "0") + "\"}"); + cache.put("1:client_config_flag:untrusted-client", "{\"captcha_flag\":\"0\"}"); + } +} diff --git a/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/CacheConstants.java b/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/CacheConstants.java index 25366a064..95f7915de 100644 --- a/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/CacheConstants.java +++ b/pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/CacheConstants.java @@ -20,6 +20,12 @@ public interface CacheConstants { */ String DEFAULT_CODE_KEY = "DEFAULT_CODE_KEY:"; + /** 图形验证码独立命名空间,不能写入短信验证码缓存。 */ + String IMAGE_CODE_KEY = "IMAGE_CODE_KEY:"; + + /** 短信验证码独立命名空间,后接登录类型与手机号。 */ + String SMS_CODE_KEY = "SMS_CODE_KEY:"; + /** * 菜单信息缓存 */ diff --git a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaValidator.java b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaValidator.java index 4c6a7936e..41f3537af 100644 --- a/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaValidator.java +++ b/pigx-common/pigx-common-security/src/main/java/com/pig4cloud/pigx/common/security/captcha/CaptchaValidator.java @@ -68,14 +68,31 @@ public class CaptchaValidator { return CaptchaResult.failed("验证码参数不能为空"); } - String key = CacheConstants.DEFAULT_CODE_KEY + randomStr; - Object codeObj = redisTemplate.opsForValue().get(key); + return validateCachedCode(CacheConstants.IMAGE_CODE_KEY + randomStr, code); + } + + /** + * 从短信专用缓存校验身份凭据,不读取图形或行为验证码缓存。 + * 非空参数触发原子读取并删除,无论匹配与否均消费该缓存中的验证码。 + * + * @param mobile 短信登录标识,格式为类型@手机号;调用方负责校验允许的登录类型 + * @param code 待校验的短信验证码 + * @return 匹配时返回成功;任一参数为 null、空白,或验证码不存在、过期、不匹配时返回失败; + * 参数为空时不访问缓存 + */ + public CaptchaResult validateSms(String mobile, String code) { + if (StrUtil.hasBlank(mobile, code)) { + return CaptchaResult.failed("验证码参数不能为空"); + } + return validateCachedCode(CacheConstants.SMS_CODE_KEY + mobile, code); + } + + private CaptchaResult validateCachedCode(String key, String code) { + Object codeObj = redisTemplate.opsForValue().getAndDelete(key); if (codeObj == null || StrUtil.isBlank(codeObj.toString()) || !StrUtil.equals(codeObj.toString(), code)) { - redisTemplate.delete(key); return CaptchaResult.failed("验证码错误或已过期"); } - redisTemplate.delete(key); return CaptchaResult.ok(); } diff --git a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/SysMessageServiceImpl.java b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/SysMessageServiceImpl.java index a3aea3667..d1dc1a6a6 100644 --- a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/SysMessageServiceImpl.java +++ b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/SysMessageServiceImpl.java @@ -329,7 +329,7 @@ public class SysMessageServiceImpl extends ServiceImpl impl // 校验手机号 if (StrUtil.isNotBlank(userDto.getPhone())) { - Object codeObj = RedisUtils.get(CacheConstants.DEFAULT_CODE_KEY + LoginTypeEnum.SMS.getType() + Object codeObj = RedisUtils.get(CacheConstants.SMS_CODE_KEY + LoginTypeEnum.SMS.getType() + StringPool.AT + userDto.getPhone()); if (!StrUtil.equals(userDto.getCode(), codeObj.toString())) { return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_APP_SMS_ERROR)); @@ -608,7 +608,7 @@ public class SysUserServiceImpl extends ServiceImpl impl return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_PARAM_ILLEGAL)); } String codeObj = RedisUtils - .get(CacheConstants.DEFAULT_CODE_KEY + LoginTypeEnum.SMS.getType() + StringPool.AT + userDto.getPhone()); + .get(CacheConstants.SMS_CODE_KEY + LoginTypeEnum.SMS.getType() + StringPool.AT + userDto.getPhone()); if (!StrUtil.equals(codeObj, userDto.getCode())) { return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_APP_SMS_ERROR)); } @@ -773,7 +773,7 @@ public class SysUserServiceImpl extends ServiceImpl impl } String codeObj = RedisUtils - .get(CacheConstants.DEFAULT_CODE_KEY + LoginTypeEnum.SMS.getType() + StringPool.AT + userDto.getPhone()); + .get(CacheConstants.SMS_CODE_KEY + LoginTypeEnum.SMS.getType() + StringPool.AT + userDto.getPhone()); if (!StrUtil.equals(codeObj, code)) { return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_APP_SMS_ERROR)); }