若依系统中增加短信登录

发布时间 2023-10-20 15:28:20作者: 橙香五花肉

若依系统中增加短信登录

首先需要明白AuthenticationManager和UserDetailsService的关系:
https://blog.csdn.net/feng905001561/article/details/119868411
了解过后就开始编写短信登录所需要的东西,基本上都是仿着账号密码登录的东西去写的

添加用户验证所执行的方法

@Service
public class SmsUserDetailsServiceImpl implements UserDetailsService
{
    private static final Logger log = LoggerFactory.getLogger(SmsUserDetailsServiceImpl.class);

    @Autowired
    private ISysUserService userService;

    @Autowired
    private SysPermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException
    {
        // 根据手机号查询用户的方法
        SysUser user = userService.selectUserByPhonenumber(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, permissionService.getMenuPermission(user));
    }

}

添加短信登录的鉴权过滤器

/**
 * 短信登录的鉴权过滤器,模仿 UsernamePasswordAuthenticationFilter 实现
 */
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    /**
     * 手机号码的字段name
     */
    public static final String SPRING_SECURITY_FORM_PHONE_KEY = "phonenumber";

    private String phoneParameter = SPRING_SECURITY_FORM_PHONE_KEY;

    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/smsLogin", "POST");

    /**
     * 是否仅 POST 方式
     */
    private boolean postOnly = true;

    public SmsAuthenticationFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }

    public SmsAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String phone = obtainPhone(request);

        if (phone == null) {
            phone = "";
        }

        phone = phone.trim();

        SmsAuthenticationToken authRequest = new SmsAuthenticationToken(phone);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }

    @Nullable
    protected String obtainPhone(HttpServletRequest request) {
        return request.getParameter(phoneParameter);
    }

    protected void setDetails(HttpServletRequest request, SmsAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

    public String getPhoneParameter() {
        return phoneParameter;
    }

    public void setPhoneParameter(String phoneParameter) {
        Assert.hasText(phoneParameter, "Phone parameter must not be empty or null");
        this.phoneParameter = phoneParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }
}

添加短信认证Token

/**
 * 短信登录 AuthenticationToken,模仿 UsernamePasswordAuthenticationToken 实现
 */
public class SmsAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

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

    public SmsAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        super.setAuthenticated(true);
    }

    public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> 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 {
        Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}

添加短信登录的鉴权认证器

/**
 * 短信登录的鉴权认证器,模仿 AbstractUserDetailsAuthenticationProvider 实现
 */
public class SmsAuthenticationProvider implements AuthenticationProvider {

    protected final Log logger = LogFactory.getLog(this.getClass());
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

    private SmsUserDetailsServiceImpl userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(SmsAuthenticationToken.class, authentication, () ->
                this.messages.getMessage("AbstractSmsCodeAuthenticationProvider.onlySupports", "Only SmsCodeAuthenticationToken is supported"));

        SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
        // 手机号
        String phone = (String) authenticationToken.getPrincipal();
        // 查询用户信息
        UserDetails userDetails = userDetailsService.loadUserByUsername(phone);

        SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        this.logger.debug("Authenticated user");
        return authenticationResult;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口
        return SmsAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(SmsUserDetailsServiceImpl userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}

添加短信认证配置

/**
 * 短信认证配置
 */
@Component
public class SmsAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private SmsUserDetailsServiceImpl userDetailsService;

    @Override
    public void configure(HttpSecurity http) {
        // 实例化认证器
        SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
        // 设置认证器 SmsUserDetailsServiceImpl
        smsAuthenticationProvider.setUserDetailsService(userDetailsService);
        http.authenticationProvider(smsAuthenticationProvider);
    }
}

在SecurityConfig中使用改配置,并放行你的登录方法

添加短信登录的方法

/**
 * 短信登录
 *
 * @param phonenumber 手机号
 * @param code        验证码
 * @return 结果
 */
public String smsLogin(String phonenumber, String code) {
    // 用户验证
    Authentication authentication;
    // 用户信息
    LoginUser loginUser;
    try {
        // 该方法会去调用SmsUserDetailsServiceImpl.loadUserByUsername
        authentication = authenticationManager.authenticate(new SmsAuthenticationToken(phonenumber));
        loginUser = (LoginUser) authentication.getPrincipal();
        // 验证码校验
        checkSms(phonenumber, code);
    } catch (Exception e) {
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(phonenumber, Constants.LOGIN_FAIL, e.getMessage()));
        throw new ServiceException(e.getMessage());
    }
    AsyncManager.me().execute(AsyncFactory.recordLogininfor(loginUser.getUsername(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
    // 记录登录信息
    recordLoginInfo(loginUser.getUserId());
    // 删除验证码缓存
    redisCache.deleteObject(String.format("sms_captcha_code:%s", phonenumber));
    // 生成token
    return tokenService.createToken(loginUser);
}

/**
  * 检查手机号登录
  *
  * @param phone 手机号
  * @param code 验证码
  */
private void checkSms(String phone, String code) {

  String verifyKey = String.format("sms_captcha_code:%s", phone);

  String smsCode = redisCache.getCacheObject(verifyKey);
  if (StringUtils.isEmpty(code)) {
      throw new BadCredentialsException("验证码不能为空");
  }
  if (StringUtils.isEmpty(smsCode)) {
      throw new BadCredentialsException("验证码失效");
  }
  if (!code.equals(smsCode)) {
      throw new BadCredentialsException("验证码错误");
  }
}