1. 程式人生 > >spring-security-oauth2(四) 圖片驗證碼

spring-security-oauth2(四) 圖片驗證碼

圖片驗證碼

  1. 圖片驗證碼生成介面
  2. 認證流程加入圖片驗證碼校驗
  3. 圖片驗證碼重構

1.圖片驗證碼生成介面

  1. 呼叫com.google.code.kaptcha.Producer生成圖片驗證碼
  2. 將隨機數存到session快取中
  3. 將生成的圖片寫到響應流中

圖片驗證碼封裝類  ImageCaptchaVo

package com.rui.tiger.auth.core.captcha;

import lombok.Data;

import java.awt.image.BufferedImage;
import java.time.LocalDateTime;

/**
 * 圖片驗證碼資訊物件
 * @author CaiRui
 * @Date 2018/12/9 18:03
 */
@Data
public class ImageCaptchaVo {
    /**
     * 圖片驗證碼
     */
    private BufferedImage image;
    /**
     *  驗證碼
     */
    private String code;
    /**
     * 失效時間 這個通常放在快取中或維護在資料庫中
     */
    private LocalDateTime expireTime;

    public ImageCaptchaVo(BufferedImage image, String code, int expireAfterSeconds){
        this.image = image;
        this.code = code;
        //多少秒後
        this.expireTime = LocalDateTime.now().plusSeconds(expireAfterSeconds);
    }

    public ImageCaptchaVo(BufferedImage image, String code, LocalDateTime expireTime){
        this.image = image;
        this.code = code;
        this.expireTime = expireTime;
    }

    /**
     * 是否失效
     * @return
     */
    public boolean isExpried() {
        return LocalDateTime.now().isAfter(expireTime);
    }

}

圖片驗證碼服務類  CaptchaController  注意這個路徑/captcha/image要在BrowserSecurityConfig配置中放行

package com.rui.tiger.auth.core.captcha;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 驗證碼控制器
 * @author CaiRui
 * @date 2018-12-10 12:13
 */
@RestController
public class CaptchaController {

	public static final String IMAGE_CAPTCHA_SESSION_KEY="image_captcha_session_key";
	private static final String FORMAT_NAME="JPEG";

	@Autowired
	private CaptchaGenerate captchaGenerate;
    //spring session 工具類
	private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();

	/**
	 * 獲取圖片驗證碼
	 * @param request
	 * @param response
	 * @throws IOException
	 */
	@GetMapping("/captcha/image")
	public  void createKaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
		//1.介面生成驗證碼
		ImageCaptchaVo imageCaptcha= (ImageCaptchaVo) captchaGenerate.generate();
		//2.儲存到session中
		sessionStrategy.setAttribute(new ServletWebRequest(request), IMAGE_CAPTCHA_SESSION_KEY, imageCaptcha);
		//3.寫到響應流中
		response.setHeader("Cache-Control", "no-store, no-cache");// 沒有快取
		response.setContentType("image/jpeg");
		ImageIO.write(imageCaptcha.getImage(),FORMAT_NAME,response.getOutputStream());
	}

}

  CaptchaGenerate 介面及其實現類

package com.rui.tiger.auth.core.captcha;

/**
 * 驗證碼生成介面
 *
 * @author CaiRui
 * @date 2018-12-10 12:03
 */
public interface CaptchaGenerate {
	/**
	 * 生成圖片驗證碼
	 *
	 * @return
	 */
	ImageCaptchaVo generate();
}
package com.rui.tiger.auth.core.captcha;

import com.google.code.kaptcha.Producer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.awt.image.BufferedImage;

/**
 * 圖片驗證碼生成介面
 *
 * @author CaiRui
 * @date 2018-12-10 12:07
 */
@Service("imageCaptchaGenerate")
public class ImageCaptchaGenerate implements CaptchaGenerate {

	@Autowired
	private Producer producer;//config bean中配置

