實現短信驗證碼登錄

在開發短信驗證碼接口

在之前圖片驗證碼的基礎上開發短信驗證碼

驗證碼實體SmsCode

  1. 短信驗證碼和圖片驗證碼就差一個圖片屬性,直接把ImageCode拿來改成SmsCode並去掉 private BufferedImage image; 屬性就行

  2. 因此可以使ImageCode繼承SmsCode

  3. 最後把SmsCode名字改爲ValidateCode比較好

  4. 驗證碼生成器ValidateCodeGenerator的返回值從ImageCode改爲ValidateCode

手機驗證碼發送接口

每個人的短信驗證碼發送服務商都不同,應該讓他們自己實現

SmsCodeSender

public interface SmsCodeSender {

    void send(String mobile,String code);

}

SmsCodeSender默認實現DefaultSmsCodeSender

public class DefaultSmsCodeSender implements SmsCodeSender {
    @Override
    public void send(String mobile, String code) {

        System.out.println("向手機發送短信驗證碼"+mobile+"---"+code);

    }
}

默認實現應該是讓使用者覆蓋掉的 同圖片驗證碼生成器接口一樣
配置如下
ValidateCodeBeanConfig

@Configuration
public class ValidateCodeBeanConfig {
	
	@Autowired
	private SecurityProperties securityProperties;
	
	@Bean
	@ConditionalOnMissingBean(name = "imageCodeGenerator")
	public ValidateCodeGenerator imageCodeGenerator() { //方法的名字就是spring容器中bean的名字
		ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
		codeGenerator.setSecurityProperties(securityProperties);
		return codeGenerator;
	}

	@Bean
//	@ConditionalOnMissingBean(name = "smsCodeSender")
	@ConditionalOnMissingBean(SmsCodeSender.class)//當容器中找到了SmsCodeSender的實現就不會再用此實現bean
	public SmsCodeSender smsCodeSender() { //方法的名字就是spring容器中bean的名字
		return new DefaultSmsCodeSender();
	}

}

手機驗證碼生成接口

類似於圖像驗證碼生成接口

ValidateCodeController


    @Autowired
    private ValidateCodeGenerator smsCodeGenerator;//注入手機驗證碼創建接口

    @Autowired
    private SmsCodeSender smsCodeSender; //注入手機驗證碼發送器

    @RequestMapping("/code/sms")
    private void createSms(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletRequestBindingException {
        //1根據請求中的隨機數生成圖片
        ValidateCode smsCode = smsCodeGenerator.generate(new ServletWebRequest(request));
        //2將隨機數放到session中
        sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,smsCode);
        //3、這塊應該由短信服務商將我們的短信發送出去,我們需要封裝一個短信驗證碼發送的接口
        String mobile = ServletRequestUtils.getRequiredStringParameter(request,"mobile");
        smsCodeSender.send(mobile,smsCode.getCode());

    }

SmsCodeGenerator

手機驗證碼的長度和過期時間做成可配置的屬性,具體方法和圖片驗證碼的創建相似,這裏就不介紹了

@Component("smsCodeGenerator")
public class SmsCodeGenerator implements ValidateCodeGenerator {

	@Autowired
	private SecurityProperties securityProperties;
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.imooc.security.core.validate.code.ValidateCodeGenerator#generate(org.
	 * springframework.web.context.request.ServletWebRequest)
	 */
	@Override
	public ValidateCode generate(ServletWebRequest request) {
		String code = RandomStringUtils.randomNumeric(securityProperties.getCode().getSms().getLength());
		return new ValidateCode(code, securityProperties.getCode().getSms().getExpireIn());
	}

	public SecurityProperties getSecurityProperties() {
		return securityProperties;
	}

	public void setSecurityProperties(SecurityProperties securityProperties) {
		this.securityProperties = securityProperties;
	}
	
	

}

不攔截短信驗證碼路徑

BrowserSecurityConfig

