1、實現圖片驗證碼
1.1、創建獲取圖片驗證碼的 controller
1.2、編寫用於校驗圖片驗證碼的過濾器
1.3、將圖片驗證碼過濾器添加在 UsernamePasswordAuthenticationFilter 之前
1.4、修改表單登陸頁
1.5、測試
2、自動登陸(記住我)
2.1、散列加密方案
2.2、持久化令牌方案
1、實現圖片驗證碼 <--返回目錄
1.1、創建獲取圖片驗證碼的 controller <--返回目錄
要想實現圖片驗證碼,首先需要一個用於獲取圖片驗證碼的 API。這里使用 kaptchar 勿用於生產)

<dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> </dependency>
配置一個 kaptcha 實例:

@Bean public Producer imageCode() { // 配置圖形驗證碼的基本參數 Properties properties = new Properties(); properties.setProperty("kaptcha.image.width", "150");//圖片寬度 properties.setProperty("kaptcha.image.height", "50");//圖片高度 properties.setProperty("kaptcha.textproducer.char.string", "0123456789");//字符集 properties.setProperty("kaptcha.textproducer.char.length", "4");//字符長度 Config config = new Config(properties); DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); defaultKaptcha.setConfig(config); return defaultKaptcha; }
創建 ValidateCodeController,用於生成圖片驗證碼

package com.oy.validate; import java.awt.image.BufferedImage; import javax.imageio.ImageIO; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import com.google.code.kaptcha.Producer; @Controller public class ValidateCodeController { private static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE"; @Autowired private Producer kaptchaProducer; @GetMapping("/code/image") public void createCode(HttpServletRequest request, HttpServletResponse response) throws Exception { response.setContentType("image/jpeg"); // 創建驗證碼文本 String codeText = kaptchaProducer.createText(); // 將驗證碼文本設置到 session request.getSession().setAttribute(SESSION_KEY, codeText); // 根據文本創建圖片 BufferedImage bi = kaptchaProducer.createImage(codeText); // 獲取響應輸出流 ServletOutputStream out = response.getOutputStream(); ImageIO.write(bi, "jpg", out); // 推送並關閉響應輸出流 try { out.flush(); } finally { out.close(); } } }
訪問圖片驗證碼(路徑 "/code/image")時不設置權限,在 JavaConfig 配置類中配置
antMatchers("/app/api/**", "/mylogin.html", "/code/image") .permitAll() // 公開權限
訪問 http://localhost:8089/BootDemo/code/image,即可看到返回一張圖片驗證碼。
1.2、編寫用於校驗圖片驗證碼的過濾器 <--返回目錄
雖然 Spring Security 的過濾器對過濾器沒有特殊要求,只要繼承 Filter 即可,但是在 Spring 體系中,推薦使用 OncePerRequestFilter 來實現,它可以確保一次請求只會通過一次該過濾器(Filter 實際上並不能保證這一點)。
ValidateCodeFIlter

package com.oy.validate; import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; public class ValidateCodeFIlter extends OncePerRequestFilter { private static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE"; private AuthenticationFailureHandler myAuthenticationFailureHandler; public void setMyAuthenticationFailureHandler(AuthenticationFailureHandler myAuthenticationFailureHandler) { this.myAuthenticationFailureHandler = myAuthenticationFailureHandler; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { System.out.println("ValidateCodeFIlter start, 請求uri:" + request.getRequestURI() + ", servletPath:" + request.getServletPath()); // 非登陸請求不校驗驗證碼 if (!"/auth/form".equals(request.getServletPath())) { filterChain.doFilter(request, response); return; } try { doValidateCode(request); filterChain.doFilter(request, response); } catch(ValidateCodeException e) { myAuthenticationFailureHandler.onAuthenticationFailure(request, response, e); } System.out.println("ValidateCodeFIlter end..."); } private void doValidateCode(HttpServletRequest request) { String requestCode = request.getParameter("image_code"); HttpSession session = request.getSession(); String sessionCode = (String) session.getAttribute(SESSION_KEY); System.out.println("ValidateCodeFIlter, requestCode:" + requestCode + " sessionCode:" + sessionCode); if (!StringUtils.isEmpty(sessionCode)) { // 隨手清除 session 中驗證碼,無論驗證成功還是失敗 session.removeAttribute(SESSION_KEY); } // 校驗不通過,拋出異常 if (StringUtils.isEmpty(requestCode)) { throw new ValidateCodeException("驗證碼輸入為空"); } if (StringUtils.isEmpty(sessionCode)) { throw new ValidateCodeException("驗證碼為空"); } if (!requestCode.equals(sessionCode)) { throw new ValidateCodeException("驗證碼輸入錯誤"); } // 沒有異常,表示校驗通過 } }
ValidateCodeException

package com.oy.validate; import org.springframework.security.core.AuthenticationException; public class ValidateCodeException extends AuthenticationException { private static final long serialVersionUID = 8369364787664640677L; public ValidateCodeException(String msg) { super(msg); } }
MyAuthenticationFailureHandler

package com.oy.security; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.stereotype.Component; @Component public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { //@Autowired //private ObjectMapper objectMapper; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { System.out.println("登錄失敗," + exception.getMessage()); super.onAuthenticationFailure(request, response, exception); /* * 根據配置項來確定返回 json 還是 按照 Spring Securiy 原來默認進行跳轉 if (LoginResponseType.JSON.equals(securityProperties.getBrowser().getLoginType())) { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage())); } else { super.onAuthenticationFailure(request, response, exception); } */ } }
MyAuthenticationSuccessHandler

package com.oy.security; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.stereotype.Component; @Component public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { //@Autowired //private ObjectMapper objectMapper; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { System.out.println("登錄成功"); super.onAuthenticationSuccess(request, response, authentication); /* if (LoginResponseType.JSON.equals(securityProperties.getBrowser().getLoginType())) { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(authentication)); } else { super.onAuthenticationSuccess(request, response, authentication); } */ } }
1.3、將圖片驗證碼過濾器添加在 UsernamePasswordAuthenticationFilter 之前 <--返回目錄
WebSecurityConfig
package com.oy; import java.io.IOException; import java.util.Properties; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import com.google.code.kaptcha.Producer; import com.google.code.kaptcha.impl.DefaultKaptcha; import com.google.code.kaptcha.util.Config; import com.oy.validate.ValidateCodeFIlter; @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationFailureHandler myAuthenticationFailureHandler; @Autowired private AuthenticationSuccessHandler myAuthenticationSuccessHandler; @Override protected void configure(HttpSecurity http) throws Exception { ValidateCodeFIlter validateCodeFIlter = new ValidateCodeFIlter(); validateCodeFIlter.setMyAuthenticationFailureHandler(myAuthenticationFailureHandler); // 將圖片驗證碼過濾器添加在 UsernamePasswordAuthenticationFilter 之前 http.addFilterBefore(validateCodeFIlter, UsernamePasswordAuthenticationFilter.class) .formLogin() .loginPage("/mylogin.html") // 指定登陸頁 .loginProcessingUrl("/auth/form") // 指定處理登陸請求的路徑 .successHandler(myAuthenticationSuccessHandler)// 指定登陸成功時的處理邏輯 .failureHandler(myAuthenticationFailureHandler)// 指定登陸失敗時的處理邏輯 .and() .authorizeRequests() .antMatchers("/admin/api/**").hasRole("admin") .antMatchers("/user/api/**").hasRole("user") // 登陸頁、驗證碼公開權限 .antMatchers("/app/api/**", "/mylogin.html", "/code/image") .permitAll() // 公開權限 .anyRequest().authenticated() .and() .csrf().disable(); } @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Bean public Producer imageCode() { // 配置圖形驗證碼的基本參數 Properties properties = new Properties(); properties.setProperty("kaptcha.image.width", "150");//圖片寬度 properties.setProperty("kaptcha.image.height", "50");//圖片高度 properties.setProperty("kaptcha.textproducer.char.string", "0123456789");//字符集 properties.setProperty("kaptcha.textproducer.char.length", "4");//字符長度 Config config = new Config(properties); DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); defaultKaptcha.setConfig(config); return defaultKaptcha; } }
1.4、修改表單登陸頁 <--返回目錄
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h2>自定義表單登陸頁</h2> <form action="auth/form" method="post"> 用戶名:<input type="text" name="username" /><br/> 密 碼:<input type="text" name="password" /><br/> 驗證碼:<input type="text" name="image_code" /><br/> <img src="code/image" alt="imagecode" height="50px" width="150px" /><br/> <input type="submit" value="提交" /> </form> </body> </html>
1.5、測試 <--返回目錄
啟動項目,訪問 http://localhost:8089/BootDemo/admin/api/1, 控制台打印結果
// 訪問:http://localhost:8089/BootDemo/admin/api/1 ValidateCodeFIlter start, 請求uri:/BootDemo/admin/api/1, servletPath:/admin/api/1 ValidateCodeFIlter start, 請求uri:/BootDemo/mylogin.html, servletPath:/mylogin.html ValidateCodeFIlter start, 請求uri:/BootDemo/code/image, servletPath:/code/image // 使用 admin/123 登陸,驗證碼輸入錯誤 ValidateCodeFIlter start, 請求uri:/BootDemo/auth/form, servletPath:/auth/form ValidateCodeFIlter, requestCode:ff sessionCode:9657 登錄失敗,驗證碼輸入錯誤 ValidateCodeFIlter end... // 登陸失敗后,Spring Security 默認行為:跳轉到登陸頁面 ValidateCodeFIlter start, 請求uri:/BootDemo/mylogin.html, servletPath:/mylogin.html ValidateCodeFIlter start, 請求uri:/BootDemo/code/image, servletPath:/code/image
2、自動登陸(記住我) <--返回目錄
自動登陸時將用戶的登陸信息保存在客戶端瀏覽器的 cookie 中,當用戶下次訪問時,自動實現校驗並建立登陸狀態的一種機制。
Spring Security 提供了兩種令牌:
1)用散列算法加密用戶必要的登陸信息並生成令牌;
2)數據庫等持久化數據存儲機制用的持久化令牌;
2.1、散列加密方案 <--返回目錄
如下紅色字體的配置
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationFailureHandler myAuthenticationFailureHandler; @Autowired private AuthenticationSuccessHandler myAuthenticationSuccessHandler; @Autowired private UserDetailsService myUserDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { ValidateCodeFIlter validateCodeFIlter = new ValidateCodeFIlter(); validateCodeFIlter.setMyAuthenticationFailureHandler(myAuthenticationFailureHandler); // 將圖片驗證碼過濾器添加在 UsernamePasswordAuthenticationFilter 之前 http.addFilterBefore(validateCodeFIlter, UsernamePasswordAuthenticationFilter.class) .formLogin() .loginPage("/mylogin.html") // 指定登陸頁 .loginProcessingUrl("/auth/form") // 指定處理登陸請求的路徑 .successHandler(myAuthenticationSuccessHandler)// 指定登陸成功時的處理邏輯 .failureHandler(myAuthenticationFailureHandler)// 指定登陸失敗時的處理邏輯 .and() .rememberMe() .userDetailsService(myUserDetailsService) .key("rem_key") .and() .authorizeRequests() .antMatchers("/admin/api/**").hasRole("admin") .antMatchers("/user/api/**").hasRole("user") // 登陸頁、驗證碼公開權限 .antMatchers("/app/api/**", "/mylogin.html", "/code/image") .permitAll() // 公開權限 .anyRequest().authenticated() .and() .csrf().disable(); } // 省略 }
表單登陸頁,添加 <input type="checkbox" name="remember-me"/> 進行測試。啟動項目,訪問 http://localhost:8089/BootDemo/admin/api/1, 跳轉到登陸頁,使用 admin/123 登陸,勾選 “remember me” 復選框。登陸成功后,查看 cookie, 默認過期時間 2 星期。
將該 cookie 的 value 值進行 base64 解碼:
YWRtaW46MTU4ODEzMTE3ODE5MzplY2RlYWQxOGNhNzcxM2NjZTk2ZmRhZjM4NzI5YTk4YQ==
=== base64 解碼 ===>
admin:1588131178193:ecdead18ca7713cce96fdaf38729a98a
驗證最后那串 hash 字符串,可以看到打印結果符合預期(注:DigestUtils 是 commons-codec.commons-codec.1.14 提供)
public void demo() { String hash = DigestUtils.md5Hex("admin:1588131178193:123:rem_key"); System.out.println(hash);//ecdead18ca7713cce96fdaf38729a98a }
那么,remember-me 這個 cookie 的 value 值是根據什么規則生成的呢?
hashInfo = md5Hex(username + ":" + expirationTime + ":" + password + ":" + key)
rememberCookie = base64(username + ":" + expirationTime + ":" + hashInfo)
其中,expirationTime 是過期時間;key 是散列鹽值,用於防止令牌被修改(防止用戶自行修改,因為用戶是知道自己的用戶和密碼的,如果沒有這個 key,用戶可以自行修改 expirationTime 的值)。
通過這中方式生成 cookie 后,在下次登陸時,Spring Security 首先用 base64 解碼,得到用戶名、過期時間和加密散列值;然后使用用戶名得到密碼;接着重新以上面的散列算法正向計算,並將計算結果與從瀏覽器獲取的加密散列值進行對比,從而確定該令牌是否有效。
2.2、持久化令牌方案 <--返回目錄
持久化令牌方案的原理
Remember Me 過濾器位置
配置:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private DataSource dataSource; @Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); //tokenRepository.setCreateTableOnStartup(true);// 啟動時創建表,第二次啟動項目注釋掉 return tokenRepository; } @Autowired private UserDetailsService myUserDetailsService; @Autowired private AuthenticationFailureHandler myAuthenticationFailureHandler; @Autowired private AuthenticationSuccessHandler myAuthenticationSuccessHandler; @Override protected void configure(HttpSecurity http) throws Exception { ValidateCodeFIlter validateCodeFIlter = new ValidateCodeFIlter(); validateCodeFIlter.setMyAuthenticationFailureHandler(myAuthenticationFailureHandler); // 將圖片驗證碼過濾器添加在 UsernamePasswordAuthenticationFilter 之前 http.addFilterBefore(validateCodeFIlter, UsernamePasswordAuthenticationFilter.class) .formLogin() .loginPage("/mylogin.html") // 指定登陸頁 .loginProcessingUrl("/auth/form") // 指定處理登陸請求的路徑 .successHandler(myAuthenticationSuccessHandler)// 指定登陸成功時的處理邏輯 .failureHandler(myAuthenticationFailureHandler)// 指定登陸失敗時的處理邏輯 .and() .rememberMe() .tokenRepository(persistentTokenRepository()) // 持久化 token .tokenValiditySeconds(3600 * 24 * 7) // 過期時間, 單位秒 .userDetailsService(myUserDetailsService) // 使用該 UserDetailsService 校驗用戶 .key("rem_key") .and() .authorizeRequests() .antMatchers("/admin/api/**").hasRole("admin") .antMatchers("/user/api/**").hasRole("user") // 登陸頁、驗證碼公開權限 .antMatchers("/app/api/**", "/mylogin.html", "/code/image") .permitAll() // 公開權限 .anyRequest().authenticated() .and() .csrf().disable(); } // 省略 }
參考:
1)《Spring Security 實戰》-- 陳木鑫