背景:若依默认使用账号密码进行登录,但是咱们客户需要增加一个短信登录功能,即在不更改原有账号密码登录的基础上,整合
短信验证码登录
。
一、自定义短信登录 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;}
大功告成!创作不容易,若对您有帮助,欢迎收藏,记得
赏个好评
。
版权归原作者 踮脚敲代码 所有, 如有侵权,请联系我们删除。