Spring boot整合Spring Security實現驗證碼登陸


Spring boot整合Spring Security實現驗證碼登陸

 

驗證碼登陸在日常使用軟件中是很常見的,甚至可以說超過了密碼登陸。

如何通過Spring Security框架實現驗證碼登陸,並且登陸成功之后也同樣返回和密碼登陸類似的token?

  • 先看一張Spring Security攔截請求的流程圖

 可以發現Spring Security默認有用戶名密碼登陸攔截器,查看 UsernamePasswordAuthenticationFilter 實現了 AbstractAuthenticationProcessingFilter類 。根據UsernamePasswordAuthenticationFilter的設計模式可以在Spring security的基礎上拓展自己的攔截器,實現相應的功能。

  • 新增一個 SmsCodeAuthenticationToken 實體類,需要繼承IrhAuthenticationToken,而IrhAuthenticationToken繼承了 AbstractAuthenticationToken 用來封裝驗證碼登陸時需要的信息,並且自定義一個AuthenticationToken的父類的作用就是用來在管理在具有多種登陸方式的系統中,能記錄最核心的兩個信息,一個是用戶身份,一個是登陸口令;並且在后期能直接向上轉型而不報錯。
復制代碼
package top.imuster.auth.config;

import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

/**
 * @ClassName: SmsCodeAuthenticationToken
 * @Description: 驗證碼登錄驗證信息封裝類
 * @author: hmr
 * @date: 2020/4/30 13:59
 */
public class SmsCodeAuthenticationToken extends IrhAuthenticationToken {
    public SmsCodeAuthenticationToken(Object principal, Object credentials) {
        super(principal, credentials);
    }

    public SmsCodeAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(principal, credentials, authorities);
    }
}
復制代碼
復制代碼
package top.imuster.auth.config;

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

import java.util.Collection;

/**
 * @Author hmr
 * @Description 自定義AbstractAuthenticationToken
 * @Date: 2020/5/1 13:51
 * @param irh平台的認證實體類   可以通過繼承該類來實現不同的登錄邏輯
 * @reture:
 **/
public class IrhAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = 110L;

    //用戶信息
    protected final Object principal;

    //密碼或者郵箱驗證碼
    protected Object credentials;

    /**
     * This constructor can be safely used by any code that wishes to create a
     * <code>UsernamePasswordAuthenticationToken</code>, as the {@link
     * #isAuthenticated()} will return <code>false</code>.
     *
     */
    public IrhAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        this.setAuthenticated(false);
    }

    /**
     * This constructor should only be used by <code>AuthenticationManager</code> or <code>AuthenticationProvider</code>
     * implementations that are satisfied with producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
     * token token.
     *
     * @param principal
     * @param credentials
     * @param authorities
     */
    public IrhAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }


    @Override
    public Object getCredentials() {
        return this.credentials;
    }

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


    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");
        } else {
            super.setAuthenticated(false);
        }
    }

    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}
復制代碼

  • 繼承AbstractAuthenticationProcessingFilter之后就可以將自定義的Filter假如到過濾器鏈中。所以自定義一個 SmsAuthenticationFilter 類並且繼承 AbstractAuthenticationProcessingFilter 類,並且該Filter中只校驗輸入參數是否正確,如果不完整,則拋出 AuthenticationServiceException異常,如果拋出自定義異常,會被Security框架處理。用原生異常可以設置異常信息,直接返回給前端。
復制代碼
package top.imuster.auth.component.login;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
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 top.imuster.auth.config.SecurityConstants;
import top.imuster.auth.config.SmsCodeAuthenticationToken;

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

/**
 * @ClassName: SmsCodeAuthenticationFilter
 * @Description: 自定義攔截器,攔截登錄請求中的登錄類型
 * @author: hmr
 * @date: 2020/4/30 12:16
 */
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

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

    private static final String POST = "post";

    private boolean postOnly = true;

    public SmsCodeAuthenticationFilter(){
        super(new AntPathRequestMatcher("/emailCodeLogin", "POST"));
    }

    @Autowired
    AuthenticationManager authenticationManager;

    @Override
    @Autowired
    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        super.setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
        if(postOnly && !POST.equalsIgnoreCase(httpServletRequest.getMethod())){
            throw new AuthenticationServiceException("不允許{}這種的請求方式: " + httpServletRequest.getMethod());
        }
        //郵箱地址
        String loginName = obtainParameter(httpServletRequest, SecurityConstants.LOGIN_PARAM_NAME);
        //驗證碼
        String credentials = obtainParameter(httpServletRequest, SecurityConstants.EMAIL_VERIFY_CODE);
        loginName = loginName.trim();

        if(StringUtils.isBlank(loginName)) throw new AuthenticationServiceException("登錄名不能為空");
        if(StringUtils.isBlank(credentials)) throw new AuthenticationServiceException("驗證碼不能為空");

        SmsCodeAuthenticationToken authenticationToken = new SmsCodeAuthenticationToken(loginName, credentials);  //將輸入的信息封裝成一個SmsCodeAuthenicationToken對象,並向后傳遞
        setDetails(httpServletRequest, authenticationToken);
        return authenticationManager.authenticate(authenticationToken);
    }

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

    /**
     * @Author hmr
     * @Description 從request中獲得參數
     * @Date: 2020/4/30 12:22
     * @param request
     * @reture: java.lang.String
     **/
    protected String obtainParameter(HttpServletRequest request, String type){
        return request.getParameter(type);
    }
}
復制代碼
  • 設置了Filter攔截到登陸請求之后,還需要一個具體的校驗驗證碼是否正確的類  SmsAuthenticationProvider,該類才是具體的業務代碼
