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

您所在的位置:网站首页 微信登录怎么发短信验证 若依RuoYi整合短信验证码登录

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

2024-07-12 17:36| 来源: 网络整理| 查看: 265

背景:若依默认使用账号密码进行登录,但是咱们客户需要增加一个短信登录功能,即在不更改原有账号密码登录的基础上,整合短信验证码登录。 本案例基于RuoYi-Vue版本实现,其他版本应该大同小异。

一、自定义短信登录 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 authorities) { super(authorities); this.principal = principal; super.setAuthenticated(true); } @Override public Object getCredentials() { return null; } @Override public Object getPrincipal() { return this.principal; } @Override public 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); } @Override public 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); @Autowired private ISysUserService userService; @Autowired private SysPermissionService permissionService; @Override public UserDetails loadUserByUsername(String phoneNumber) throws UsernameNotFoundException { // 防止漏查角色等所需的信息,尽量模仿根据用户名查用户的方法开发:ISysUserService.selectUserByUserName(username) 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)); } }

您们后台的框架很有可能被您的上师或者以前的老员工优化处理过,为了防止漏查角色等权限所需的信息,因此您在查询用户(userService.selectUserByPhonenumber(phoneNumber))时,尽可能模仿账号密码登录的代码(ISysUserService.selectUserByUserName(username)),包含SQL,返回对应的数据;测试阶段最好在创建用户(createLoginUser)方法内加个异常捕捉;防止抛了异常却没有捕捉,导致浪费大量时间精力排查。

评论区很多小伙伴都在这里踩雷了,因此特显标志。

三、自定义短信登录身份认证

在 Sping Security 中因为 UserDetailsService 只提供一个根据用户名返回用户信息的动作,其他的责任跟他都没有关系,怎么将 UserDetails 组装成 Authentication 进一步向调用者返回呢?其实这个工作是由 AuthenticationProvider 完成的,下面我们自定义一个短信登录的身份鉴权。

自定义一个身份认证,实现 AuthenticationProvider 接口;

确定 AuthenticationProvider 仅支持短信登录类型的 Authentication 对象验证;

1、重写 supports(Class authentication) 方法,指定所定义的 AuthenticationProvider 仅支持短信身份验证。

2、重写 authenticate(Authentication authentication) 方法,实现身份验证逻辑。

package com.ruoyi.framework.security.authentication; import com.ruoyi.framework.security.authentication.SmsCodeAuthenticationToken; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; /** * 自定义短信登录身份认证 */ public class SmsCodeAuthenticationProvider implements AuthenticationProvider { private UserDetailsService userDetailsService; public SmsCodeAuthenticationProvider(UserDetailsService userDetailsService){ setUserDetailsService(userDetailsService); } /** * 重写 authenticate方法,实现身份验证逻辑。 */ @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication; String telephone = (String) authenticationToken.getPrincipal(); // 委托 UserDetailsService 查找系统用户 UserDetails userDetails = userDetailsService.loadUserByUsername(telephone); // 鉴权成功,返回一个拥有鉴权的 AbstractAuthenticationToken SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities()); authenticationResult.setDetails(authenticationToken.getDetails()); return authenticationResult; } /** * 重写supports方法,指定此 AuthenticationProvider 仅支持短信验证码身份验证。 */ @Override public boolean supports(Class authentication) { return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication); } public UserDetailsService getUserDetailsService() { return userDetailsService; } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } } 四、修改 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.loadUserByUsername authentication = 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()); // 生成token String token = tokenService.createToken(loginUser); // 返回token给前端 AjaxResult ajax = AjaxResult.success(); ajax.put(Constants.TOKEN, token); return ajax; }

大功告成!创作不容易,若对您有帮助,欢迎收藏 。 点赞 点赞 点赞 一定要点赞 👍👍🏻👍🏼👍🏽👍🏾👍🏿



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3