RUOYI集成手机短信登录

背景:

工作过程中遇到需求,需要将短信验证码登录集成到RUOYI框架中。框架中使用的用户认证组件为Security,由于没有怎么研究过这个组件,这个功能不太会搞。所以这是一篇抄作业记录。参考文章如下

若依RuoYi整合短信验证码登录_若依短信登录-CSDN博客

任务需求描述请参考下面这篇文章

手机短信验证码登录-CSDN博客

需求分析:

根据需求描述,将需求分为以下几个工作任务:

1、需要有一个生成随机6位数验证码的方法

2、需要对接第三方平台将验证码发出去。(我这里对接的是阿里云)

3、对前端提供一个短信发送接口和手机号短信验证码登录接口

遇到的问题:

框架中只有提供用户名密码的登录认证方法,未提供手机号和验证码的认证方法。为前端提供的手机号短信验证码登录接口中无法使用用户名密码的这种认证方式。因此需要修改框架中的文件实现手机号和验证码的这种身份认证方法。

认证方法实现过程

想要实现新的身份认证方法就必须要搞明白Security这个组件的工作原理。参考如下文章

SpringBoot集成Spring Security(7)——认证流程_springboot整合springsecurity若依-CSDN博客

要实现整个过程必须要实现以下三个类

1、SmsCodeAuthenticationProvider 该类实现了AuthenticationProvider接口,用于实现自定义的认证逻辑

package com.ruoyi.framework.security.provider;

import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.framework.security.token.SmsCodeAuthenticationToken;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
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() {
    }

    public SmsCodeAuthenticationProvider(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
        String mobile = (String) authenticationToken.getPrincipal();
        String code = (String) authenticationToken.getCode();
//        checkSmsCode(mobile, code);
        UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
        // 此时鉴权成功后,应当重新 new 一个拥有鉴权的 authenticationResult 返回
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities(), code);
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    private void checkSmsCode(String mobile, String code) {
        RedisCache redisCache = SpringUtils.getBean(RedisCache.class);
        String verifyKey = CacheConstants.SMS_CAPTCHA_CODE_KEY + mobile;
        String smsCode = redisCache.getCacheObject(verifyKey);
        if (smsCode == null) {
            throw new BadCredentialsException("验证码失效");
        }
        if (!code.equals(smsCode)) {
            throw new BadCredentialsException("验证码错误");
        }
    }


    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }

}

2、SmsCodeAuthenticationToken 该类继承了AbstractAuthenticationToken类,用来传递认证信息

package com.ruoyi.framework.security.token;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;

import java.util.Collection;

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {



    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    /**
     * 在 UsernamePasswordAuthenticationToken 中该字段代表登录的用户名,
     * 在这里就代表登录的手机号码
     */
    private final Object principal;
    private final Object code;

    /**
     * 构建一个没有鉴权的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal,Object code) {
        super(null);
        this.principal = principal;
        this.code = code;
        setAuthenticated(false);
    }

    /**
     * 构建拥有鉴权的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities, Object code) {
        super(authorities);
        this.principal = principal;
        this.code = code;
        // must use super, as we override
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    public Object getCode() {
        return code;
    }

    @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();
    }
}

3、SmsUserDetailsServiceImpl 该类实现了UserDetailsService接口,在SmsCodeAuthenticationProvider 类的的认证过程中通过UserDetailsService接口回调到这个实现类,用来查询并创建登录用户信息。注意这个service必须有个名字,此处是“userDetailsByPhonenumber”

package com.ruoyi.framework.web.service;

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.base.BaseException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysUserService;
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;

@Service("userDetailsByPhonenumber")
public class SmsUserDetailsServiceImpl implements UserDetailsService {

    private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    @Autowired
    private ISysUserService userService;

    @Autowired
    private SysPermissionService permissionService;


    @Override
    public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {
        SysUser user = userService.selectUserByUserName(phone);
        if (StringUtils.isNull(user))
        {
            log.info("登录手机号:{} 不存在.", phone);
            throw new UsernameNotFoundException("登录手机号:" + phone + " 不存在");
        }
        else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
        {
            log.info("登录用户:{} 已被删除.", phone);
            throw new BaseException("对不起,您的账号:" + phone + " 已被删除");
        }
        else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
        {
            log.info("登录用户:{} 已被停用.", phone);
            throw new BaseException("对不起,您的账号:" + phone + " 已停用");
        }

        return createLoginUser(user);
    }

    public UserDetails createLoginUser(SysUser user)
    {
        return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
    }
}

还要对以下几个文件进行修改

1、SecurityConfig 位于ruoyi-framework模块下的config文件夹中,是Security组件的配置类

①注入认证的服务,@Qualifier中指定了服务接口的实现类

②配置身份认证接口

2、UserDetailsServiceImpl 该类是若依框架中账号密码认证的实现类,需要给这个service起一个名字,用来区分手机号验证码的实现类,并且@Primary这个注解是必须要加,不然系统无法启动

手机号短信验证码登录接口的实现

在SysLoginService中实现手机号验证码登录验证方法

/**
     * 手机号登录验证*
     * @param mobile
     * @return
     */
    public String smsLogin(String mobile,String code){
        // 用户验证
        Authentication authentication = null;
        try
        {
            SmsCodeAuthenticationToken authenticationToken = new SmsCodeAuthenticationToken(mobile,code);
            AuthenticationContextHolder.setContext(authenticationToken);
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager.authenticate(authenticationToken);
        }
        catch (Exception e)
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(mobile, Constants.LOGIN_FAIL, e.getMessage()));
            throw new CustomException(e.getMessage());
        }
        finally
        {
            AuthenticationContextHolder.clearContext();
        }
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(mobile, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        recordLoginInfo(loginUser.getUserId());
        if (!soloLogin)
        {
            // 如果用户不允许多终端同时登录,清除缓存信息
            String userIdKey = Constants.LOGIN_USERID_KEY + loginUser.getUser().getUserId();
            String userKey = redisCache.getCacheObject(userIdKey);
            if (StringUtils.isNotEmpty(userKey))
            {
                redisCache.deleteObject(userIdKey);
                redisCache.deleteObject(userKey);
            }
        }
        // 生成token
        return tokenService.createToken(loginUser);
    }

总结:

通过实现不同的provider可以实现不同的登录验证方式,例如邮箱登录验证和扫码登录验证等。不同的provider需要实现对应的token类,将认证信息传递到security组件中,provider的实现类中实现对认证信息的校验。UserDetailsService接口的实现类,用来查询用户在系统中的信息,一些数据状态的验证可以在此接口的实现类中实现验证逻辑。

  • 8
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值