我们的系统集成了短信通知服务。在这里,我们扩展OAuth2,使系统支持短信验证码登录。OAuth2的登录方式如下:
- 授权码模式(authorization code)
- 隐式授权模式(implicit grant)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
- 扩展模式(grant types extensions)
/** * 短信验证码模式 */public class SmsCaptchaTokenGranter extends AbstractTokenGranter { private static final String GRANT_TYPE = "sms_captcha"; private final AuthenticationManager authenticationManager; private UserDetailsService userDetailsService; private IUserFeign userFeign; private ISmsFeign smsFeign; private RedisTemplate redisTemplate; private CaptchaService captchaService; private String captchaType; public SmsCaptchaTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, RedisTemplate redisTemplate, IUserFeign userFeign, ISmsFeign smsFeign, CaptchaService captchaService, UserDetailsService userDetailsService, String captchaType) { this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE); this.redisTemplate = redisTemplate; this.captchaService = captchaService; this.captchaType = captchaType; this.smsFeign = smsFeign; this.userFeign = userFeign; this.userDetailsService = userDetailsService; } protected SmsCaptchaTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) { super(tokenServices, clientDetailsService, requestFactory, grantType); this.authenticationManager = authenticationManager; } @Override protected OAuth2Authentication getoAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) { Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters()); // 获取验证码类型 String captchaType = parameters.get(CaptchaConstant.CAPTCHA_TYPE); // 判断输入的验证码类型是否与系统配置一致 if (!StringUtils.isEmpty(captchaType) && !captchaType.equals(this.captchaType)) { throw new UserDeniedAuthorizationException(ResultCodeEnum.INVALID_CAPTCHA_TYPE.getMsg()); } if (CaptchaConstant.IMAGE_CAPTCHA.equalsIgnoreCase(captchaType)) { // 图片验证码验证 String captchaKey = parameters.get(CaptchaConstant.CAPTCHA_KEY); String captchaCode = parameters.get(CaptchaConstant.CAPTCHA_CODE); // 获取验证码 String redisCode = (String)redisTemplate.opsForValue().get(CaptchaConstant.IMAGE_CAPTCHA_KEY + captchaKey); // 判断验证码 if (captchaCode == null || !captchaCode.equalsIgnoreCase(redisCode)) { throw new UserDeniedAuthorizationException(ResultCodeEnum.INVALID_CAPTCHA.getMsg()); } } else { // 滑动验证码验证 String captchaVerification = parameters.get(CaptchaConstant.CAPTCHA_VERIFICATION); CaptchaVO captchaVO = new CaptchaVO(); captchaVO.setCaptchaVerification(captchaVerification); ResponseModel responseModel = captchaService.verification(captchaVO); if (null == responseModel || !RepCodeEnum.SUCCESS.getCode().equals(responseModel.getRepCode())) { throw new UserDeniedAuthorizationException(ResultCodeEnum.INVALID_CAPTCHA.getMsg()); } } String phoneNumber = parameters.get(TokenConstant.PHONE_NUMBER); String smsCode = parameters.get(TokenConstant.SMS_CODE); String code = parameters.get(TokenConstant.CODE); // Protect from downstream leaks of password parameters.remove(TokenConstant.CODE); Result<Boolean> checkResult = smsFeign.checkSmsVerificationCode(smsCode, phoneNumber, code); if (null == checkResult || !checkResult.getData()) { throw new InvalidGrantException(("Could not authenticate user: " + phoneNumber)); } UserDetails userDetails = this.userDetailsService.loadUserByUsername(phoneNumber); Authentication userAuth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); ((AbstractAuthenticationToken)userAuth).setDetails(parameters); OAuth2Request storedoauth2Request = getRequestFactory().createoAuth2Request(client, tokenRequest); return new OAuth2Authentication(storedoauth2Request, userAuth); }}
2、自定义Gitegtokengranter,支持多种token模式/** * token自定义 */public class GitEggTokenGranter { /** * token自定义Granter */ public static TokenGranter getTokenGranter(final AuthenticationManager authenticationManager, final AuthorizationServerEndpointsConfigurer endpoints, RedisTemplate redisTemplate, IUserFeign userFeign, ISmsFeign smsFeign, CaptchaService captchaService, UserDetailsService userDetailsService, String captchaType) { // 默许tokengranter集合 List<TokenGranter> granters = new ArrayList<>(Collections.singletonList(endpoints.getTokenGranter())); // 增加验证码模式 granters.add(new CaptchaTokenGranter(authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getoAuth2RequestFactory(), redisTemplate, captchaService, captchaType)); // 增加短信验证码模式 granters.add(new SmsCaptchaTokenGranter(authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getoAuth2RequestFactory(), redisTemplate, userFeign, smsFeign, captchaService, userDetailsService, captchaType)); // tokengranter集合 return new CompositeTokenGranter(granters); }}
3、GitegoAuthcontroller添加获取短信验证码的方法 @ApiOperation("发送短信验证码") @PostMapping("/sms/captcha/send") public Result sendSmsCaptcha(@RequestBody SmsVerificationDTO smsVerificationDTO) { Result<Object> sendResult = smsFeign.sendSmsVerificationCode(smsVerificationDTO.getSmsCode(), smsVerificationDTO.getPhoneNumber()); return sendResult; }
4、前端页面增加短信验证码登录 <a-tab-pane key="phone_account" :tab="$t('user.login.tab-login-mobile')" class="color:#1890ff;"> <a-form-item> <a-input size="large" type="text" :placeholder="$t('user.login.mobile.placeholder')" v-decorator="['phoneNumber', {rules: [{ required: true, pattern: /^1[34578]\d{9}$/, message: $t('user.phone-number.required') }], validateTrigger: 'change'}]"> <a-icon slot="prefix" type="mobile" :style="{ color: '#1890ff' }" /> </a-input> </a-form-item> <a-row :gutter="16"> <a-col class="gutter-row" :span="16"> <a-form-item> <a-input size="large" type="text" :placeholder="$t('user.login.mobile.verification-code.placeholder')" v-decorator="['captcha', {rules: [{ required: true, message: $t('user.verification-code.required') }], validateTrigger: 'blur'}]"> <a-icon slot="prefix" type="mail" :style="{ color: '#1890ff' }" /> </a-input> </a-form-item> </a-col> <a-col class="gutter-row" :span="8"> <a-button class="getCaptcha" tabindex="-1" :disabled="state.smsSendBtn" @click.stop.prevent="getCaptcha" v-text="!state.smsSendBtn && $t('user.register.get-verification-code') || (state.time+' s')"></a-button> </a-col> </a-row> </a-tab-pane>
getCaptcha (e) { e.preventDefault() const { form: { validateFields }, state } = this validateFields(['phoneNumber'], { force: true }, (err, values) => { if (!err) { state.smsSendBtn = true const interval = window.setInterval(() => { if (state.time-- <= 0) { state.time = 60 state.smsSendBtn = false window.clearInterval(interval) } }, 1000) const hide = this.$message.loading(在发送验证码时..', 0) getSmsCaptcha({ phoneNumber: values.phoneNumber, smsCode: 'aliLoginCode' }).then(res => { setTimeout(hide, 2500) this.$notification['success']({ message: '提示', description: 获得验证码成功,您的验证码为:' + res.result.captcha, duration: 8 }) }).catch(err => { setTimeout(hide, 1) clearInterval(interval) state.time = 60 state.smsSendBtn = false this.requestFailed(err) }) } }) }, stepCaptchaSuccess () { this.loginSuccess() }, stepCaptchaCancel () { this.Logout().then(() => { this.loginBtn = false this.stepCaptchaVisible = false }) },
5、通过短信验证码登录界面 短信登录的主要优势如下:方便快捷:用户无需记忆密码,只需输入手机号码即可接收验证码登录系统。
高安全性:短信验证可以保证用户的身份认证,减少恶意攻击和非法访问。
不用担心忘记密码:因为没有密码可以忘记。
减少垃圾注册:短信验证可有效减少无意义注册。
促销:由于短信验证的使用,用户可以在微信、微博等社交平台上分享更广泛的应用。
确保隐私安全:短信验证不需要用户输入任何隐私信息,保护用户个人隐私安全。