javascript
SpringBoot 整合Security——自定义表单登录
文章目錄
- 一、添加驗證碼
- 1.1 驗證servlet
- 1.2 修改 login.html
- 1.3 添加匿名訪問 Url
- 二、AJAX 驗證
- 三、過濾器驗證
- 3.1 編寫驗證碼過濾器
- 3.2 注入過濾器
- 3.3 運行程序
- 四、Spring Security 驗證
- 4.1 WebAuthenticationDetails
- 4.2 AuthenticationDetailsSource
- 4.3 AuthenticationProvider
- 4.4 運行程序
通過前面三篇文章,你應該大致了解了 Spring Security 的流程。你應該發現了,真正的 login 請求是由 Spring Security 幫我們處理的,那么我們如何實現自定義表單登錄呢,比如添加一個驗證碼…
一、添加驗證碼
1.1 驗證servlet
public class VerifyServlet extends HttpServlet {/*** 驗證碼圖片的寬度。*/private int width = 100;/*** 驗證碼圖片的高度。*/private int height = 30;/*** 驗證碼字符個數*/private int codeCount = 4;/*** 字體高度*/private int fontHeight;/*** 干擾線數量*/private int interLine = 16;/*** 第一個字符的x軸值,因為后面的字符坐標依次遞增,所以它們的x軸值是codeX的倍數*/private int codeX;/*** codeY ,驗證字符的y軸值,因為并行所以值一樣*/private int codeY;/*** codeSequence 表示字符允許出現的序列值*/char[] codeSequence = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J','K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W','X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };/*** 初始化驗證圖片屬性*/@Overridepublic void init() throws ServletException {// 從web.xml中獲取初始信息// 寬度String strWidth = this.getInitParameter("width");// 高度String strHeight = this.getInitParameter("height");// 字符個數String strCodeCount = this.getInitParameter("codeCount");// 將配置的信息轉換成數值try {if (strWidth != null && strWidth.length() != 0) {width = Integer.parseInt(strWidth);}if (strHeight != null && strHeight.length() != 0) {height = Integer.parseInt(strHeight);}if (strCodeCount != null && strCodeCount.length() != 0) {codeCount = Integer.parseInt(strCodeCount);}} catch (NumberFormatException e) {e.printStackTrace();}//width-4 除去左右多余的位置,使驗證碼更加集中顯示,減得越多越集中。//codeCount+1 //等比分配顯示的寬度,包括左右兩邊的空格codeX = (width-4) / (codeCount+1);//height - 10 集中顯示驗證碼fontHeight = height - 10;codeY = height - 7;}/*** @param request* @param response* @throws ServletException* @throws java.io.IOException*/@Overrideprotected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, java.io.IOException {// 定義圖像bufferBufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);Graphics2D gd = buffImg.createGraphics();// 創建一個隨機數生成器類Random random = new Random();// 將圖像填充為白色gd.setColor(Color.LIGHT_GRAY);gd.fillRect(0, 0, width, height);// 創建字體,字體的大小應該根據圖片的高度來定。Font font = new Font("Times New Roman", Font.PLAIN, fontHeight);// 設置字體。gd.setFont(font);// 畫邊框。gd.setColor(Color.BLACK);gd.drawRect(0, 0, width - 1, height - 1);// 隨機產生16條干擾線,使圖象中的認證碼不易被其它程序探測到。gd.setColor(Color.gray);for (int i = 0; i < interLine; i++) {int x = random.nextInt(width);int y = random.nextInt(height);int xl = random.nextInt(12);int yl = random.nextInt(12);gd.drawLine(x, y, x + xl, y + yl);}// randomCode用于保存隨機產生的驗證碼,以便用戶登錄后進行驗證。StringBuffer randomCode = new StringBuffer();int red = 0, green = 0, blue = 0;// 隨機產生codeCount數字的驗證碼。for (int i = 0; i < codeCount; i++) {// 得到隨機產生的驗證碼數字。String strRand = String.valueOf(codeSequence[random.nextInt(36)]);// 產生隨機的顏色分量來構造顏色值,這樣輸出的每位數字的顏色值都將不同。red = random.nextInt(255);green = random.nextInt(255);blue = random.nextInt(255);// 用隨機產生的顏色將驗證碼繪制到圖像中。gd.setColor(new Color(red,green,blue));gd.drawString(strRand, (i + 1) * codeX, codeY);// 將產生的四個隨機數組合在一起。randomCode.append(strRand);}// 將四位數字的驗證碼保存到Session中。HttpSession session = request.getSession();session.setAttribute("validateCode", randomCode.toString());// 禁止圖像緩存。response.setHeader("Pragma", "no-cache");response.setHeader("Cache-Control", "no-cache");response.setDateHeader("Expires", 0);response.setContentType("image/jpeg");// 將圖像輸出到Servlet輸出流中。ServletOutputStream sos = response.getOutputStream();ImageIO.write(buffImg, "jpeg", sos);sos.close();}}然后在 Application 中注入該 Servlet:
@SpringBootApplication public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}@Beanpublic ServletRegistrationBean servletRegistrationBean() {ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new VerifyServlet());servletRegistrationBean.addUrlMappings("/getVerifyCode");return servletRegistrationBean;} }1.2 修改 login.html
在原本的 Login 頁面基礎上加上驗證碼字段
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>登陸</title> </head> <body> <h1>登陸</h1> <form method="post" action="/login"><div>用戶名:<input type="text" name="username"></div><div>密碼:<input type="password" name="password"></div><div><input type="text" class="form-control" name="verifyCode" required="required" placeholder="驗證碼" /><img src="getVerifyCode" title="看不清,請點我" onclick="refresh(this)" onmouseover="mouseover(this)" /></div><div><label><input type="checkbox" name="remember-me"/>自動登錄</label><button type="submit">立即登陸</button></div> </form> <script>function refresh(obj) { obj.src = "getVerifyCode?" + Math.random(); }function mouseover(obj) { obj.style.cursor = "pointer"; } </script> </body> </html>1.3 添加匿名訪問 Url
在 WebSecurityConfig 中允許該 Url 的匿名訪問,不然沒有登錄是沒有辦法訪問的:
@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()// 如果有允許匿名的url,填在下面.antMatchers("/getVerifyCode").permitAll().anyRequest().authenticated().and().formLogin().loginPage("/login")// 設置登陸成功頁.defaultSuccessUrl("/").permitAll().failureHandler(new SimpleUrlAuthenticationFailureHandler())//登錄失敗url.failureForwardUrl("/login/error").authenticationDetailsSource(authenticationDetailsSource).and()// 自定義登陸用戶名和密碼參數,默認為username和password// .usernameParameter("username")// .passwordParameter("password").logout().permitAll()//基于內存自動登錄.and().rememberMe().tokenRepository(persistentTokenRepository()).tokenValiditySeconds(60) ;http.csrf().disable();}這樣驗證碼就加好了,運行下程序:
下面才算是這篇文章真正的部分。我們如何才能實現驗證碼驗證呢,思考一下,應該有以下幾種實現方式:
二、AJAX 驗證
使用 AJAX 方式驗證和我們 Spring Security 框架就沒有任何關系了,其實就是表單提交前先發個 HTTP 請求驗證驗證碼,本篇不再贅述。
三、過濾器驗證
使用過濾器的思路是:在 Spring Security 處理登錄驗證請求前,驗證驗證碼,如果正確,放行;如果不正確,調到異常。
3.1 編寫驗證碼過濾器
自定義一個過濾器,實現 OncePerRequestFilter (該 Filter 保證每次請求一定會過濾),在 isProtectedUrl() 方法中攔截了 POST 方式的 /login 請求。
在邏輯處理中從 request 中取出驗證碼,并進行驗證,如果驗證成功,放行;驗證失敗,手動生成異常。
//獲取當前線程綁定的request對象
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String validateCode = ((String)request.getSession().getAttribute(“validateCode”)).toUpperCase();
3.2 注入過濾器
修改 WebSecurityConfig 的 configure 方法,添加一個 addFilterBefore() ,具有兩個參數,作用是在參數二之前執行參數一設置的過濾器。
Spring Security 對于用戶名/密碼登錄方式是通過 UsernamePasswordAuthenticationFilter 處理的,我們在它之前執行驗證碼過濾器即可。
@Override protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()// 如果有允許匿名的url,填在下面.antMatchers("/getVerifyCode").permitAll().anyRequest().authenticated().and()// 設置登陸頁.formLogin().loginPage("/login")// 設置登陸成功頁.defaultSuccessUrl("/").permitAll()// 登錄失敗Url.failureUrl("/login/error")// 自定義登陸用戶名和密碼參數,默認為username和password // .usernameParameter("username") // .passwordParameter("password").and().addFilterBefore(new VerifyFilter(),UsernamePasswordAuthenticationFilter.class).logout().permitAll()// 自動登錄.and().rememberMe().tokenRepository(persistentTokenRepository())// 有效時間:單位s.tokenValiditySeconds(60).userDetailsService(userDetailsService);// 關閉CSRF跨域http.csrf().disable(); }3.3 運行程序
四、Spring Security 驗證
使用過濾器就已經實現了驗證碼功能,但其實它和 AJAX 驗證差別不大。
- AJAX 是在提交前發一個請求,請求返回成功就提交,否則不提交
- 過濾器是先驗證驗證碼,驗證成功就讓 Spring Security 驗證用戶名和密碼;驗證失敗,則產生異常·。
如果我們要做的需求是用戶登錄是需要多個驗證字段,不單單是用戶名和密碼,那么使用過濾器會讓邏輯變得復雜,這時候可以考慮自定義 Spring Security 的驗證邏輯了…
4.1 WebAuthenticationDetails
我們知道 Spring security 默認只會處理用戶名和密碼信息。這時候就要請出我們的主角——WebAuthenticationDetails
WebAuthenticationDetails: 該類提供了獲取用戶登錄時攜帶的額外信息的功能,默認提供了 remoteAddress 與 sessionId 信息。
我們需要實現自定義的 WebAuthenticationDetails,并在其中加入我們的驗證碼:
/*** 獲取用戶登錄時攜帶的額外信息* @author shuliangzhao* @Title: CustomWebAuthenticationDetails* @ProjectName spring-boot-learn* @Description: TODO* @date 2019/8/4 17:14*/ public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {private final String verifyCode;public CustomWebAuthenticationDetails(HttpServletRequest request) {super(request);// verifyCode為頁面中驗證碼的nameverifyCode = request.getParameter("verifyCode");}public String getVerifyCode() {return this.verifyCode;}@Overridepublic String toString() {StringBuilder sb = new StringBuilder();sb.append(super.toString()).append("; VerifyCode: ").append(this.getVerifyCode());return sb.toString();} }在這個方法中,我們將前臺 form 表單中的 verifyCode 獲取到,并通過 get 方法方便被調用。
4.2 AuthenticationDetailsSource
自定義了WebAuthenticationDetails,我i們還需要將其放入 AuthenticationDetailsSource 中來替換原本的 WebAuthenticationDetails ,因此還得實現自定義 AuthenticationDetailsSource :
/*** 該接口用于在Spring Security登錄過程中對用戶的登錄信息的詳細信息進行填充* @author shuliangzhao* @Title: CustomAuthenticationDetailsSource* @ProjectName spring-boot-learn* @Description: TODO* @date 2019/8/4 17:17*/ @Component("authenticationDetailsSource") public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {@Overridepublic WebAuthenticationDetails buildDetails(HttpServletRequest request) {return new CustomWebAuthenticationDetails(request);} }該類內容將原本的 WebAuthenticationDetails 替換為了我們的 CustomWebAuthenticationDetails。
然后我們將 CustomAuthenticationDetailsSource 注入Spring Security中,替換掉默認的 AuthenticationDetailsSource。
修改 WebSecurityConfig,將其注入,然后在config()中使用 authenticationDetailsSource(authenticationDetailsSource)方法來指定它。
private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource;@Override protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()// 如果有允許匿名的url,填在下面.antMatchers("/getVerifyCode").permitAll().anyRequest().authenticated().and()// 設置登陸頁.formLogin().loginPage("/login")// 設置登陸成功頁.defaultSuccessUrl("/").permitAll()// 登錄失敗Url.failureUrl("/login/error")// 自定義登陸用戶名和密碼參數,默認為username和password // .usernameParameter("username") // .passwordParameter("password")// 指定authenticationDetailsSource.authenticationDetailsSource(authenticationDetailsSource).and().logout().permitAll()// 自動登錄.and().rememberMe().tokenRepository(persistentTokenRepository())// 有效時間:單位s.tokenValiditySeconds(60).userDetailsService(userDetailsService);// 關閉CSRF跨域http.csrf().disable();4.3 AuthenticationProvider
至此我們通過自定義WebAuthenticationDetails和AuthenticationDetailsSource將驗證碼和用戶名、密碼一起帶入了Spring Security中,下面我們需要將它取出來。
這里需要我們自定義AuthenticationProvider,需要注意的是,如果是我們自己實現AuthenticationProvider,那么我們就需要自己做密碼校驗了
/*** @author shuliangzhao* @Title: CustomAuthenticationProvider* @ProjectName spring-boot-learn* @Description: TODO* @date 2019/8/4 17:20*/ @Component public class CustomAuthenticationProvider implements AuthenticationProvider {@Autowiredprivate CustomUserDetailsService userDetailsService;@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {// 獲取用戶輸入的用戶名和密碼String name = authentication.getName();String inputPassword = authentication.getCredentials().toString();CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails();String verifyCode = details.getVerifyCode();if(!validateVerify(verifyCode)) {throw new DisabledException("驗證碼輸入錯誤");}//userDetails為數據庫中查詢到的用戶信息UserDetails userDetails = userDetailsService.loadUserByUsername(name);if (!userDetails.getPassword().equals(inputPassword)) {throw new BadCredentialsException("密碼錯誤");}return new UsernamePasswordAuthenticationToken(name, inputPassword, userDetails.getAuthorities());}private boolean validateVerify(String verifyCode) {//獲取當前線程綁定的request對象HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String validateCode = ((String)request.getSession().getAttribute("validateCode")).toUpperCase();verifyCode = verifyCode.toUpperCase();System.out.println("驗證碼:" + validateCode + "用戶輸入:" + verifyCode);return validateCode.equals(verifyCode);}@Overridepublic boolean supports(Class<?> authentication) {// 這里不要忘記,和UsernamePasswordAuthenticationToken比較return authentication.equals(UsernamePasswordAuthenticationToken.class);}}最后在 WebSecurityConfig 中將其注入,并在 config 方法中通過 auth.authenticationProvider() 指定使用。
@Autowiredprivate CustomAuthenticationProvider customAuthenticationProvider;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.authenticationProvider(customAuthenticationProvider);}4.4 運行程序
是不是比較復雜,為了實現該需求自定義了 WebAuthenticationDetails、AuthenticationDetailsSource、AuthenticationProvider,讓我們運行一下程序,當輸入錯誤驗證碼時:
總結
以上是生活随笔為你收集整理的SpringBoot 整合Security——自定义表单登录的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SpringBoot整合Spring S
- 下一篇: gradle idea java ssm