···························
  			.and()
                .authorizeRequests() //對請求授權
//                .antMatchers("/signIn.html","/code/image").permitAll() //加一個匹配器 對匹配的路徑不進行身份認證
                .antMatchers(securityProperties.getBrowser().getLoginPage(),"/code/*").permitAll() //加一個匹配器 對匹配的路徑不進行身份認證
              
              ···························

登錄頁面

在這裏插入圖片描述

<h3>短信登錄</h3>
<form action="/authentication/mobile" method="post">
    <table>
        <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">發送驗證碼</a>
            </td>
        </tr>
        <tr>
            <td colspan="2"><button type="submit">登錄</button></td>
        </tr>
    </table>
</form>

測試

在這裏插入圖片描述
在這裏插入圖片描述

重構

現在我們的驗證碼生成器有兩個實現的接口

@Autowired
private ValidateCodeGenerator imageCodeGenerator;

@Autowired
private ValidateCodeGenerator smsCodeGenerator;

使用模板方法重構,重構後的結構如下
在這裏插入圖片描述

校驗碼處理接口 ValidateCodeProcessor,封裝不同校驗碼的處理邏輯

/**
 * 校驗碼處理器,封裝不同校驗碼的處理邏輯
 * 
 * @author zhailiang
 *
 */
public interface ValidateCodeProcessor {

	/**
	 * 驗證碼放入session時的前綴
	 */
	String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_";

	/**
	 * 創建校驗碼
	 * 
	 * @param request
	 * @throws Exception
	 * ServletWebRequest spring提供的一個工具類  可以封裝請求和響應
	 */
	void create(ServletWebRequest request) throws Exception;

}

抽象實現 AbstractValidateCodeProcessor

public abstract class AbstractValidateCodeProcessor<C extends ValidateCode> implements ValidateCodeProcessor {

	/**
	 * 操作session的工具類
	 */
	private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
	/**
	 * 收集系統中所有的 {@link ValidateCodeGenerator} 接口的實現。
	 *
	 * 這個map的注入
	 * spring啓動的時候,會查找map的value接口ValidateCodeGenerator的所有實現bean,
	 * 並把這個bean爲value,bean的名稱爲key存入map中
	 *
	 * 這種行爲叫依賴搜索
	 */
	@Autowired
	private Map<String, ValidateCodeGenerator> validateCodeGenerators;

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * com.whale.security.core.validate.code.ValidateCodeProcessor#create(org.
	 * springframework.web.context.request.ServletWebRequest)
	 */
	@Override
	public void create(ServletWebRequest request) throws Exception {
		C validateCode = generate(request);//生成
		save(request, validateCode);//保存
		send(request, validateCode);//發送 這是一個抽象方法 需要子類去實現
	}

	/**
	 * 生成校驗碼
	 * 
	 * @param request
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private C generate(ServletWebRequest request) {
		String type = getProcessorType(request);
		String generatorName = type + "CodeGenerator";
		ValidateCodeGenerator validateCodeGenerator = validateCodeGenerators.get(generatorName);
		if (validateCodeGenerator == null) {
			throw new ValidateCodeException("驗證碼生成器" + generatorName + "不存在");
		}
		return (C) validateCodeGenerator.generate(request);
	}

	/**
	 * 保存校驗碼
	 *
	 * @param request
	 * @param validateCode
	 */
	private void save(ServletWebRequest request, C validateCode) {
		sessionStrategy.setAttribute(request, getSessionKey(request), validateCode);
	}

	/**
	 * 構建驗證碼放入session時的key
	 *
	 * @param request
	 * @return
	 */
	private String getSessionKey(ServletWebRequest request) {
		return SESSION_KEY_PREFIX + getProcessorType(request);
	}

	/**
	 * 發送校驗碼,由子類實現
     * 它的抽象方法 send由具體的子類實現
	 * 
	 * @param request
	 * @param validateCode
	 * @throws Exception
	 */
	protected abstract void send(ServletWebRequest request, C validateCode) throws Exception;

    /**
     * 根據請求的url獲取校驗碼的類型
     * @param request
     * @return
     */
	private String getProcessorType(ServletWebRequest request){
        String type = StringUtils.substringAfter(request.getRequest().getRequestURI(), "/code/");
        return type;

    }
}

