Spring Security - 自定义登录流程(短信登录)

October . 20 . 2020
  • 前言

接上篇, 以短信登录为例, 记录一下 Spring Secyrity 自定义登录流程的方式.

项目 GitHub 地址: sign-demo

  • 原理

简单介绍一下 Spring Secyrity 的认证流程, 这里以原生的表单登录为例如下图:

image.png

UsernamePasswordAuthenticationToken: Authentication接口的实现, 负责保存登录用户的认证信息

UsernamePasswordAuthenticationFilter: 认证认证过滤器, 配置在过滤器链上, 负责拦截登录请求, 提取请求中的参数构建为未认证的 Authentication 并传递给 AuthenticationProvider。
ProviderManager: 管理系统中的所有 AuthenticationProvider实现, 负责将对应的登录请求导向对应的AuthenticationProvider实现类。

DaoAuthenticationProvider: AuthenticationProvider实现类, 负责处理实际的认证逻辑

UserDetailService: 负责查询用户相关逻辑

  • 具体实现

按照流程图实现指定接口即可:

1. SmsCodeAuthenticationToken

package com.niu.sign.authentication.sms;

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

import java.util.Collection;

/**
 * 短信短信登录认证Token
 *
 * @version 1.0 [2020/10/15 14:45]
 * @author [nza]
 * @createTime [2020/10/15 14:45]
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = -6833557082904186230L;

    private final Object principal;

    /**
     * 未认证调用
     */
    public SmsCodeAuthenticationToken(Object mobile) {
        super(null);
        this.principal = mobile;
        this.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 principal;
    }

    @Override
    public void setAuthenticated(boolean authenticated) {
        super.setAuthenticated(authenticated);
    }
}

2. SmsAuthenticationFilter.java

SmsAuthenticationFilter.java

package com.niu.sign.authentication.sms;

import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 短信登录过滤器
 *
 * @author [nza]
 * @version 1.0 [2020/10/15 15:01]
 * @createTime [2020/10/15 15:01]
 */
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    /**
     * 请求方式
     */
    private static final String POST = "POST";

    /**
     * 手机号参数
     */
    private String mobileParameter = "mobile";

    /**
     * 仅支持 post 方法请求
     */
    private boolean postOnly = true;

    public SmsAuthenticationFilter() {
        super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
    }

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

            mobile = mobile.trim();
            // 调用未认证构造函数
            SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
            this.setDetails(request, authRequest);

            // 调用认证方法
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(this.mobileParameter);
    }

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

    public void setMobileParameter(String mobileParameter) {
        Assert.hasText(mobileParameter, "mobile parameter must not be empty or null");
    }

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

    public String getMobileParameter() {
        return mobileParameter;
    }
}

3. SmsCodeAuthenticationProvider.java

package com.niu.sign.authentication.sms;

import com.niu.sign.service.SysUserService;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
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;

/**
 * SmsCodeAuthenticationProvider
 *
 * @author [nza]
 * @version 1.0 [2020/10/15 15:08]
 * @createTime [2020/10/15 15:08]
 */
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    /**
     * 用户业务类
     */
    private final SysUserService sysUserService;

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

    /**
     * 短信登录认证逻辑
     *
     * @param authentication token
     * @return {@link org.springframework.security.core.Authentication}
     * @author nza
     * @createTime 2020/10/15 15:10
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken token = (SmsCodeAuthenticationToken) authentication;
        UserDetails userDetails = sysUserService.loadUserByMobile((String) token.getPrincipal());

        if (userDetails == null) {
            throw new InternalAuthenticationServiceException("无法获取用户信息");
        }

        // 调用已认证构造函数
        SmsCodeAuthenticationToken res = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());

        res.setDetails(token.getDetails());

        return res;
    }

    /**
     * 判断当前provider是否支持当前登录方式
     *
     * @param aClass 登陆方式
     * @return boolean
     * @author nza
     * @createTime 2020/10/15 15:09
     */
    @Override
    public boolean supports(Class aClass) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass);
    }
}

4. SysUserDetailServiceImpl.java

package com.niu.sign.service.impl;

import com.niu.sign.pojo.SysUser;
import com.niu.sign.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

/**
 * 用户详情服务
 *
 * @author [nza]
 * @version 1.0 [2020/10/10 16:25]
 * @createTime [2020/10/10 16:25]
 */
@Service
@Slf4j
public class SysUserDetailServiceImpl implements SysUserService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("username: " + username);

        // todo: 自己登录实现逻辑

        SysUser user = new SysUser();
        user.setName("admin");
        user.setPassword(passwordEncoder.encode("admin"));
        return user;
    }

    @Override
    public SysUser loadUserByMobile(String mobile) throws UsernameNotFoundException {
        log.info("mobile: " + mobile);
        SysUser user = new SysUser();
        user.setName("admin");
        user.setPassword(passwordEncoder.encode("admin"));
        return user;
    }
}
  • 配置

SmsCodeAuthenticationSecurityConfig.java

package com.niu.sign.config;

import com.niu.sign.authentication.sms.SmsAuthenticationFilter;
import com.niu.sign.authentication.sms.SmsCodeAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

/**
 * 短信认证配置类
 *
 * @author [nza]
 * @version 1.0 [2020/10/15 15:27]
 * @createTime [2020/10/15 15:27]
 */
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter {

    @Autowired
    private AuthenticationSuccessHandler customAuthenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler customAuthenticationFailureHandler;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity http) {

        // 配置filter
        SmsAuthenticationFilter filter = new SmsAuthenticationFilter();
        filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        filter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
        filter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);

        // 配置 provider
        SmsCodeAuthenticationProvider provider = new SmsCodeAuthenticationProvider(userDetailsService);

        // 添加到过滤器链上
        http.authenticationProvider(provider)
                .addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);
    }
},>

具体代码可下载源码查看