1. 程式人生 > 程式設計 >Spring Security 實現簡訊驗證碼登入功能

Spring Security 實現簡訊驗證碼登入功能

之前文章都是基於使用者名稱密碼登入,第六章圖形驗證碼登入其實還是使用者名稱密碼登入,只不過多了一層圖形驗證碼校驗而已;Spring Security預設提供的認證流程就是使用者名稱密碼登入,整個流程都已經固定了,雖然提供了一些介面擴充套件,但是有些時候我們就需要有自己特殊的身份認證邏輯,比如用簡訊驗證碼登入,它和使用者名稱密碼登入的邏輯是不一樣的,這時候就需要重新寫一套身份認證邏輯。

開發簡訊驗證碼介面

獲取驗證碼

簡訊驗證碼的傳送獲取邏輯和圖片驗證碼類似,這裡直接貼出程式碼。

@GetMapping("/code/sms")
	public void createSmsCode(HttpServletRequest request,HttpServletResponse response) throws Exception {
		// 建立驗證碼
		ValidateCode smsCode = createCodeSmsCode(request);
		// 將驗證碼放到session中
		sessionStrategy.setAttribute(new ServletWebRequest(request),SMS_CODE_SESSION_KEY,smsCode);
		String mobile = ServletRequestUtils.getRequiredStringParameter(request,"mobile");
		// 傳送驗證碼
		smsCodeSender.send(mobile,smsCode.getCode());
	}

前端程式碼

<tr>
				<td>手機號:</td>
				<td><input type="text" name="mobile" value="13012345678"></td>
			</tr>
			<tr>
				<td>簡訊驗證碼:</td>
				<td>
					<input type="text" name="smsCode">
					<a href="/code/sms?mobile=13012345678" rel="external nofollow" >傳送驗證碼</a>
				</td>
			</tr>

簡訊驗證碼流程原理

簡訊驗證碼登入和使用者名稱密碼登入對比

流程對比

步驟流程

  • 首先點選登入應該會被SmsAuthenticationFilter過濾器處理,這個過濾器拿到請求以後會在登入請求中拿到手機號,然後封裝成自定義的一個SmsAuthenticationToken(未認證)。
  • 這個Token也會傳給AuthenticationManager,因為AuthenticationManager整個系統只有一個,它會檢索系統中所有的AuthenticationProvider,這時候我們要提供自己的SmsAuthenticationProvider,用它來校驗自己寫的SmsAuthenticationToken的手機號資訊。
  • 在校驗的過程中同樣會呼叫UserDetailsService,把手機號傳給它讓它去讀使用者資訊,去判斷是否能登入,登入成功的話再把SmsAuthenticationToken標記為已認證。
  • 到這裡為止就是簡訊驗證碼的認證流程,上面的流程並沒有提到校驗驗證碼資訊,其實它的驗證流程和圖形驗證碼驗證流程也是類似,同樣是在SmsAuthenticationFilter過濾器之前加一個過濾器來驗證簡訊驗證碼

程式碼實現

SmsCodeAuthenticationToken

  • 作用:封裝認證Token
  • 實現:可以繼承AbstractAuthenticationToken抽象類,該類實現了Authentication介面
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
	private final Object principal;
	/**
	 * 進入SmsAuthenticationFilter時,構建一個未認證的Token
	 *
	 * @param mobile
	 */
	public SmsCodeAuthenticationToken(String mobile) {
		super(null);
		this.principal = mobile;
		setAuthenticated(false);
	}
	/**
	 * 認證成功以後構建為已認證的Token
	 *
	 * @param principal
	 * @param authorities
	 */
	public SmsCodeAuthenticationToken(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 {
		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();
	}
}

SmsCodeAuthenticationFilter

  • 作用:處理簡訊登入的請求,構建Token,把請求資訊設定到Token中。
  • 實現:該類可以模仿UsernamePasswordAuthenticationFilter類,繼承AbstractAuthenticationProcessingFilter抽象類
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	private String mobileParameter = "mobile";
	private boolean postOnly = true;
 /**
 * 表示要處理的請求路徑
 */
	public SmsCodeAuthenticationFilter() {
 super(new AntPathRequestMatcher("/authentication/mobile","POST"));
	}
 @Override
	public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response)
			throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		String mobile = obtainMobile(request);
		if (mobile == null) {
			mobile = "";
		}
		mobile = mobile.trim();
		SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
		// 把請求資訊設到Token中
		setDetails(request,authRequest);
		return this.getAuthenticationManager().authenticate(authRequest);
	}
	/**
	 * 獲取手機號
	 */
	protected String obtainMobile(HttpServletRequest request) {
		return request.getParameter(mobileParameter);
	}
	protected void setDetails(HttpServletRequest request,SmsCodeAuthenticationToken authRequest) {
		authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
	}
	public void setMobileParameter(String usernameParameter) {
		Assert.hasText(usernameParameter,"Username parameter must not be empty or null");
		this.mobileParameter = usernameParameter;
	}
	public void setPostOnly(boolean postOnly) {
		this.postOnly = postOnly;
	}
	public final String getMobileParameter() {
		return mobileParameter;
	}
}

