Spring?Security?登錄時添加圖形驗證碼實現(xiàn)實例
前言
在前面的幾篇文章中,登錄時都是使用用戶名 + 密碼進行登錄的,但是在實際項目當中,登錄時,還需要輸入圖形驗證碼。那如何在 Spring Security 現(xiàn)有的認證體系中,加入自己的認證邏輯呢?這就是本文的內(nèi)容,本文會介紹兩種實現(xiàn)方案,一是基于過濾器實現(xiàn);二是基于認證器實現(xiàn)。
驗證碼生成
既然需要輸入圖形驗證碼,那先來生成驗證碼吧。
加入驗證碼依賴
<!--驗證碼生成器--> <dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
Kaptcha 依賴是谷歌的驗證碼工具。
驗證碼配置
@Configuration public class KaptchaConfig { @Bean public DefaultKaptcha captchaProducer() { Properties properties = new Properties(); // 是否顯示邊框 properties.setProperty("kaptcha.border","yes"); // 邊框顏色 properties.setProperty("kaptcha.border.color","105,179,90"); // 字體顏色 properties.setProperty("kaptcha.textproducer.font.color","blue"); // 字體大小 properties.setProperty("kaptcha.textproducer.font.size","35"); // 圖片寬度 properties.setProperty("kaptcha.image.width","300"); // 圖片高度 properties.setProperty("kaptcha.image.height","100"); // 文字個數(shù) properties.setProperty("kaptcha.textproducer.char.length","4"); //文字大小 properties.setProperty("kaptcha.textproducer.font.size","100"); //文字隨機字體 properties.setProperty("kaptcha.textproducer.font.names", "宋體"); //文字距離 properties.setProperty("kaptcha.textproducer.char.space","16"); //干擾線顏色 properties.setProperty("kaptcha.noise.color","blue"); // 文本內(nèi)容 從設置字符中隨機抽取 properties.setProperty("kaptcha.textproducer.char.string","0123456789"); DefaultKaptcha kaptcha = new DefaultKaptcha(); kaptcha.setConfig(new Config(properties)); return kaptcha; } }
驗證碼接口
/** * 生成驗證碼 */ @GetMapping("/verify-code") public void getVerifyCode(HttpServletResponse resp, HttpSession session) throws IOException { resp.setContentType("image/jpeg"); // 生成圖形校驗碼內(nèi)容 String text = producer.createText(); // 將驗證碼內(nèi)容存入HttpSession session.setAttribute("verify_code", text); // 生成圖形校驗碼圖片 BufferedImage image = producer.createImage(text); // 使用try-with-resources 方式,可以自動關(guān)閉流 try(ServletOutputStream out = resp.getOutputStream()) { // 將校驗碼圖片信息輸出到瀏覽器 ImageIO.write(image, "jpeg", out); } }
代碼注釋寫的很清楚,就不過多的介紹。屬于固定的配置,既然配置完了,那就看看生成的效果吧!
接下來就看看如何集成到 Spring Security 中的認證邏輯吧!
加入依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
基于過濾器
編寫自定義認證邏輯
這里繼承的過濾器為 UsernamePasswordAuthenticationFilter
,并重寫attemptAuthentication
方法。用戶登錄的用戶名/密碼是在 UsernamePasswordAuthenticationFilter
類中處理,那我們就繼承這個類,增加對驗證碼的處理。當然也可以實現(xiàn)其他類型的過濾器,比如:GenericFilterBean
、OncePerRequestFilter
,不過處理起來會比繼承UsernamePasswordAuthenticationFilter
麻煩一點。
import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; public class VerifyCodeFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // 需要是 POST 請求 if (!request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } // 獲得請求驗證碼值 String code = request.getParameter("code"); HttpSession session = request.getSession(); // 獲得 session 中的 驗證碼值 String sessionVerifyCode = (String) session.getAttribute("verify_code"); if (StringUtils.isEmpty(code)){ throw new AuthenticationServiceException("驗證碼不能為空!"); } if(StringUtils.isEmpty(sessionVerifyCode)){ throw new AuthenticationServiceException("請重新申請驗證碼!"); } if (!sessionVerifyCode.equalsIgnoreCase(code)) { throw new AuthenticationServiceException("驗證碼錯誤!"); } // 驗證碼驗證成功,清除 session 中的驗證碼 session.removeAttribute("verify_code"); // 驗證碼驗證成功,走原本父類認證邏輯 return super.attemptAuthentication(request, response); } }
代碼邏輯很簡單,驗證驗證碼是否正確,正確則走父類原本邏輯,去驗證用戶名密碼是否正確。 過濾器定義完成后,接下來就是用我們自定義的過濾器代替默認的 UsernamePasswordAuthenticationFilter
。
- SecurityConfig
import cn.cxyxj.study04.Authentication.config.MyAuthenticationFailureHandler; import cn.cxyxj.study04.Authentication.config.MyAuthenticationSuccessHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Bean @Override protected UserDetailsService userDetailsService() { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User.withUsername("cxyxj").password("123").roles("admin").build()); manager.createUser(User.withUsername("security").password("security").roles("user").build()); return manager; } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { // 用自定義的 VerifyCodeFilter 實例代替 UsernamePasswordAuthenticationFilter http.addFilterBefore(new VerifyCodeFilter(), UsernamePasswordAuthenticationFilter.class); http.authorizeRequests() //開啟配置 // 驗證碼、登錄接口放行 .antMatchers("/verify-code","/auth/login").permitAll() .anyRequest() //其他請求 .authenticated().and()//驗證 表示其他請求需要登錄才能訪問 .csrf().disable(); // 禁用 csrf 保護 } @Bean VerifyCodeFilter loginFilter() throws Exception { VerifyCodeFilter verifyCodeFilter = new VerifyCodeFilter(); verifyCodeFilter.setFilterProcessesUrl("/auth/login"); verifyCodeFilter.setUsernameParameter("account"); verifyCodeFilter.setPasswordParameter("pwd"); verifyCodeFilter.setAuthenticationManager(authenticationManagerBean()); verifyCodeFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler()); verifyCodeFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler()); return verifyCodeFilter; } }
當我們替換了 UsernamePasswordAuthenticationFilter
之后,原本在 SecurityConfig#configure 方法中關(guān)于 form 表單的配置就會失效,那些失效的屬性,都可以在配置 VerifyCodeFilter 實例的時候配置;還需要記得配置AuthenticationManager
,否則啟動時會報錯。
- MyAuthenticationFailureHandler
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.LockedException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; /** * 登錄失敗回調(diào) */ public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); String msg = ""; if (e instanceof LockedException) { msg = "賬戶被鎖定,請聯(lián)系管理員!"; } else if (e instanceof BadCredentialsException) { msg = "用戶名或者密碼輸入錯誤,請重新輸入!"; } out.write(e.getMessage()); out.flush(); out.close(); } }
- MyAuthenticationSuccessHandler
import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; /** * 登錄成功回調(diào) */ public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { Object principal = authentication.getPrincipal(); response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); out.write(new ObjectMapper().writeValueAsString(principal)); out.flush(); out.close(); } }
測試
- 不傳入驗證碼發(fā)起請求。
- 請求獲取驗證碼接口
- 輸入錯誤的驗證碼
- 輸入正確的驗證碼
輸入已經(jīng)使用過的驗證碼
各位讀者是不是會覺得既然繼承了 Filter,那是不是每個接口都會進入到我們的自定義方法中呀!如果是繼承了 GenericFilterBean、OncePerRequestFilter 那是肯定會的,需要手動處理。 但我們繼承的是 UsernamePasswordAuthenticationFilter,security 已經(jīng)幫忙處理了。處理邏輯在其父類 AbstractAuthenticationProcessingFilter#doFilter 中。
基于認證器
編寫自定義認證邏輯
驗證碼邏輯編寫完成,那接下來就自定義一個 VerifyCodeAuthenticationProvider
繼承自 DaoAuthenticationProvider
,并重寫 authenticate
方法。
import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.util.StringUtils; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; /** * 驗證碼驗證器 */ public class VerifyCodeAuthenticationProvider extends DaoAuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); // 獲得請求驗證碼值 String code = req.getParameter("code"); // 獲得 session 中的 驗證碼值 HttpSession session = req.getSession(); String sessionVerifyCode = (String) session.getAttribute("verify_code"); if (StringUtils.isEmpty(code)){ throw new AuthenticationServiceException("驗證碼不能為空!"); } if(StringUtils.isEmpty(sessionVerifyCode)){ throw new AuthenticationServiceException("請重新申請驗證碼!"); } if (!code.toLowerCase().equals(sessionVerifyCode.toLowerCase())) { throw new AuthenticationServiceException("驗證碼錯誤!"); } // 驗證碼驗證成功,清除 session 中的驗證碼 session.removeAttribute("verify_code"); // 驗證碼驗證成功,走原本父類認證邏輯 return super.authenticate(authentication); } }
自定義的認證邏輯完成了,剩下的問題就是如何讓 security 走我們的認證邏輯了。
在 security 中,所有的 AuthenticationProvider 都是放在 ProviderManager 中統(tǒng)一管理的,所以接下來我們就要自己提供 ProviderManager,然后注入自定義的 VerifyCodeAuthenticationProvider。
- SecurityConfig
import cn.cxyxj.study02.config.MyAuthenticationFailureHandler; import cn.cxyxj.study02.config.MyAuthenticationSuccessHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Bean @Override protected UserDetailsService userDetailsService() { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User.withUsername("cxyxj").password("123").roles("admin").build()); manager.createUser(User.withUsername("security").password("security").roles("user").build()); return manager; } @Bean VerifyCodeAuthenticationProvider verifyCodeAuthenticationProvider() { VerifyCodeAuthenticationProvider provider = new VerifyCodeAuthenticationProvider(); provider.setPasswordEncoder(passwordEncoder()); provider.setUserDetailsService(userDetailsService()); return provider; } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { ProviderManager manager = new ProviderManager(verifyCodeAuthenticationProvider()); return manager; } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() //開啟配置 // 驗證碼接口放行 .antMatchers("/verify-code").permitAll() .anyRequest() //其他請求 .authenticated()//驗證 表示其他請求需要登錄才能訪問 .and() .formLogin() .loginPage("/login.html") //登錄頁面 .loginProcessingUrl("/auth/login") //登錄接口,此地址可以不真實存在 .usernameParameter("account") //用戶名字段 .passwordParameter("pwd") //密碼字段 .successHandler(new MyAuthenticationSuccessHandler()) .failureHandler(new MyAuthenticationFailureHandler()) .permitAll() // 上述 login.html 頁面、/auth/login接口放行 .and() .csrf().disable(); // 禁用 csrf 保護 ; } }
測試
不傳入驗證碼發(fā)起請求。
- 請求獲取驗證碼接口
- 輸入錯誤的驗證碼
- 輸入正確的驗證碼
- 輸入已經(jīng)使用過的驗證碼
以上就是Spring Security 登錄時添加圖形驗證碼實現(xiàn)實例的詳細內(nèi)容,更多關(guān)于Spring Security 登錄圖形驗證碼的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
spring boot2結(jié)合mybatis增刪改查的實現(xiàn)
這篇文章主要給大家介紹了關(guān)于spring boot2結(jié)合mybatis增刪改查的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家學習或者使用spring boot2具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧2019-09-09Java內(nèi)存溢出的幾個區(qū)域總結(jié)(注意避坑!)
內(nèi)存溢出是指應用系統(tǒng)中存在無法回收的內(nèi)存或使用的內(nèi)存過多,最終使得程序運行要用到的內(nèi)存大于虛擬機能提供的最大內(nèi)存,下面這篇文章主要給大家介紹了關(guān)于Java內(nèi)存溢出的幾個區(qū)域,總結(jié)出來給大家提醒注意避坑,需要的朋友可以參考下2022-11-11關(guān)于scanner.nextInt()等next()和scanner.nextIine()連用注意事項
這篇文章主要介紹了關(guān)于scanner.nextInt()等next()和scanner.nextIine()連用注意事項,具有很好的參考價值,希望對大家有所幫助。2023-04-04Spring?Boot?整合?Fisco?Bcos部署、調(diào)用區(qū)塊鏈合約的案例
本篇文章介紹?Spring?Boot?整合?Fisco?Bcos?的相關(guān)技術(shù),最最重要的技術(shù)點,部署、調(diào)用區(qū)塊鏈合約的工程案例,本文通過流程分析給大家介紹的非常詳細,需要的朋友參考下吧2022-01-01spring boot配置ssl實現(xiàn)HTTPS的方法
這篇文章主要介紹了spring boot配置ssl實現(xiàn)HTTPS的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-03-03