文章目錄
開發生成圖形驗證碼接口
圖片實體 ImageCode
public class ImageCode {
/**
* 一個圖片驗證碼包含三個信息
*/
private BufferedImage image; //圖片
private String code;//code是一個隨機數,圖片根據這個隨機數生成,這個隨機數是要存入到session中的
private LocalDateTime expireTime;//驗證碼圖片過期時間
/**
*
* @param image
* @param code
* @param expireIn 多少秒過期
*/
public ImageCode(BufferedImage image, String code, int expireIn) {
this.image = image;
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}
public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
this.image = image;
this.code = code;
this.expireTime = expireTime;
}
public boolean isExpried() {
return LocalDateTime.now().isAfter(expireTime);
}
圖片接口 ValidateCodeController
根據隨機數生成圖片
將隨機數存到session中
將生成的圖片寫到接口的響應中
@RestController
public class ValidateCodeController implements Serializable {
private static final String SESSION_KEY ="SESSION_KEY_IMAGE_CODE";//key
//spring 操作session的工具類
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@RequestMapping("/code/image")
private void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
//1根據請求中的隨機數生成圖片
ImageCode imageCode = createImageCode(request);
//2將隨機數放到session中
sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
//3將生成的圖片寫到接口的響應中
ImageIO.write(imageCode.getImage(),"jpeg",response.getOutputStream());
}
private ImageCode createImageCode(HttpServletRequest request) {
//生成一個圖片對象
int width = 67;
int height =23;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
//生成干擾條紋
Random random = new Random();
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
//生成四位隨機數 寫入圖片
String sRand = "";
for (int i = 0; i <4; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 16);
}
g.dispose();
return new ImageCode(image, sRand, 60);
}
在認證流程中加入圖形驗證碼校驗
登錄頁面
<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="/code/image">
</td>
</tr>
<tr>
<td colspan="2"><button type="submit">登錄</button></td>
</tr>
</table>
安全認證配置不攔截圖片路徑
測試 圖片路徑無法訪問404及解決
圖片路徑無法訪問404,發現對應的model在mave中爲灰色
解決
https://blog.csdn.net/L359389556/article/details/82852244
訪問
自定義過濾器 ValidateCodeFilter 校驗登錄驗證碼
/**
* 繼承spring中的OncePerRequestFilter,確保每次請求調用一次過濾器
*/
public class ValidateCodeFilter extends OncePerRequestFilter {
private AuthenticationFailureHandler authenticationFailureHandler;
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
if(StringUtils.equals("/authentication/form",httpServletRequest.getRequestURI())
&& StringUtils.equalsAnyIgnoreCase(httpServletRequest.getMethod(),"post")){
try {
validate(new ServletWebRequest(httpServletRequest));
}catch (ValidateCodeException e){
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);
return;//失敗後直接返回,不再走下面的過濾器
}
}
//如果不是登錄請求,直接放行
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
if(StringUtils.isBlank(codeInRequest)){
throw new ValidateCodeException("驗證碼的值不能爲空");
}
if(codeInSession == null){
throw new ValidateCodeException("驗證碼不存在");
}
if(codeInSession.isExpried()){
sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);
throw new ValidateCodeException("驗證碼已過期");
}
if(! StringUtils.equals(codeInSession.getCode(),codeInRequest)){
throw new ValidateCodeException("驗證碼不匹配");
}
sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);
}
}
自定義異常 ValidateCodeException
/**
* AuthenticationException 是 springframework.security提供的,登錄過程中所有異常的基類
*
*/
public class ValidateCodeException extends AuthenticationException {
private static final long serialVersionUID = -7285211528095468156L;
public ValidateCodeException(String msg) {
super(msg);
}
}
配置指定位置中加入此攔截器
protected void configure(HttpSecurity http) throws Exception {
//http.formLogin() //指定身份認證的方式爲表單登錄
//http.httpBasic()
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(whaleAuthenctiationFailureHandler);//設置錯誤過濾器
http.addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class)
.formLogin()
// .loginPage("/signIn.html") //指定登錄頁面的url
// .loginPage("/anthentication/require") //指定登錄頁面的url
.loginPage(securityProperties.getBrowser().getLoginPage()) //指定登錄頁面的url
.loginProcessingUrl("/authentication/form")
.successHandler(whaleAuthenticationSuccessHandler)
.failureHandler(whaleAuthenctiationFailureHandler)
.permitAll()
.and()
.authorizeRequests() //對請求授權
// .antMatchers("/signIn.html","/code/image").permitAll() //加一個匹配器 對匹配的路徑不進行身份認證
.antMatchers(securityProperties.getBrowser().getLoginPage(),"/code/image").permitAll() //加一個匹配器 對匹配的路徑不進行身份認證
.anyRequest() //任何請求
.authenticated() //安全認證
.and()
.cors().disable().csrf().disable();// 禁用跨站攻擊
// 默認都會產生一個hiden標籤 裏面有安全相關的驗證 防止請求僞造 這邊我們暫時不需要 可禁用掉
//任何請求都必須經過表單驗證才能進行訪問
/* http.csrf().disable().cors().disable().headers().disable()
.authorizeRequests()
.antMatchers("/signIn.html").permitAll() // 配置不需要身份認證的請求地址
.anyRequest().authenticated() // 其他所有訪問路徑需要身份認證
.and()
.formLogin()
.loginPage("/signIn.html") // 指定登錄請求地址
.loginProcessingUrl("/authentication/form")
.permitAll();
*/
}
測試ok
簡化認證失敗處理信息
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException authenticationException ) throws IOException, ServletException {
logger.info("登錄失敗");
if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
httpServletResponse.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
httpServletResponse.setContentType("application/json;charset=UTF-8");
// httpServletResponse.getWriter().write(objectMapper.writeValueAsString(authenticationException));//打印的信息太多 簡化如下
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse(authenticationException.getMessage())));
}else {
super.onAuthenticationFailure(httpServletRequest,httpServletResponse,authenticationException);
}
}
重構圖形驗證碼接口
驗證碼基本參數可配置
默認配置
ImageCodeProperties
public class ImageCodeProperties {
private int width = 67;
private int height = 23;
private int length = 4;
private int expireIn = 60;
再封裝一層ValidateCodeProperties
public class ValidateCodeProperties {
private ImageCodeProperties image = new ImageCodeProperties();
public ImageCodeProperties getImage() {
return image;
}
public void setImage(ImageCodeProperties image) {
this.image = image;
}
}
SecurityProperties加入ValidateCodeProperties
@ConfigurationProperties(prefix = "whale.security") //這個類會讀取以whale.security開頭的配置項
public class SecurityProperties {
//瀏覽器配置
private BrowserProperties browser = new BrowserProperties();
//驗證碼配置
private ValidateCodeProperties code = new ValidateCodeProperties();
應用級配置
demo application
whale.security.code.image.length = 6#驗證碼長度、
whale.security.code.image.width = 200
請求級配置
<tr>
<td>驗證碼:</td>
<td>
<input type="text" name="imageCode">
<img src="/code/image?width=200">
</td>
</tr>
攔截器引用配置
@RestController
public class ValidateCodeController implements Serializable {
public static final String SESSION_KEY ="SESSION_KEY_IMAGE_CODE";//key
//spring 操作session的工具類
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private SecurityProperties securityProperties;
@RequestMapping("/code/image")
private void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
//1根據請求中的隨機數生成圖片
// ImageCode imageCode = createImageCode(request);
ImageCode imageCode = createImageCode(new ServletWebRequest(request));
//2將隨機數放到session中
sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
//3將生成的圖片寫到接口的響應中
ImageIO.write(imageCode.getImage(),"jpeg",response.getOutputStream());
}
/**
*
* @param request(HttpServletRequest)
* @return
*/
private ImageCode createImageCode(ServletWebRequest request) {
//生成一個圖片對象
// int width = 67;
int width = ServletRequestUtils.getIntParameter(request.getRequest(),"width",securityProperties.getCode().getImage().getWidth());
// int height =23;
int height = ServletRequestUtils.getIntParameter(request.getRequest(),"height",securityProperties.getCode().getImage().getHeight());
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
//生成干擾條紋
Random random = new Random();
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
//生成四位隨機數 寫入圖片
String sRand = "";
// for (int i = 0; i <4; i++) {
// 驗證碼的長度不應該在請求中配置
for (int i = 0; i <securityProperties.getCode().getImage().getLength(); i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 16);
}
g.dispose();
// return new ImageCode(image, sRand, 60);
return new ImageCode(image, sRand, securityProperties.getCode().getImage().getExpireIn());
}
/**
* 生成隨機背景條紋
*
* @param fc
* @param bc
* @return
*/
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
}
測試
驗證碼攔截的接口可配置
ImageCodeProperties加屬性url
private String url;
驗證碼匹配路徑以逗號隔開
demo application 中配置路徑
whale.security.code.image.url = /user,/user/*
攔截器處理
/**
* 繼承spring中的OncePerRequestFilter,確保每次請求調用一次過濾器
*/
//InitializingBean 實現此接口中的 afterPropertiesSet 初始化方法在其中初始化圖片驗證碼攔截路徑
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
private AuthenticationFailureHandler authenticationFailureHandler;
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
//驗證碼路徑,需要初始化
private Set<String> urls = new HashSet<>();
//配置
private SecurityProperties securityProperties;
//路徑正則匹配工具
private AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getImage().getUrl(),",");
for (String url : configUrls) {
urls.add(url);
}
//這個路徑是默認的
urls.add("/authentication/form");
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
System.out.println(httpServletRequest.getRequestURI());
System.out.println(httpServletRequest.getRequestURL());
//如果請求路徑滿足匹配模式 則需要驗證碼
boolean action = false;
for (String url : urls) {
if(pathMatcher.match(url,httpServletRequest.getRequestURI())){
action = true;
}
}
// if(StringUtils.equals("/authentication/form",httpServletRequest.getRequestURI())
// && StringUtils.equalsAnyIgnoreCase(httpServletRequest.getMethod(),"post")){
if(action){
try {
validate(new ServletWebRequest(httpServletRequest));
}catch (ValidateCodeException e){
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);
return;//失敗後直接返回,不再走下面的過濾器
}
}
//如果不是登錄請求,直接放行
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
if(StringUtils.isBlank(codeInRequest)){
throw new ValidateCodeException("驗證碼的值不能爲空");
}
if(codeInSession == null){
throw new ValidateCodeException("驗證碼不存在");
}
if(codeInSession.isExpried()){
sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);
throw new ValidateCodeException("驗證碼已過期");
}
if(! StringUtils.equals(codeInSession.getCode(),codeInRequest)){
throw new ValidateCodeException("驗證碼不匹配");
}
sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY);
}
public SecurityProperties getSecurityProperties() {
return securityProperties;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
}
BrowserSecurityConfig中配置攔截器
················
@Override
protected void configure(HttpSecurity http) throws Exception {
//http.formLogin() //指定身份認證的方式爲表單登錄
//http.httpBasic()
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(whaleAuthenctiationFailureHandler);//設置錯誤過濾器
validateCodeFilter.setSecurityProperties(securityProperties);
validateCodeFilter.afterPropertiesSet();
············
測試
驗證碼的生成邏輯可配置
創建驗證碼生成器接口及實現類
把com.whale.security.core.validate.ValidateCodeController#createImageCode中生成圖片驗證碼的邏輯搬到驗證碼生成器接口及實現類中
如下
ValidateCodeGenerator
public interface ValidateCodeGenerator {
ImageCode generate(ServletWebRequest request);
}
ImageCodeGenerator
public class ImageCodeGenerator 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 ImageCode generate(ServletWebRequest request) {
ValidateCodeController中調用驗證碼生成器接口
@RestController
public class ValidateCodeController implements Serializable {
public static final String SESSION_KEY ="SESSION_KEY_IMAGE_CODE";//key
//spring 操作session的工具類
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private SecurityProperties securityProperties;
@Autowired
private ValidateCodeGenerator imageCodeGenerator;
@RequestMapping("/code/image")
private void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
//1根據請求中的隨機數生成圖片
// ImageCode imageCode = createImageCode(request);
// ImageCode imageCode = createImageCode(new ServletWebRequest(request));
ImageCode imageCode = imageCodeGenerator.generate(new ServletWebRequest(request));
//2將隨機數放到session中
sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
//3將生成的圖片寫到接口的響應中
ImageIO.write(imageCode.getImage(),"jpeg",response.getOutputStream());
}
imageCodeGenerator 圖片生成器接口實現類的初始化和可配置
imageCodeGenerator是如何注入進去的呢
如下
@Configuration
public class ValidateCodeBeanConfig {
@Autowired
private SecurityProperties securityProperties;
@Bean
@ConditionalOnMissingBean(name = "imageCodeGenerator")
//爲啥這樣配置
//@ConditionalOnMissingBean(name = "imageCodeGenerator")spring初始化這個類之前會判斷容器中是否有名字爲imageCodeGenerator的bean,
//若果有就用已經初始化的bean,沒有的話才初始化當前bean
//這樣 這個接口就可被用戶覆蓋
public ValidateCodeGenerator imageCodeGenerator() { //方法的名字就是spring容器中bean的名字
ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
codeGenerator.setSecurityProperties(securityProperties);
return codeGenerator;
}
}
測試demo模塊中覆蓋驗證碼生成器
@Component("imageCodeGenerator")
public class DemoImageCodeGenerator implements ValidateCodeGenerator {
@Override
public ImageCode generate(ServletWebRequest request) {
System.out.println("更高級的圖形驗證碼生成代碼");
return null;
}
}
訪問報錯 ok
主要是設計思想
以增量的方式去適應變化
當需求邏輯發生變化時,我們不是改變原來得代碼,而是加一段代碼:很重要