SmsAuthenticationProvider

  • 作用:提供認證Token的校驗邏輯,配置為能夠支援SmsCodeAuthenticationToken的校驗
  • 實現:實現AuthenticationProvider介面,實現其兩個方法。
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
	private UserDetailsService userDetailsService;
 /**
 * 進行身份認證的邏輯
 *
 * @param authentication
 * @return
 * @throws AuthenticationException
 */
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
		
		UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
		if (user == null) {
			throw new InternalAuthenticationServiceException("無法獲取使用者資訊");
		}
		
		SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user,user.getAuthorities());
		
		authenticationResult.setDetails(authenticationToken.getDetails());
		return authenticationResult;
	}
 /**
 * 表示支援校驗的Token,這裡是SmsCodeAuthenticationToken
 *
 * @param authentication
 * @return
 */
	@Override
	public boolean supports(Class<?> authentication) {
		return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
	}
	public UserDetailsService getUserDetailsService() {
		return userDetailsService;
	}
	public void setUserDetailsService(UserDetailsService userDetailsService) {
		this.userDetailsService = userDetailsService;
	}
}

ValidateCodeFilter

  • :校驗簡訊驗證碼
  • 實現:和圖形驗證碼類似,繼承OncePerRequestFilter介面防止多次呼叫,主要就是驗證碼驗證邏輯,驗證通過則繼續下一個過濾器。
@Component("validateCodeFilter")
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
	/**
	 * 驗證碼校驗失敗處理器
	 */
	@Autowired
	private AuthenticationFailureHandler authenticationFailureHandler;
	/**
	 * 系統配置資訊
	 */
	@Autowired
	private SecurityProperties securityProperties;
	/**
	 * 系統中的校驗碼處理器
	 */
	@Autowired
	private ValidateCodeProcessorHolder validateCodeProcessorHolder;
	/**
	 * 存放所有需要校驗驗證碼的url
	 */
	private Map<String,ValidateCodeType> urlMap = new HashMap<>();
	/**
	 * 驗證請求url與配置的url是否匹配的工具類
	 */
	private AntPathMatcher pathMatcher = new AntPathMatcher();
	/**
	 * 初始化要攔截的url配置資訊
	 */
	@Override
	public void afterPropertiesSet() throws ServletException {
		super.afterPropertiesSet();
		urlMap.put("/authentication/mobile",ValidateCodeType.SMS);
		addUrlToMap(securityProperties.getCode().getSms().getUrl(),ValidateCodeType.SMS);
	}
	/**
	 * 講系統中配置的需要校驗驗證碼的URL根據校驗的型別放入map
	 * 
	 * @param urlString
	 * @param type
	 */
	protected void addUrlToMap(String urlString,ValidateCodeType type) {
		if (StringUtils.isNotBlank(urlString)) {
			String[] urls = StringUtils.splitByWholeSeparatorPreserveAllTokens(urlString,",");
			for (String url : urls) {
				urlMap.put(url,type);
			}
		}
	}
	/**
	 * 驗證簡訊驗證碼
	 * 
	 * @param request
	 * @param response
	 * @param chain
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain)
			throws ServletException,IOException {
		ValidateCodeType type = getValidateCodeType(request);
		if (type != null) {
			logger.info("校驗請求(" + request.getRequestURI() + ")中的驗證碼,驗證碼型別" + type);
			try {
				// 進行驗證碼的校驗
				validateCodeProcessorHolder.findValidateCodeProcessor(type)
						.validate(new ServletWebRequest(request,response));
				logger.info("驗證碼校驗通過");
			} catch (ValidateCodeException exception) {
				// 如果校驗丟擲異常,則交給我們之前文章定義的異常處理器進行處理
				authenticationFailureHandler.onAuthenticationFailure(request,response,exception);
				return;
			}
		}
		// 繼續呼叫後邊的過濾器
		chain.doFilter(request,response);
	}
	/**
	 * 獲取校驗碼的型別,如果當前請求不需要校驗,則返回null
	 * 
	 * @param request
	 * @return
	 */
	private ValidateCodeType getValidateCodeType(HttpServletRequest request) {
		ValidateCodeType result = null;
		if (!StringUtils.equalsIgnoreCase(request.getMethod(),"GET")) {
			Set<String> urls = urlMap.keySet();
			for (String url : urls) {
				if (pathMatcher.match(url,request.getRequestURI())) {
					result = urlMap.get(url);
				}
			}
		}
		return result;
	}
}

