<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在前面的幾篇文章中,登入時都是使用使用者名稱 + 密碼進行登入的,但是在實際專案當中,登入時,還需要輸入圖形驗證碼。那如何在 Spring Security 現有的認證體系中,加入自己的認證邏輯呢?這就是本文的內容,本文會介紹兩種實現方案,一是基於過濾器實現;二是基於認證器實現。
既然需要輸入圖形驗證碼,那先來生成驗證碼吧。
<!--驗證碼生成器--> <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"); // 文字個數 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"); // 文字內容 從設定字元中隨機抽取 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"); // 生成圖形校驗碼內容 String text = producer.createText(); // 將驗證碼內容存入HttpSession session.setAttribute("verify_code", text); // 生成圖形校驗碼圖片 BufferedImage image = producer.createImage(text); // 使用try-with-resources 方式,可以自動關閉流 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
類中處理,那我們就繼承這個類,增加對驗證碼的處理。當然也可以實現其他型別的過濾器,比如: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
。
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 方法中關於 form 表單的設定就會失效,那些失效的屬性,都可以在設定 VerifyCodeFilter 範例的時候設定;還需要記得設定AuthenticationManager
,否則啟動時會報錯。
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; /** * 登入失敗回撥 */ 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 = "賬戶被鎖定,請聯絡管理員!"; } else if (e instanceof BadCredentialsException) { msg = "使用者名稱或者密碼輸入錯誤,請重新輸入!"; } out.write(e.getMessage()); out.flush(); out.close(); } }
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; /** * 登入成功回撥 */ 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(); } }
輸入已經使用過的驗證碼
各位讀者是不是會覺得既然繼承了 Filter,那是不是每個介面都會進入到我們的自定義方法中呀!如果是繼承了 GenericFilterBean、OncePerRequestFilter 那是肯定會的,需要手動處理。 但我們繼承的是 UsernamePasswordAuthenticationFilter,security 已經幫忙處理了。處理邏輯在其父類別 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 中統一管理的,所以接下來我們就要自己提供 ProviderManager,然後注入自定義的 VerifyCodeAuthenticationProvider。
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 保護 ; } }
不傳入驗證碼發起請求。
以上就是Spring Security 登入時新增圖形驗證碼實現範例的詳細內容,更多關於Spring Security 登入圖形驗證碼的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45