0


若依RuoYi整合短信验证码登录

背景:若依默认使用账号密码进行登录,但是咱们客户需要增加一个短信登录功能,即在不更改原有账号密码登录的基础上,整合

短信验证码登录

一、自定义短信登录 token 验证

仿照 UsernamePasswordAuthenticationToken 类,编写短信登录 token 验证。

packagecom.ruoyi.framework.security.authentication;importorg.springframework.security.authentication.AbstractAuthenticationToken;importorg.springframework.security.core.GrantedAuthority;importorg.springframework.security.core.SpringSecurityCoreVersion;importjava.util.Collection;/**
 * 自定义短信登录token验证
 */publicclassSmsCodeAuthenticationTokenextendsAbstractAuthenticationToken{privatestaticfinallong serialVersionUID =SpringSecurityCoreVersion.SERIAL_VERSION_UID;/**
     * 存储手机号码
     */privatefinalObject principal;/**
     * 构建一个没有鉴权的构造函数
     */publicSmsCodeAuthenticationToken(Object principal){super(null);this.principal = principal;setAuthenticated(false);}/**
     * 构建一个拥有鉴权的构造函数
     */publicSmsCodeAuthenticationToken(Object principal,Collection<?extendsGrantedAuthority> authorities){super(authorities);this.principal = principal;super.setAuthenticated(true);}@OverridepublicObjectgetCredentials(){returnnull;}@OverridepublicObjectgetPrincipal(){returnthis.principal;}@OverridepublicvoidsetAuthenticated(boolean isAuthenticated)throwsIllegalArgumentException{if(isAuthenticated){thrownewIllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");}super.setAuthenticated(false);}@OverridepublicvoideraseCredentials(){super.eraseCredentials();}}

二、编写 UserDetailsService 实现类

在用户信息库中查找出当前需要鉴权的用户,如果用户不存在,loadUserByUsername() 方法抛出异常;如果用户名存在,将用户信息和权限列表一起封装到 UserDetails 对象中。

packagecom.ruoyi.system.service.impl;importcom.ruoyi.common.core.domain.entity.SysUser;importcom.ruoyi.common.core.domain.model.LoginUser;importcom.ruoyi.common.enums.UserStatus;importcom.ruoyi.common.exception.ServiceException;importcom.ruoyi.common.utils.StringUtils;importcom.ruoyi.system.service.ISysUserService;importcom.ruoyi.system.service.SysPermissionService;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.core.userdetails.UsernameNotFoundException;importorg.springframework.stereotype.Service;/**
 * 用户验证处理
 *
 * @author hjs
 */@Service("userDetailsByPhonenumber")publicclassUserDetailsByPhonenumberServiceImplimplementsUserDetailsService{privatestaticfinalLogger log =LoggerFactory.getLogger(UserDetailsByPhonenumberServiceImpl.class);@AutowiredprivateISysUserService userService;@AutowiredprivateSysPermissionService permissionService;@OverridepublicUserDetailsloadUserByUsername(String phoneNumber)throwsUsernameNotFoundException{SysUser user = userService.selectUserByPhonenumber(phoneNumber);if(StringUtils.isNull(user)){
            log.info("登录用户:{} 不存在.", phoneNumber);thrownewServiceException("登录用户:"+ phoneNumber+" 不存在");}elseif(UserStatus.DELETED.getCode().equals(user.getDelFlag())){
            log.info("登录用户:{} 已被删除.", phoneNumber);thrownewServiceException("对不起,您的账号:"+ phoneNumber+" 已被删除");}elseif(UserStatus.DISABLE.getCode().equals(user.getStatus())){
            log.info("登录用户:{} 已被停用.", phoneNumber);thrownewServiceException("对不起,您的账号:"+ phoneNumber+" 已停用");}returncreateLoginUser(user);}publicUserDetailscreateLoginUser(SysUser user){returnnewLoginUser(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) 方法,实现身份验证逻辑。
packagecom.ruoyi.framework.security.authentication;importcom.ruoyi.framework.security.authentication.SmsCodeAuthenticationToken;importorg.springframework.security.authentication.AuthenticationProvider;importorg.springframework.security.core.Authentication;importorg.springframework.security.core.AuthenticationException;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsService;/**
 * 自定义短信登录身份认证
 */publicclassSmsCodeAuthenticationProviderimplementsAuthenticationProvider{privateUserDetailsService userDetailsService;publicSmsCodeAuthenticationProvider(UserDetailsService userDetailsService){setUserDetailsService(userDetailsService);}/**
     * 重写 authenticate方法,实现身份验证逻辑。
     */@OverridepublicAuthenticationauthenticate(Authentication authentication)throwsAuthenticationException{SmsCodeAuthenticationToken authenticationToken =(SmsCodeAuthenticationToken) authentication;String telephone =(String) authenticationToken.getPrincipal();// 委托 UserDetailsService 查找系统用户UserDetails userDetails = userDetailsService.loadUserByUsername(telephone);// 鉴权成功,返回一个拥有鉴权的 AbstractAuthenticationTokenSmsCodeAuthenticationToken authenticationResult =newSmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());return authenticationResult;}/**
     * 重写supports方法,指定此 AuthenticationProvider 仅支持短信验证码身份验证。
     */@Overridepublicbooleansupports(Class<?> authentication){returnSmsCodeAuthenticationToken.class.isAssignableFrom(authentication);}publicUserDetailsServicegetUserDetailsService(){return userDetailsService;}publicvoidsetUserDetailsService(UserDetailsService userDetailsService){this.userDetailsService = userDetailsService;}}

四、修改 SecurityConfig 配置类

4.1 添加 bean 注入

在这里插入图片描述

4.2 身份认证方法加入手机登录鉴权

在这里插入图片描述

五、发送短信验证码

/**
 * 发送短信验证码接口
 * @param phoneNumber 手机号
 */@ApiOperation("发送短信验证码")@PostMapping("/sendSmsCode/{phoneNumber}")publicAjaxResultsendSmsCode(@PathVariable("phoneNumber")String phoneNumber){// 手机号码
    phoneNumber = phoneNumber.trim();// 校验手机号SysUser user = sysUserService.selectUserByPhonenumber(phoneNumber);if(StringUtils.isNull(user)){thrownewServiceException("登录用户:"+ phoneNumber+" 不存在");}elseif(UserStatus.DELETED.getCode().equals(user.getDelFlag())){thrownewServiceException("对不起,您的账号:"+ phoneNumber+" 已被删除");}elseif(UserStatus.DISABLE.getCode().equals(user.getStatus())){thrownewServiceException("对不起,您的账号:"+ phoneNumber+" 已停用");}/**
     * 省略一千万行代码:校验发送次数,一分钟内只能发1条,一小时最多5条,一天最多10条,超出提示前端发送频率过快。
     * 登录第三方短信平台后台,康康是否可以设置短信发送频率,如果有也符合业务需求可以不做处理。
     */// 生成短信验证码String smsCode =""+(int)((Math.random()*9+1)*1000);// 发送短信(实际按系统业务实现)SmsEntity entity =newSmsEntity(phoneNumber, smsCode);SendMessage.sendSms(entity);if(entity==null||!SmsResponseCodeEnum.SUCCESS.getCode().equals(entity.getResponseCode())){thrownewServiceException(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")publicAjaxResultsmsLogin(@RequestBody@ValidatedSmsLoginDto 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")));// 抛出一个验证码过期异常thrownewCaptchaExpireException();}if(!captcha.equals(dto.getSmsCode().trim())){AsyncManager.me().execute(AsyncFactory.recordLogininfor(phoneNumber,Constants.LOGIN_FAIL,MessageUtils.message("user.jcaptcha.error")));// 抛出一个验证码错误的异常thrownewCaptchaException();}
    redisCache.deleteObject(verifyKey);// 用户验证Authentication authentication =null;try{// 该方法会去调用UserDetailsByPhonenumberServiceImpl.loadUserByUsername
        authentication = authenticationManager.authenticate(newSmsCodeAuthenticationToken(phoneNumber));}catch(Exception e){if(e instanceofBadCredentialsException){AsyncManager.me().execute(AsyncFactory.recordLogininfor(phoneNumber,Constants.LOGIN_FAIL,MessageUtils.message("account.not.incorrect")));thrownewUserPasswordNotMatchException();}else{AsyncManager.me().execute(AsyncFactory.recordLogininfor(phoneNumber,Constants.LOGIN_FAIL, e.getMessage()));thrownewServiceException(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;}
大功告成!创作不容易,若对您有帮助,欢迎收藏,记得
赏个好评


本文转载自: https://blog.csdn.net/ii950606/article/details/128485787
版权归原作者 踮脚敲代码 所有, 如有侵权,请联系我们删除。

“若依RuoYi整合短信验证码登录”的评论:

还没有评论