兩個子類分別實現發送功能 ImageCodeProcessor SmsCodeProcessor

ImageCodeProcessor

@Component("imageCodeProcessor")
public class ImageCodeProcessor extends AbstractValidateCodeProcessor<ImageCode> {

	/**
	 * 發送圖形驗證碼,將其寫到響應中
	 */
	@Override
	protected void send(ServletWebRequest request, ImageCode imageCode) throws Exception {
		ImageIO.write(imageCode.getImage(), "JPEG", request.getResponse().getOutputStream());
	}

}

SmsCodeProcessor

@Component("smsCodeProcessor")
public class SmsCodeProcessor extends AbstractValidateCodeProcessor<ValidateCode> {

	/**
	 * 短信驗證碼發送器
	 */
	@Autowired
	private SmsCodeSender smsCodeSender;
	
	@Override
	protected void send(ServletWebRequest request, ValidateCode validateCode) throws Exception {
//		String mobile= ServletRequestUtils.getStringParameter((ServletRequest) request, "mobile");
//		String mobile= ServletRequestUtils.getRequiredStringParameter((ServletRequest)request,"mobile");
		String mobile= ServletRequestUtils.getRequiredStringParameter(request.getRequest(),"mobile");
		smsCodeSender.send(mobile, validateCode.getCode());
	}

}

ValidateCodeController 簡化

@RestController
public class ValidateCodeController implements Serializable {

    public static  final  String SESSION_KEY ="SESSION_KEY_IMAGE_CODE";//key

    @Autowired
    private Map<String, ValidateCodeProcessor> validateCodeProcessors;

    /**
     * 創建驗證碼,根據驗證碼類型不同,調用不同的 {@link ValidateCodeProcessor}接口實現
     *
     * @param request
     * @param response
     * @param type
     * @throws Exception
     */
    @GetMapping("/code/{type}")
    public void createCode(HttpServletRequest request, HttpServletResponse response, @PathVariable String type)
            throws Exception {
        validateCodeProcessors.get(type+"CodeProcessor").create(new ServletWebRequest(request,response));
        }
      }


短信登錄開發

在這裏插入圖片描述
在這裏插入圖片描述

SmsCodeAuthenticationToken

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	// ~ Instance fields
	// ================================================================================================

	private final Object principal;

	// ~ Constructors
	// ===================================================================================================

	/**
	 * 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 SmsCodeAuthenticationToken(String mobile) {
		super(null);
		this.principal = mobile;
		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>)
	 * authentication token.
	 *
	 * @param principal
	 * @param
	 * @param authorities
	 */
	public SmsCodeAuthenticationToken(Object principal,
			Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		super.setAuthenticated(true); // must use super, as we override
	}

	// ~ Methods
	// ========================================================================================================

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

SmsCodeAuthenticationProvider

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

	private UserDetailsService userDetailsService;

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.security.authentication.AuthenticationProvider#
	 * authenticate(org.springframework.security.core.Authentication)
	 */
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {

		SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;

		String principal = (String) authenticationToken.getPrincipal();//token中的手機號
		UserDetails user = userDetailsService.loadUserByUsername(principal);//根據手機號拿到對應的UserDetails

		if (user == null) {
			throw new InternalAuthenticationServiceException("無法獲取用戶信息");
		}
		
		SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
		
		authenticationResult.setDetails(authenticationToken.getDetails());

		return authenticationResult;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.security.authentication.AuthenticationProvider#
	 * supports(java.lang.Class)
	 *
	 * AuthenticationManager 判斷參數authentication是不是對應的token
	 */
	@Override
	public boolean supports(Class<?> authentication) {
		return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
	}

	public UserDetailsService getUserDetailsService() {
		return userDetailsService;
	}

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

}