	@Override
	public ImageCaptchaVo generate() {
		String code = producer.createText();
		BufferedImage bufferedImage = producer.createImage(code);
		return new ImageCaptchaVo(bufferedImage, code, 60 * 5);//5分鐘過期
	}
}

Producer配置類 KaptchaGenerateConfig

package com.rui.tiger.auth.core.config;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

/**
 * 驗證碼生成配置類
 * @author CaiRui
 * @date 2018-12-10 12:09
 */
@Configuration
public class KaptchaGenerateConfig {

	//TODO 配置項放在配置檔案中
	@Bean
	public DefaultKaptcha producer() {
		Properties properties = new Properties();
		properties.put("kaptcha.border", "no");
		properties.put("kaptcha.textproducer.font.color", "black");
		properties.put("kaptcha.textproducer.char.space", "5");
		Config config = new Config(properties);
		DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
		defaultKaptcha.setConfig(config);
		return defaultKaptcha;
	}
}

2.認證流程加入圖片驗證碼校驗

通過上篇的原始碼分析spring-security原理,其實就是過濾器鏈上的各個過濾器協同工作,思路如下:

  • 編寫我們的自定義圖片驗證碼過濾器
  • 將它放在UsernamePasswordAuthenticationFilter表單過濾器之前

驗證碼過濾器 

package com.rui.tiger.auth.core.captcha;

import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang.StringUtils;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;

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

/**
 * 圖片驗證碼過濾器
 * OncePerRequestFilter 過濾器只會呼叫一次
 *
 * @author CaiRui
 * @date 2018-12-10 12:23
 */
public class CaptchaFilter extends OncePerRequestFilter {

	//一般在配置類中進行注入
	@Setter
	@Getter
	private AuthenticationFailureHandler failureHandler;

	private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
		//表單登入的post請求
		if (StringUtils.equals("/authentication/form", request.getRequestURI())
				&& StringUtils.equalsIgnoreCase("post", request.getMethod())) {
			try {
				validate(request);
			} catch (CaptchaException captchaException) {
				//失敗呼叫我們的自定義失敗處理器
				failureHandler.onAuthenticationFailure(request, response, captchaException);
				//後續流程終止
				return;
			}
		}
		filterChain.doFilter(request, response);

	}

	/**
	 * 圖片驗證碼校驗
	 *
	 * @param request
	 */
	private void validate(HttpServletRequest request) throws ServletRequestBindingException {
		// 拿到之前儲存的imageCode資訊
		ServletWebRequest swr = new ServletWebRequest(request);
		ImageCaptchaVo imageCodeInSession = (ImageCaptchaVo) sessionStrategy.getAttribute(swr, CaptchaController.IMAGE_CAPTCHA_SESSION_KEY);
		String codeInRequest = ServletRequestUtils.getStringParameter(request, "imageCode");

		if (StringUtils.isBlank(codeInRequest)) {
			throw new CaptchaException("驗證碼的值不能為空");
		}
		if (imageCodeInSession == null) {
			throw new CaptchaException("驗證碼不存在");
		}
		if (imageCodeInSession.isExpried()) {
			sessionStrategy.removeAttribute(swr, CaptchaController.IMAGE_CAPTCHA_SESSION_KEY);
			throw new CaptchaException("驗證碼已過期");
		}
		if (!StringUtils.equals(imageCodeInSession.getCode(), codeInRequest)) {
			throw new CaptchaException("驗證碼不匹配");
		}
		//驗證通過 移除快取
		sessionStrategy.removeAttribute(swr, CaptchaController.IMAGE_CAPTCHA_SESSION_KEY);
	}
}

自定義驗證碼異常 

package com.rui.tiger.auth.core.captcha;

import org.springframework.security.core.AuthenticationException;

/**
 * 自定義驗證碼異常
 * @author CaiRui
 * @date 2018-12-10 12:43
 */
public class CaptchaException extends AuthenticationException {