新增配置

SmsCodeAuthenticationSecurityConfig

作用:配置SmsCodeAuthenticationFilter,後面需要把這些配置加到主配置類BrowserSecurityConfig

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain,HttpSecurity> {
	
	@Autowired
	private AuthenticationSuccessHandler meicloudAuthenticationSuccessHandler;
	
	@Autowired
	private AuthenticationFailureHandler meicloudAuthenticationFailureHandler;
	
	@Autowired
	private UserDetailsService userDetailsService;
	
	@Autowired
	private PersistentTokenRepository persistentTokenRepository;
	
	@Override
	public void configure(HttpSecurity http) throws Exception {
		
		SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
		// 設定AuthenticationManager
		smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
		// 設定登入成功處理器
		smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(meicloudAuthenticationSuccessHandler);
		// 設定登入失敗處理器
		smsCodeAuthenticationFilter.setAuthenticationFailureHandler(meicloudAuthenticationFailureHandler);
		String key = UUID.randomUUID().toString();
		smsCodeAuthenticationFilter.setRememberMeServices(new PersistentTokenBasedRememberMeServices(key,userDetailsService,persistentTokenRepository));
		
		SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
		smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
		// 將自己寫的Provider加到Provider集合裡去
		http.authenticationProvider(smsCodeAuthenticationProvider)
			.addFilterAfter(smsCodeAuthenticationFilter,UsernamePasswordAuthenticationFilter.class);
	}
}

BrowserSecurityConfig

作用:主配置類;新增簡訊驗證碼配置類、新增SmsCodeAuthenticationSecurityConfig配置

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	@Autowired
	private SecurityProperties securityProperties;
	@Autowired
	private DataSource dataSource;
	@Autowired
	private UserDetailsService userDetailsService;
	@Autowired
	private AuthenticationSuccessHandler meicloudAuthenticationSuccessHandler;
	@Autowired
	private AuthenticationFailureHandler meicloudAuthenticationFailureHandler;
	@Autowired
	private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// 驗證碼校驗過濾器
		ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
		// 將驗證碼校驗過濾器加到 UsernamePasswordAuthenticationFilter 過濾器之前
		http.addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class)
				.formLogin()
				// 當用戶登入認證時預設跳轉的頁面
				.loginPage("/authentication/require")
				// 以下這行 UsernamePasswordAuthenticationFilter 會知道要處理表單的 /authentication/form 請求,而不是預設的 /login
				.loginProcessingUrl("/authentication/form")
				.successHandler(meicloudAuthenticationSuccessHandler)
				.failureHandler(meicloudAuthenticationFailureHandler)
				// 配置記住我功能
				.and()
				.rememberMe()
				// 配置TokenRepository
				.tokenRepository(persistentTokenRepository())
				// 配置Token過期時間
				.tokenValiditySeconds(3600)
				// 最終拿到使用者名稱之後,使用UserDetailsService去做登入
				.userDetailsService(userDetailsService)
				.and()
				.authorizeRequests()
				// 排除對 "/authentication/require" 和 "/meicloud-signIn.html" 的身份驗證
				.antMatchers("/authentication/require",securityProperties.getBrowser().getSignInPage(),"/code/*").permitAll()
				// 表示所有請求都需要身份驗證
				.anyRequest()
				.authenticated()
				.and()
				.csrf().disable()// 暫時把跨站請求偽造的功能關閉掉
				// 相當於把smsCodeAuthenticationSecurityConfig裡的配置加到上面這些配置的後面
				.apply(smsCodeAuthenticationSecurityConfig);
	}
	/**
	 * 記住我功能的Token存取器配置
	 *
	 * @return
	 */
	@Bean
	public PersistentTokenRepository persistentTokenRepository() {
		JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
		tokenRepository.setDataSource(dataSource);
		// 啟動的時候自動建立表,建表語句 JdbcTokenRepositoryImpl 已經都寫好了
		tokenRepository.setCreateTableOnStartup(true);
		return tokenRepository;
	}
}

總結

到此這篇關於Spring Security 實現簡訊驗證碼登入功能的文章就介紹到這了,更多相關spring security 驗證碼登入內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!