若依RuoYi整合短信验证码登录
背景:若依默認(rèn)使用賬號(hào)密碼進(jìn)行登錄,但是咱們客戶需要增加一個(gè)短信登錄功能,即在不更改原有賬號(hào)密碼登錄的基礎(chǔ)上,整合短信驗(yàn)證碼登錄。
一、自定義短信登錄 token 驗(yàn)證
仿照 UsernamePasswordAuthenticationToken 類,編寫短信登錄 token 驗(yàn)證。
package com.ruoyi.framework.security.authentication;import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.SpringSecurityCoreVersion;import java.util.Collection;/*** 自定義短信登錄token驗(yàn)證*/ public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;/*** 存儲(chǔ)手機(jī)號(hào)碼*/private final Object principal;/*** 構(gòu)建一個(gè)沒有鑒權(quán)的構(gòu)造函數(shù)*/public SmsCodeAuthenticationToken(Object principal) {super(null);this.principal = principal;setAuthenticated(false);}/*** 構(gòu)建一個(gè)擁有鑒權(quán)的構(gòu)造函數(shù)*/public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;super.setAuthenticated(true);}@Overridepublic Object getCredentials() {return null;}@Overridepublic Object getPrincipal() {return this.principal;}@Overridepublic void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {if (isAuthenticated) {throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");}super.setAuthenticated(false);}@Overridepublic void eraseCredentials() {super.eraseCredentials();}}二、編寫 UserDetailsService 實(shí)現(xiàn)類
在用戶信息庫中查找出當(dāng)前需要鑒權(quán)的用戶,如果用戶不存在,loadUserByUsername() 方法拋出異常;如果用戶名存在,將用戶信息和權(quán)限列表一起封裝到 UserDetails 對(duì)象中。
package com.ruoyi.system.service.impl;import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.core.domain.model.LoginUser; import com.ruoyi.common.enums.UserStatus; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.system.service.ISysUserService; import com.ruoyi.system.service.SysPermissionService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service;/*** 用戶驗(yàn)證處理** @author hjs*/ @Service("userDetailsByPhonenumber") public class UserDetailsByPhonenumberServiceImpl implements UserDetailsService {private static final Logger log = LoggerFactory.getLogger(UserDetailsByPhonenumberServiceImpl.class);@Autowiredprivate ISysUserService userService;@Autowiredprivate SysPermissionService permissionService;@Overridepublic UserDetails loadUserByUsername(String phoneNumber) throws UsernameNotFoundException {SysUser user = userService.selectUserByPhonenumber(phoneNumber);if (StringUtils.isNull(user)) {log.info("登錄用戶:{} 不存在.", phoneNumber);throw new ServiceException("登錄用戶:" + phoneNumber+ " 不存在");} else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {log.info("登錄用戶:{} 已被刪除.", phoneNumber);throw new ServiceException("對(duì)不起,您的賬號(hào):" + phoneNumber+ " 已被刪除");} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {log.info("登錄用戶:{} 已被停用.", phoneNumber);throw new ServiceException("對(duì)不起,您的賬號(hào):" + phoneNumber+ " 已停用");}return createLoginUser(user);}public UserDetails createLoginUser(SysUser user) {return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));// return new LoginUser(user, permissionService.getMenuPermission(user));}}若你不了解創(chuàng)建用戶(createLoginUser)的權(quán)限封裝,查詢用戶(userService.selectUserByPhonenumber(phoneNumber))盡可能模仿賬號(hào)密碼登錄的例子,返回對(duì)應(yīng)的數(shù)據(jù);或者在創(chuàng)建用戶(createLoginUser)方法內(nèi)加個(gè)異常捕捉;防止拋了異常沒有捕捉浪費(fèi)時(shí)間精力跟蹤。
最近幾個(gè)伙伴都在這里踩雷了,因此特顯標(biāo)志。
三、自定義短信登錄身份認(rèn)證
在 Sping Security 中因?yàn)?UserDetailsService 只提供一個(gè)根據(jù)用戶名返回用戶信息的動(dòng)作,其他的責(zé)任跟他都沒有關(guān)系,怎么將 UserDetails 組裝成 Authentication 進(jìn)一步向調(diào)用者返回呢?其實(shí)這個(gè)工作是由 AuthenticationProvider 完成的,下面我們自定義一個(gè)短信登錄的身份鑒權(quán)。
-
自定義一個(gè)身份認(rèn)證,實(shí)現(xiàn) AuthenticationProvider 接口;
-
確定 AuthenticationProvider 僅支持短信登錄類型的 Authentication 對(duì)象驗(yàn)證;
-
1、重寫 supports(Class<?> authentication) 方法,指定所定義的 AuthenticationProvider 僅支持短信身份驗(yàn)證。
-
2、重寫 authenticate(Authentication authentication) 方法,實(shí)現(xiàn)身份驗(yàn)證邏輯。
四、修改 SecurityConfig 配置類
4.1 添加 bean 注入
4.2 身份認(rèn)證方法加入手機(jī)登錄鑒權(quán)
五、發(fā)送短信驗(yàn)證碼
/*** 發(fā)送短信驗(yàn)證碼接口* @param phoneNumber 手機(jī)號(hào)*/ @ApiOperation("發(fā)送短信驗(yàn)證碼") @PostMapping("/sendSmsCode/{phoneNumber}") public AjaxResult sendSmsCode(@PathVariable("phoneNumber") String phoneNumber) {// 手機(jī)號(hào)碼phoneNumber = phoneNumber.trim();// 校驗(yàn)手機(jī)號(hào)SysUser user = sysUserService.selectUserByPhonenumber(phoneNumber);if (StringUtils.isNull(user)) {throw new ServiceException("登錄用戶:" + phoneNumber+ " 不存在");}else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {throw new ServiceException("對(duì)不起,您的賬號(hào):" + phoneNumber+ " 已被刪除");}else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {throw new ServiceException("對(duì)不起,您的賬號(hào):" + phoneNumber+ " 已停用");}/*** 省略一千萬行代碼:校驗(yàn)發(fā)送次數(shù),一分鐘內(nèi)只能發(fā)1條,一小時(shí)最多5條,一天最多10條,超出提示前端發(fā)送頻率過快。* 登錄第三方短信平臺(tái)后臺(tái),康康是否可以設(shè)置短信發(fā)送頻率,如果有也符合業(yè)務(wù)需求可以不做處理。*/// 生成短信驗(yàn)證碼String smsCode = "" + (int)((Math.random()*9+1)*1000);// 發(fā)送短信(實(shí)際按系統(tǒng)業(yè)務(wù)實(shí)現(xiàn))SmsEntity entity = new SmsEntity(phoneNumber, smsCode);SendMessage.sendSms(entity);if(entity==null || !SmsResponseCodeEnum.SUCCESS.getCode().equals(entity.getResponseCode())){throw new ServiceException(entity.getResponseDesc());}// 保存redis緩存String uuid = IdUtils.simpleUUID();String verifyKey = SysConst.REDIS_KEY_SMSLOGIN_SMSCODE + uuid;redisCache.setCacheObject(verifyKey, smsCode, SysConst.REDIS_EXPIRATION_SMSLOGIN_SMSCODE, TimeUnit.MINUTES);/*** 省略一千萬行代碼:保存數(shù)據(jù)庫*/AjaxResult ajax = AjaxResult.success(); ajax.put("uuid", uuid);return ajax; }六、手機(jī)驗(yàn)證碼登錄接口
/*** 短信驗(yàn)證碼登錄驗(yàn)證* @param dto phoneNumber 手機(jī)號(hào)* @param dto smsCode 短信驗(yàn)證碼* @param dto uuid 唯一標(biāo)識(shí)*/ @ApiOperation("短信驗(yàn)證碼登錄驗(yàn)證") @PostMapping("/smsLogin") public AjaxResult smsLogin(@RequestBody @Validated SmsLoginDto dto) {// 手機(jī)號(hào)碼String phoneNumber = dto.getPhoneNumber();// 校驗(yàn)驗(yàn)證碼String verifyKey = SysConst.REDIS_KEY_SMSLOGIN_SMSCODE + dto.getUuid();String captcha = redisCache.getCacheObject(verifyKey);if(captcha == null) {AsyncManager.me().execute(AsyncFactory.recordLogininfor(phoneNumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));// 拋出一個(gè)驗(yàn)證碼過期異常throw new CaptchaExpireException();}if(!captcha.equals(dto.getSmsCode().trim())){AsyncManager.me().execute(AsyncFactory.recordLogininfor(phoneNumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));// 拋出一個(gè)驗(yàn)證碼錯(cuò)誤的異常throw new CaptchaException();}redisCache.deleteObject(verifyKey);// 用戶驗(yàn)證Authentication authentication = null;try {// 該方法會(huì)去調(diào)用UserDetailsByPhonenumberServiceImpl.loadUserByUsernameauthentication = authenticationManager.authenticate(new SmsCodeAuthenticationToken(phoneNumber));} catch (Exception e) {if (e instanceof BadCredentialsException) {AsyncManager.me().execute(AsyncFactory.recordLogininfor(phoneNumber, Constants.LOGIN_FAIL, MessageUtils.message("account.not.incorrect")));throw new UserPasswordNotMatchException();} else {AsyncManager.me().execute(AsyncFactory.recordLogininfor(phoneNumber, Constants.LOGIN_FAIL, e.getMessage()));throw new ServiceException(e.getMessage());}}// 執(zhí)行異步任務(wù),記錄登錄信息AsyncManager.me().execute(AsyncFactory.recordLogininfor(phoneNumber, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));// 獲取登錄人信息LoginUser loginUser = (LoginUser) authentication.getPrincipal();// 修改最近登錄IP和登錄時(shí)間recordLoginInfo(loginUser.getUserId());// 生成tokenString token = tokenService.createToken(loginUser);// 返回token給前端AjaxResult ajax = AjaxResult.success();ajax.put(Constants.TOKEN, token);return ajax; }大功告成!創(chuàng)作不容易,若對(duì)您有幫助,歡迎收藏,記得賞個(gè)好評(píng)。
總結(jié)
以上是生活随笔為你收集整理的若依RuoYi整合短信验证码登录的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CloudVM 之 域名绑定服务器
- 下一篇: Keil uVision5 创建工程(S