若依RuoYi整合短信验证码登录
背景:若依默認使用賬號密碼進行登錄,但是咱們客戶需要增加一個短信登錄功能,即在不更改原有賬號密碼登錄的基礎上,整合短信驗證碼登錄。
一、自定義短信登錄 token 驗證
仿照 UsernamePasswordAuthenticationToken 類,編寫短信登錄 token 驗證。
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驗證*/ public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;/*** 存儲手機號碼*/private final Object principal;/*** 構建一個沒有鑒權的構造函數*/public SmsCodeAuthenticationToken(Object principal) {super(null);this.principal = principal;setAuthenticated(false);}/*** 構建一個擁有鑒權的構造函數*/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 實現類
在用戶信息庫中查找出當前需要鑒權的用戶,如果用戶不存在,loadUserByUsername() 方法拋出異常;如果用戶名存在,將用戶信息和權限列表一起封裝到 UserDetails 對象中。
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;/*** 用戶驗證處理** @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("對不起,您的賬號:" + phoneNumber+ " 已被刪除");} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {log.info("登錄用戶:{} 已被停用.", phoneNumber);throw new ServiceException("對不起,您的賬號:" + 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));}}若你不了解創建用戶(createLoginUser)的權限封裝,查詢用戶(userService.selectUserByPhonenumber(phoneNumber))盡可能模仿賬號密碼登錄的例子,返回對應的數據;或者在創建用戶(createLoginUser)方法內加個異常捕捉;防止拋了異常沒有捕捉浪費時間精力跟蹤。
最近幾個伙伴都在這里踩雷了,因此特顯標志。
三、自定義短信登錄身份認證
在 Sping Security 中因為 UserDetailsService 只提供一個根據用戶名返回用戶信息的動作,其他的責任跟他都沒有關系,怎么將 UserDetails 組裝成 Authentication 進一步向調用者返回呢?其實這個工作是由 AuthenticationProvider 完成的,下面我們自定義一個短信登錄的身份鑒權。
-
自定義一個身份認證,實現 AuthenticationProvider 接口;
-
確定 AuthenticationProvider 僅支持短信登錄類型的 Authentication 對象驗證;
-
1、重寫 supports(Class<?> authentication) 方法,指定所定義的 AuthenticationProvider 僅支持短信身份驗證。
-
2、重寫 authenticate(Authentication authentication) 方法,實現身份驗證邏輯。
四、修改 SecurityConfig 配置類
4.1 添加 bean 注入
4.2 身份認證方法加入手機登錄鑒權
五、發送短信驗證碼
/*** 發送短信驗證碼接口* @param phoneNumber 手機號*/ @ApiOperation("發送短信驗證碼") @PostMapping("/sendSmsCode/{phoneNumber}") public AjaxResult sendSmsCode(@PathVariable("phoneNumber") String phoneNumber) {// 手機號碼phoneNumber = phoneNumber.trim();// 校驗手機號SysUser user = sysUserService.selectUserByPhonenumber(phoneNumber);if (StringUtils.isNull(user)) {throw new ServiceException("登錄用戶:" + phoneNumber+ " 不存在");}else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {throw new ServiceException("對不起,您的賬號:" + phoneNumber+ " 已被刪除");}else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {throw new ServiceException("對不起,您的賬號:" + phoneNumber+ " 已停用");}/*** 省略一千萬行代碼:校驗發送次數,一分鐘內只能發1條,一小時最多5條,一天最多10條,超出提示前端發送頻率過快。* 登錄第三方短信平臺后臺,康康是否可以設置短信發送頻率,如果有也符合業務需求可以不做處理。*/// 生成短信驗證碼String smsCode = "" + (int)((Math.random()*9+1)*1000);// 發送短信(實際按系統業務實現)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);/*** 省略一千萬行代碼:保存數據庫*/AjaxResult ajax = AjaxResult.success(); ajax.put("uuid", uuid);return ajax; }六、手機驗證碼登錄接口
/*** 短信驗證碼登錄驗證* @param dto phoneNumber 手機號* @param dto smsCode 短信驗證碼* @param dto uuid 唯一標識*/ @ApiOperation("短信驗證碼登錄驗證") @PostMapping("/smsLogin") public AjaxResult smsLogin(@RequestBody @Validated SmsLoginDto dto) {// 手機號碼String phoneNumber = dto.getPhoneNumber();// 校驗驗證碼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")));// 拋出一個驗證碼過期異常throw new CaptchaExpireException();}if(!captcha.equals(dto.getSmsCode().trim())){AsyncManager.me().execute(AsyncFactory.recordLogininfor(phoneNumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));// 拋出一個驗證碼錯誤的異常throw new CaptchaException();}redisCache.deleteObject(verifyKey);// 用戶驗證Authentication authentication = null;try {// 該方法會去調用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());}}// 執行異步任務,記錄登錄信息AsyncManager.me().execute(AsyncFactory.recordLogininfor(phoneNumber, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));// 獲取登錄人信息LoginUser loginUser = (LoginUser) authentication.getPrincipal();// 修改最近登錄IP和登錄時間recordLoginInfo(loginUser.getUserId());// 生成tokenString token = tokenService.createToken(loginUser);// 返回token給前端AjaxResult ajax = AjaxResult.success();ajax.put(Constants.TOKEN, token);return ajax; }大功告成!創作不容易,若對您有幫助,歡迎收藏,記得賞個好評。
總結
以上是生活随笔為你收集整理的若依RuoYi整合短信验证码登录的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CloudVM 之 域名绑定服务器
- 下一篇: Keil uVision5 创建工程(S