	public CaptchaException(String msg, Throwable t) {
		super(msg, t);
	}

	public CaptchaException(String msg) {
		super(msg);
	}
}

將過濾器加入到瀏覽器許可權配置中

package com.rui.tiger.auth.browser.config;

import com.rui.tiger.auth.core.authentication.TigerAuthenticationFailureHandler;
import com.rui.tiger.auth.core.authentication.TigerAuthenticationSuccessHandler;
import com.rui.tiger.auth.core.captcha.CaptchaFilter;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * 瀏覽器security配置類
 *
 * @author CaiRui
 * @date 2018-12-4 8:41
 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private SecurityProperties securityProperties;
    @Autowired
    private TigerAuthenticationFailureHandler tigerAuthenticationFailureHandler;
    @Autowired
    private TigerAuthenticationSuccessHandler tigerAuthenticationSuccessHandler;

    /**
     * 密碼加密解密
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //加入圖片驗證碼過濾器
        CaptchaFilter captchaFilter=new CaptchaFilter();
        captchaFilter.setFailureHandler(tigerAuthenticationFailureHandler);
        //圖片驗證碼放在認證之前
        http.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                .loginPage( "/authentication/require")//自定義登入請求
                .loginProcessingUrl("/authentication/form")//自定義登入表單請求
                .successHandler(tigerAuthenticationSuccessHandler)
                .failureHandler(tigerAuthenticationFailureHandler)
                .and()
                .authorizeRequests()
                .antMatchers(securityProperties.getBrowser().getLoginPage(),
                        "/authentication/require","/captcha/image")//此路徑放行 否則會陷入死迴圈
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .csrf().disable()//跨域關閉
        ;
    }

}

前段標準登入介面加入驗證碼改造

<html>
<head>
    <meta charset="UTF-8">
    <title>登入</title>
</head>
<body>
<h2>標準登入頁面</h2>
<h3>表單登入</h3>
<form action="/authentication/form" method="post">
    <table>
        <tr>
            <td>使用者名稱:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>密碼:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td>圖形驗證碼:</td>
            <td>
                <input type="text" name="imageCode">
                <img src="/captcha/image">
            </td>
        </tr>
        <tr>
            <td colspan="2"><button type="submit">登入</button></td>
        </tr>
    </table>
</form>
</body>
</html>

ok  我們專案重新啟動下 來看下自定義的驗證碼過濾器是否可用,直接瀏覽器輸入我們的登入地址http://localhost:8070/tiger-login.html

可以看到圖片驗證碼已經成功顯示出來了,我們來看看驗證邏輯是否可用,試下不輸入驗證碼

這是因為我們自定義的失敗處理器,列印了全部的錯誤堆疊資訊我們來調整下,調整後如下

package com.rui.tiger.auth.core.authentication;

import com.alibaba.fastjson.JSON;
import com.rui.tiger.auth.core.model.enums.LoginTypeEnum;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import com.rui.tiger.auth.core.support.SimpleResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

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

/**
 * 認證失敗處理器
 * @author CaiRui
 * @date 2018-12-6 12:40
 */
@Component("tigerAuthenticationFailureHandler")
@Slf4j
public class TigerAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        log.info("登入失敗");
        if (LoginTypeEnum.JSON.equals(securityProperties.getBrowser().getLoginType())) {
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(JSON.toJSONString(new SimpleResponse(exception.getMessage())));
        } else {
            // 如果使用者配置為跳轉,則跳到Spring Boot預設的錯誤頁面
            super.onAuthenticationFailure(request, response, exception);
        }

    }
}

這次我們試下一個錯誤的驗證碼登入看下

 

 ok 我們的自定義驗證碼生效,其它的情況可以自行除錯 下面我們將對驗證碼進行重構

3.圖片驗證碼重構

  1. 驗證碼生成的基本引數可以配置
  2. 驗證碼攔截的介面可以配置
  3. 驗證碼的生成邏輯可以配置