SmsCodeAuthenticationFilter

public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	// ~ Static fields/initializers
	// =====================================================================================

	private static String WHALE_FROM_MOBILE_KEY="mobile";

	private String mobileParameter = WHALE_FROM_MOBILE_KEY;
	private boolean postOnly = true;//當前處理器是否處理post請求

	// ~ Constructors
	// ===================================================================================================

	/**
	 * 當前過濾器處理的請求是什麼
	 * 一個路徑匹配器  手機表單登錄的一個請求
	 */
	public SmsCodeAuthenticationFilter() {
		super(new AntPathRequestMatcher("/authentication/mobile", "POST"));

	}

	// ~ Methods
	// ========================================================================================================

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			//當前請求如果不是post請求則拋出異常
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}

		String mobile = obtainMobile(request);//從請求中獲取mobile參數

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

		mobile = mobile.trim();

		//實例化token
		SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

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


		//使用AuthenticationManager 調用 token
		return this.getAuthenticationManager().authenticate(authRequest);
	}


	/**
	 * 獲取手機號
	 */
	protected String obtainMobile(HttpServletRequest request) {
		return request.getParameter(mobileParameter);
	}

	/**
	 * Provided so that subclasses may configure what is put into the
	 * authentication request's details property.
	 *
	 * @param request
	 *            that an authentication request is being created for
	 * @param authRequest
	 *            the authentication request object that should have its details
	 *            set
	 */
	protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
		authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
	}

	/**
	 * Sets the parameter name which will be used to obtain the username from
	 * the login request.
	 *
	 * @param usernameParameter
	 *            the parameter name. Defaults to "username".
	 */
	public void setMobileParameter(String usernameParameter) {
		Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
		this.mobileParameter = usernameParameter;
	}


	/**
	 * Defines whether only HTTP POST requests will be allowed by this filter.
	 * If set to true, and an authentication request is received which is not a
	 * POST request, an exception will be raised immediately and authentication
	 * will not be attempted. The <tt>unsuccessfulAuthentication()</tt> method
	 * will be called as if handling a failed authentication.
	 * <p>
	 * Defaults to <tt>true</tt> but may be overridden by subclasses.
	 */
	public void setPostOnly(boolean postOnly) {
		this.postOnly = postOnly;
	}

	public final String getMobileParameter() {
		return mobileParameter;
	}

}

SmsCodeAuthenticationSecurityConfig

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
	
	@Autowired
	private AuthenticationSuccessHandler whaleAuthenticationSuccessHandler;
	
	@Autowired
	private AuthenticationFailureHandler whaleAuthenticationFailureHandler;

	@Qualifier("myUserDetailsService")
	@Autowired
	private UserDetailsService userDetailsService;

	
	@Override
	public void configure(HttpSecurity http) throws Exception {
		
		SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
		smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));

		smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(whaleAuthenticationSuccessHandler);
		smsCodeAuthenticationFilter.setAuthenticationFailureHandler(whaleAuthenticationFailureHandler);
		
		SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
		smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
		
		http.authenticationProvider(smsCodeAuthenticationProvider)
			.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
		
	}

}

SmsCodeFilter

同ImageCodeFilter

校驗短信驗證碼並登錄
重構代碼

BrowserSecurityConfig

同圖片驗證碼過濾器一樣
添加短信驗證碼過濾器
在這裏插入圖片描述
添加配置
在這裏插入圖片描述

測試

ok

重構

在這裏插入圖片描述
1兩個驗證碼的過濾器合併爲一個
2config配置整合
3重複字符串整合SecurityConstants

項目地址
https://github.com/whaleluo/securitydemo.git

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章