復制代碼
package top.imuster.auth.component.login;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import top.imuster.auth.config.SmsCodeAuthenticationToken;
import top.imuster.common.core.utils.RedisUtil;

/**
 * @ClassName: IrhAuthenticationProvider
 * @Description: IrhAuthenticationProvider
 * @author: hmr
 * @date: 2020/4/30 14:36
 */
public class SmsAuthenticationProvider implements AuthenticationProvider {
    private final Logger log = LoggerFactory.getLogger(this.getClass());

    @Autowired
    RedisTemplate redisTemplate;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;     //先將authentication強轉成SmsCodeAuthenticationToken
        //登錄名
        String loginName = authenticationToken.getPrincipal() == null?"NONE_PROVIDED":authentication.getName(); 
        //驗證碼
        String verify = (String)authenticationToken.getCredentials();
        String redisCode = (String)redisTemplate.opsForValue().get(RedisUtil.getConsumerLoginByEmail(loginName));   //從redis中獲得申請到的驗證碼
        if(StringUtils.isEmpty(redisCode) || !verify.equalsIgnoreCase(redisCode)){
            throw new AuthenticationServiceException("驗證碼失效或者錯誤");
        }
        return authentication;
    }

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

}
復制代碼

 並且在一個繼承了 WebSecurityConfigurerAdapter的類中將Filter和Provide聲明成Bean

復制代碼
package top.imuster.auth.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import top.imuster.auth.component.login.*;
import top.imuster.auth.service.Impl.UsernameUserDetailsServiceImpl;

import java.util.Arrays;

@Configuration
@EnableWebSecurity
@Order(2147483636)
class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public SmsCodeAuthenticationFilter smsCodeAuthenticationFilter(){
    SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
try {
smsCodeAuthenticationFilter.setAuthenticationManager(this.authenticationManager());
} catch (Exception e) {
e.printStackTrace();
}
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(irhAuthenticationSuccessHandler());
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(irhAuthenticationFailHandler());
return smsCodeAuthenticationFilter;
}
@Bean
public SmsAuthenticationProvider smsAuthenticationProvider(){
SmsAuthenticationProvider provider = new SmsAuthenticationProvider();
// 設置userDetailsService
// 禁止隱藏用戶未找到異常
return provider;
}


@Bean
@Override
public AuthenticationManager authenticationManager() throws Exception {
ProviderManager authenticationManager = new ProviderManager(Arrays.asList(smsAuthenticationProvider()));
return authenticationManager;
}

}
復制代碼

 

說明:Filter是用來攔截Request請求,並且從request請求中將指定的信息提取出來,判斷一些必要信息是否存在,不對信息進行校驗,校驗信息合法之后將其封裝到token中,向后傳遞給provide如;provider則

將filter中得到的信息進行校驗,在provider中,主要要注意的就是需要重寫 supports(Class<?> aClass) 方法,該方法的作用就是假如在系統中有多個filter,並且向后傳遞了多個不同的token,那么對應的token只能傳遞到對應的provider中,所以該方法返回到是一個boolean類型,用來給Spring Security決策是否需要進入該provider。

  • 獲取驗證碼
復制代碼
    /**
     * @Author hmr
     * @Description 發送email驗證碼
     * @Date: 2020/4/30 10:12
     * @param email  接受code的郵箱
     * @param type   1-注冊  2-登錄  3-忘記密碼
     * @reture: top.imuster.common.base.wrapper.Message<java.lang.String>
     **/
    @ApiOperation(value = "發送email驗證碼",httpMethod = "GET")
    @Idempotent(submitTotal = 5, timeTotal = 30, timeUnit = TimeUnit.MINUTES)
    @GetMapping("/sendCode/{type}/{email}")
    public Message<String> getCode(@ApiParam("郵箱地址") @PathVariable("email") String email, @PathVariable("type") Integer type) throws Exception {
        if(type != 1 && type != 2 && type != 3 && type != 4){
            return Message.createByError("參數異常,請刷新后重試");
        }
        userLoginService.getCode(email, type);
        return Message.createBySuccess();
    }
復制代碼

具體代碼我已經開源到GitHub上,倉庫地址為https://github.com/HMingR/irh,該項目中還有利用Spring security實現微信小程序登陸,用戶名密碼登陸等。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM