Spring?Security實現(xiàn)添加圖片驗證功能
本章內(nèi)容
Spring security添加圖片驗證方式,在互聯(lián)網(wǎng)上面有很多這種博客,都寫的非常的詳細(xì)了。本篇主要講一些添加圖片驗證的思路。還有前后端分離方式,圖片驗證要怎么去處理?
- 圖片驗證的思路
- 簡單的demo
思路
小白: "我們從總體流程上看圖片驗證在認(rèn)證的哪一個階段?"
小黑: "在獲取客戶輸入的用戶名密碼那一階段,而且要在服務(wù)器獲取數(shù)據(jù)庫中用戶名密碼之前。這是一個區(qū)間[獲取請求用戶名密碼, 獲取數(shù)據(jù)庫用戶名密碼)
而在 Spring security中, 可以很明顯的發(fā)現(xiàn)有兩種思路。
- 第1種思路是在攔截登錄請求準(zhǔn)備認(rèn)證的那個過濾器。
- 第2種思路是在那個過濾器背后的認(rèn)證器。"
小白: "為什么是這個階段呢? 不能是在判斷密碼驗證之前呢?"
小黑: "你傻啊, 如果在你說的階段, 服務(wù)器需要去數(shù)據(jù)庫中獲取用戶信息, 這相當(dāng)?shù)睦速M系統(tǒng)資源"
小白: "哦哦, 我錯了, 讓我屢屢整個流程應(yīng)該是啥樣"
小白: "我需要事先在后端生成一個驗證碼,然后通過驗證碼返回一張圖片給前端。前端登錄表單添加圖片驗證。用戶輸入圖片驗證后點擊登錄,會存放在request
請求中, 后端需要從request
請求中讀取到圖片驗證,判斷前后端驗證碼是否相同, 如果圖片驗證碼相同之后才開始從數(shù)據(jù)庫拿用戶信息。否則直接拋出認(rèn)證異常"
簡單點: 數(shù)據(jù)庫獲取用戶賬戶之前, 先進(jìn)行圖片驗證碼驗證
方案
怎么將字符串變成圖片驗證碼?
這輪子肯定不能自己造, 有就拿來吧你
kaptcha
hutool
kaptcha
這么玩
<!--驗證碼生成器--> <dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> <exclusions> <exclusion> <artifactId>javax.servlet-api</artifactId> <groupId>javax.servlet</groupId> </exclusion> </exclusions> </dependency>
@Bean public DefaultKaptcha captchaProducer() { Properties properties = new Properties(); properties.put("kaptcha.border", "no"); properties.put("kaptcha.textproducer.char.length","4"); properties.put("kaptcha.image.height","50"); properties.put("kaptcha.image.width","150"); properties.put("kaptcha.obscurificator.impl","com.google.code.kaptcha.impl.ShadowGimpy"); properties.put("kaptcha.textproducer.font.color","black"); properties.put("kaptcha.textproducer.font.size","40"); properties.put("kaptcha.noise.impl","com.google.code.kaptcha.impl.NoNoise"); //properties.put("kaptcha.noise.impl","com.google.code.kaptcha.impl.DefaultNoise"); properties.put("kaptcha.textproducer.char.string","acdefhkmnprtwxy2345678"); DefaultKaptcha kaptcha = new DefaultKaptcha(); kaptcha.setConfig(new Config(properties)); return kaptcha; }
@Resource private DefaultKaptcha producer; @GetMapping("/verify-code") public void getVerifyCode(HttpServletResponse response, HttpSession session) throws Exception { response.setContentType("image/jpeg"); String text = producer.createText(); session.setAttribute("verify_code", text); BufferedImage image = producer.createImage(text); try (ServletOutputStream outputStream = response.getOutputStream()) { ImageIO.write(image, "jpeg", outputStream); } }
hutool
這么玩
@GetMapping("hutool-verify-code") public void getHtoolVerifyCode(HttpServletResponse response, HttpSession session) throws IOException { CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(200, 100, 4, 80); session.setAttribute("hutool_verify_code", circleCaptcha.getCode()); response.setContentType(MediaType.IMAGE_PNG_VALUE); circleCaptcha.write(response.getOutputStream()); }
這倆隨便挑選一個完事
前端就非常簡單了
<form th:action="@{/login}" method="post"> <div class="input"> <label for="name">用戶名</label> <input type="text" name="username" id="name"> <span class="spin"></span> </div> <div class="input"> <label for="pass">密碼</label> <input type="password" name="password" id="pass"> <span class="spin"></span> </div> <div class="input"> <label for="code">驗證碼</label> <input type="text" name="code" id="code"><img src="/verify-code" alt="驗證碼"> <!--<input type="text" name="code" id="code"><img src="/hutool-verify-code" alt="驗證碼">--> <span class="spin"></span> </div> <div class="button login"> <button type="submit"> <span>登錄</span> <i class="fa fa-check"></i> </button> </div> <div th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}"></div> </form>
傳統(tǒng)web項目
我們現(xiàn)在根據(jù)上面的思路來設(shè)計設(shè)計該怎么實現(xiàn)這項功能
過濾器方式
/** * 使用 OncePerRequestFilter 的方式需要配置匹配器 */ @RequiredArgsConstructor public class ValidateCodeFilter extends OncePerRequestFilter { private final String login; private static final AntPathRequestMatcher requiresAuthenticationRequestMatcher = new AntPathRequestMatcher(this.login, "POST"); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (requiresAuthenticationRequestMatcher.matches(request)) { validateCode(request); } filterChain.doFilter(request, response); } private void validateCode(HttpServletRequest request) { HttpSession session = request.getSession(); // 獲取保存在session中的code String verifyCode = (String) session.getAttribute("verify_code"); if (StringUtils.isBlank(verifyCode)) { throw new ValidateCodeException("請重新申請驗證碼!"); } // 拿到前端的 code String code = request.getParameter("code"); if (StringUtils.isBlank(code)) { throw new ValidateCodeException("驗證碼不能為空!"); } // 對比 if (!StringUtils.equalsIgnoreCase(code, verifyCode)) { throw new AuthenticationServiceException("驗證碼錯誤!"); } // 刪除掉 session 中的 verify_code session.removeAttribute("verify_code"); } }
雖然OncePerRequestFilter
每次瀏覽器請求過來, 都會調(diào)用過濾器. 但是過濾器順序是非常重要的
@Controller @Slf4j public class IndexController { @GetMapping("login") public String login() { return "login"; } @GetMapping("") @ResponseBody public Principal index(Principal principal) { return principal; } }
@Configuration public class SecurityConfig { public static final String[] MATCHERS_URLS = {"/verify-code", "/css/**", "/images/**", "/js/**", "/hutool-verify-code"}; public static final String LOGIN_PROCESSING_URL = "/login"; public static final String LOGIN_PAGE = "/login"; public static final String SUCCESS_URL = "/index"; @Bean public ValidateCodeFilter validateCodeFilter() { return new ValidateCodeFilter(LOGIN_PROCESSING_URL); } // @Bean // public WebSecurityCustomizer webSecurityCustomizer() { // return web -> web.ignoring() // .antMatchers("/js/**", "/css/**", "/images/**"); // } @Bean SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { httpSecurity .authorizeHttpRequests() .antMatchers(MATCHERS_URLS).permitAll() .anyRequest() .authenticated() .and() .formLogin() .loginPage(LOGIN_PAGE) .loginProcessingUrl(LOGIN_PROCESSING_URL) .defaultSuccessUrl(SUCCESS_URL, true) .permitAll() .and() .csrf() .disable(); httpSecurity.addFilterBefore(validateCodeFilter(), UsernamePasswordAuthenticationFilter.class); return httpSecurity.build(); } }
小白: "我在網(wǎng)上看到有些網(wǎng)友并不是繼承的OncePerRequestFilter
接口啊?"
小黑: "是的, 有一部分朋友選擇繼承UsernamePasswordAuthenticationFilter
"
小黑: "繼承這個過濾器的話, 我們需要配置很多東西, 比較麻煩"
小白: "為什么要有多余的配置?"
小黑: "你想想, 你自定義的過濾器繼承至UsernamePasswordAuthenticationFilter
, 自定義的過濾器和原先的過濾器是同時存在的"
小黑: "沒有為你自定義的過濾器配置對應(yīng)的Configurer
, 那么它里面啥也沒有全部屬性都是默認(rèn)值, 不說別的, 下面AuthenticationManager
至少要配置吧?"
小黑: "他可是沒有任何默認(rèn)值, 這樣會導(dǎo)致下面這行代碼報錯"
小黑: "當(dāng)然如果你有自定義屬于自己的Configurer
那沒話說, 比如FormLoginConfigurer
"
p>小黑: "默認(rèn)這個函數(shù)需要HttpSecurity
調(diào)用的, 我們自定義的Filter
并沒有重寫Configurer
這個環(huán)節(jié)"
小白: "哦, 我知道了, 那我就是要繼承至UsernamePasswordAuthenticationFilter
呢? 我要怎么做?"
小黑: "也行, 這樣就可以不用配置AntPathRequestMatcher
了"
public class VerifyCodeFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { HttpSession session = request.getSession(); String sessionVerifyCode = (String) session.getAttribute(Constants.VERIFY_CODE); String verifyCode = request.getParameter(Constants.VERIFY_CODE); if (StrUtil.isBlank(sessionVerifyCode) || StrUtil.isBlank(verifyCode) || !StrUtil.equalsIgnoreCase(sessionVerifyCode, verifyCode)) { throw new ValidateCodeException("圖片驗證碼錯誤, 請重新獲取"); } return super.attemptAuthentication(request, response); } }
@Bean public VerifyCodeFilter verifyCodeFilter() throws Exception { VerifyCodeFilter verifyCodeFilter = new VerifyCodeFilter(); verifyCodeFilter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager()); return verifyCodeFilter; }
小黑: "這樣就可以了"
小白: "也不麻煩啊"
小黑: "好吧, 好像是"
小白: "等等, 那SecurityFilterChain
呢? 特別是formLogin()
函數(shù)要怎么配置?"
httpSecurity.formLogin() .loginPage(loginPage) .loginProcessingUrl(loginUrl) .defaultSuccessUrl("/", true) .permitAll(); httpSecurity.addFilterBefore(verifyCodeFilter(), UsernamePasswordAuthenticationFilter.class);
小白: "那我前端表單用戶名和密碼的input
標(biāo)簽的name
屬性變成user
和pwd
了呢? 也在上面formLogin
上配置?"
小黑: "這里就有區(qū)別了, 明顯只能在VerifyCodeFilter Bean
上配置"
@Bean public VerifyCodeFilter verifyCodeFilter() throws Exception { VerifyCodeFilter verifyCodeFilter = new VerifyCodeFilter(); verifyCodeFilter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager()); verifyCodeFilter.setUsernameParameter("user"); verifyCodeFilter.setPasswordParameter("pwd"); return verifyCodeFilter; }
小白: "我還以為有多麻煩呢, 就這..."
小黑: "額, 主要是spring security的過濾器不能代替, 只能插入某個過濾器前后位置, 所以如果自定義過濾器就需要我們配置一些屬性"
認(rèn)證器方式
小白: "認(rèn)證器要怎么實現(xiàn)圖片驗證呢?"
小黑: "說到認(rèn)證的認(rèn)證器, 一定要想到DaoAuthenticationProvider
"
小黑: "很多人在基于認(rèn)證器實現(xiàn)圖片驗證時, 都重寫additionalAuthenticationChecks
, 這是不對的"
小白: "那應(yīng)該重寫哪個方法 ?"
小黑: "應(yīng)該重寫下面那個函數(shù)"
小白: "等一下, 你注意到這個方法的參數(shù)了么? 你這要怎么從request
中拿驗證碼?"
小黑: "有別的方法, 看源碼"
public class MyDaoAuthenticationProvider extends DaoAuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); assert requestAttributes != null; HttpServletRequest request = requestAttributes.getRequest(); String verifyCode = request.getParameter(Constants.VERIFY_CODE); String sessionVerifyCode = (String) request.getSession().getAttribute(Constants.VERIFY_CODE); if (StrUtil.isBlank(sessionVerifyCode) && StrUtil.isBlank(verifyCode) && !StrUtil.equalsIgnoreCase(sessionVerifyCode, verifyCode)) { throw new ValidateCodeException("圖片驗證碼錯誤, 請重新獲取"); } return super.authenticate(authentication); } }
小白: "哦, 我看到了, 沒想到還能這樣"
小白: "那你現(xiàn)在要怎么加入到Spring Security, 讓它代替掉原本的DaoAuthenticationProvider
呢?"
小黑: "這里有一個思路, 還記得AuthenticationManager
的父子關(guān)系吧, 你看到父親只有一個, 你看到兒子可以有幾個?"
小白: "好像是無數(shù)個, 那我是不是可以這么寫?"
/** * 往父類的 AuthenticationManager 里添加 authenticationProvider * 在源碼里面是這樣的AuthenticationProvider authenticationProvider = getBeanOrNull(AuthenticationProvider.class); * * @return * @throws Exception */ @Bean public MyDaoAuthenticationProvider authenticationProvider() throws Exception { MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD); authenticationProvider.setPasswordEncoder(passwordEncoder()); authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager()); return authenticationProvider; } // 往子類AuthenticationManager里面添加的 authenticationProvider httpSecurity.authenticationProvider(authenticationProvider());
小黑: "這上面的代碼有問題, AuthenticationManger
有父類和子類, 上面這段代碼同時往父類和子類都添加MyDaoAuthenticationProvider
, 這樣MyDaoAuthenticationProvider
會被執(zhí)行兩次, 但request的流只能執(zhí)行一次, 會報錯"
小黑: "我們可以這么玩"
@Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { // 代碼省略 // 代碼省略 // 代碼省略 // 代碼省略 // 往子類AuthenticationManager里面添加的 authenticationProvider, 但不能阻止 AuthenticationManger 父類加載 DaoAuthenticationProvider AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class); // 但是這種方式可以將 parent Manager 設(shè)置為 null, 所以是可以的 authenticationManagerBuilder.parentAuthenticationManager(null); MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD); authenticationProvider.setPasswordEncoder(passwordEncoder()); authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager()); authenticationManagerBuilder.authenticationProvider(authenticationProvider); http.authenticationManager(authenticationManagerBuilder.build()); return http.build(); }
小黑: "SecurityFilterChain
表示一個Filter
集合, 更直接點就是子類的AuthenticationManager
"
小黑: "所以這種玩法是給子類AuthenticationManager
添加Provider
, 但是它需要手動將parent
置為 null
, 否則父類的DaoAuthenticationProvider
還是會執(zhí)行, 最后報錯信息就不對了, 本來應(yīng)該是驗證碼錯誤, 將會變成用戶名和密碼錯誤"
小黑: "還有就是, 很多人很喜歡在舊版本像下面這么玩"
@Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD); authenticationProvider.setPasswordEncoder(passwordEncoder()); authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager()); return new ProviderManager(authenticationProvider); }
小黑: "在新版本也類似的這么搞, 但這樣是有區(qū)別的, 下面這種方式只會加入到spring Bean上下文, 但是不會加入到Spring Security中執(zhí)行, 他是無效的"
@Bean public ProviderManager providerManager() throws Exception { MyDaoAuthenticationProvider authenticationProvider = authenticationProvider(); return new ProviderManager(authenticationProvider); }
小黑: "在新版本中, 使用上面那段代碼是一點用都沒有"
public MyDaoAuthenticationProvider authenticationProvider() throws Exception { MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD); authenticationProvider.setPasswordEncoder(passwordEncoder()); authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager()); return authenticationProvider; } // 往子類AuthenticationManager里面添加的 authenticationProvider httpSecurity.authenticationProvider(authenticationProvider());
小黑: "上面這樣做也是不行, 他還是會存在兩個, 一個是MyDaoAuthenticationProvider
(子類), 另一個是DaoAuthenticationProvider
(父類)"
小白: "那最好的辦法是什么?"
小黑: "直接將MyDaoAuthenticationProvider
添加到Spring Bean上下文"
@Bean public MyDaoAuthenticationProvider authenticationProvider() throws Exception { MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD); authenticationProvider.setPasswordEncoder(passwordEncoder()); authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager()); return authenticationProvider; }
小白: "那還有別的思路么?"
小黑: "還有么? 不清楚了, 萬能網(wǎng)友應(yīng)該知道"
小白: "就這樣設(shè)置就行了? 其他還需不需要配置?"
小黑: "其他和過濾器方式一致"
總結(jié)下
@Bean public MyDaoAuthenticationProvider authenticationProvider() throws Exception { // 最好的辦法就是直接MyDaoAuthenticationProvider加入到Spring Bean里面就行了, 其他都不要 MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD); authenticationProvider.setPasswordEncoder(passwordEncoder()); authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager()); return authenticationProvider; }
和
@Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { // 代碼省略 // 代碼省略 // 代碼省略 // 代碼省略 // 往子類AuthenticationManager里面添加的 authenticationProvider, 但不能阻止 AuthenticationManger 父類加載 DaoAuthenticationProvider AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class); // 但是這種方式可以將 parent Manager 設(shè)置為 null, 所以是可以的 authenticationManagerBuilder.parentAuthenticationManager(null); MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD); authenticationProvider.setPasswordEncoder(passwordEncoder()); authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager()); authenticationManagerBuilder.authenticationProvider(authenticationProvider); http.authenticationManager(authenticationManagerBuilder.build()); return http.build(); }
都是可以的, 一個往父類的AuthenticationManager
添加MyDaoAuthenticationProvider
, 另一個往子類添加, 設(shè)置父類為null
前后端分離項目
小白: "前后端分離和傳統(tǒng)web項目的區(qū)別是什么?"
小黑: "請求request
和響應(yīng)response
都使用JSON
傳遞數(shù)據(jù)"
小白: "那我們分析源碼時只要關(guān)注 request
和 response
咯, 只要發(fā)現(xiàn)存在request的讀, 和 response的寫通通都要重寫一邊"
小黑: "是的, 其實很簡單, 無非是圖片驗證碼改用json
讀, 認(rèn)證時的讀取username
和password
也使用json
讀, 其次是出現(xiàn)異常需要響應(yīng)response
, 也改成json
寫, 認(rèn)證成功和失敗需要響應(yīng)到前端也改成json
寫"
小白: "哦, 那只要分析過源碼, 就能夠完成前后端分離功能了"
小黑: "所以還講源碼么? "
小白: "不用, 非常簡單"
基于過濾器方式
public class VerifyCodeFilter extends UsernamePasswordAuthenticationFilter { @Resource private ObjectMapper objectMapper; /** * 很多人這里同時支持前后端分離, 其實不對, 既然是前后端分離就徹底點 * 但為了跟上潮流, 我這里也搞前后端分離 * * @param request * @param response * @return * @throws AuthenticationException */ @SneakyThrows @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (!"POST".equals(request.getMethod())) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String contentType = request.getContentType(); HttpSession session = request.getSession(); if (MediaType.APPLICATION_JSON_VALUE.equals(contentType) || MediaType.APPLICATION_JSON_UTF8_VALUE.equals(contentType)) { Map map = objectMapper.readValue(request.getInputStream(), Map.class); imageJSONVerifyCode(session, map); String username = (String) map.get(this.getUsernameParameter()); username = (username != null) ? username.trim() : ""; String password = (String) map.get(this.getPasswordParameter()); password = (password != null) ? password : ""; UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } imageVerifyCode(request, session); return super.attemptAuthentication(request, response); } private void imageJSONVerifyCode(HttpSession session, Map map) throws ValidateCodeException { String verifyCode = (String) map.get(Constants.VERIFY_CODE); String code = (String) session.getAttribute(Constants.VERIFY_CODE); if (StrUtil.isBlank(verifyCode) || StrUtil.isBlank(code) || !StrUtil.equalsIgnoreCase(verifyCode, code)) { throw new ValidateCodeException("驗證碼錯誤, 請重新獲取驗證碼"); } } private void imageVerifyCode(HttpServletRequest request, HttpSession session) throws ValidateCodeException { String verifyCode = request.getParameter(Constants.VERIFY_CODE); String code = (String) session.getAttribute(Constants.VERIFY_CODE); if (StrUtil.isBlank(verifyCode) || StrUtil.isBlank(code) || !StrUtil.equalsIgnoreCase(verifyCode, code)) { throw new ValidateCodeException("驗證碼錯誤, 請重新獲取驗證碼"); } } }
小白: "為什么你要寫imageJSONVerifyCode
, imageVerifyCode
兩個函數(shù)? 寫一個不就行了?"
小黑: "額, 是的, 把參數(shù)改成兩個String verifyCode, String code
也行"
@Configuration public class SecurityConfig { @Resource private AuthenticationConfiguration authenticationConfiguration; @Bean PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } @Bean public ObjectMapper objectMapper() throws Exception { return new ObjectMapper(); } @Bean public VerifyCodeFilter verifyCodeFilter() throws Exception { VerifyCodeFilter verifyCodeFilter = new VerifyCodeFilter(); verifyCodeFilter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager()); verifyCodeFilter.setAuthenticationFailureHandler((request, response, exception) -> { HashMap<String, Object> map = new HashMap<>(); map.put("status", 401); map.put("msg", exception.getMessage()); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter().write(JSONUtil.toJsonStr(map)); }); verifyCodeFilter.setAuthenticationSuccessHandler((request, response, authentication) -> { HashMap<String, Object> map = new HashMap<>(); map.put("status", 200); map.put("msg", "登錄成功"); map.put("user", authentication); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter().write(JSONUtil.toJsonStr(map)); }); return verifyCodeFilter; } @Bean SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { httpSecurity .authorizeHttpRequests() .antMatchers(Constants.MATCHERS_LIST) .permitAll() .anyRequest() .authenticated() ; httpSecurity.formLogin() .loginPage(Constants.LOGIN_PAGE) .loginProcessingUrl(Constants.LOGIN_PROCESSING_URL) .defaultSuccessUrl(Constants.SUCCESS_URL, true) .permitAll(); httpSecurity.logout() .clearAuthentication(true) .invalidateHttpSession(true) .logoutSuccessHandler((request, response, authentication) -> { HashMap<String, Object> map = new HashMap<>(); map.put("status", 200); map.put("msg", "注銷成功"); map.put("user", authentication); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter().write(JSONUtil.toJsonStr(map)); }); httpSecurity.csrf() .disable(); httpSecurity.addFilterAt(verifyCodeFilter(), UsernamePasswordAuthenticationFilter.class); httpSecurity.exceptionHandling() .accessDeniedHandler((request, response, accessDeniedException) -> { HashMap<String, Object> map = new HashMap<>(); map.put("status", 401); map.put("msg", "您沒有權(quán)限, 拒絕訪問: " + accessDeniedException.getMessage()); // map.put("msg", "您沒有權(quán)限, 拒絕訪問"); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter().write(JSONUtil.toJsonStr(map)); }) .authenticationEntryPoint((request, response, authException) -> { HashMap<String, Object> map = new HashMap<>(); map.put("status", HttpStatus.UNAUTHORIZED.value()); map.put("msg", "認(rèn)證失敗, 請重新認(rèn)證: " + authException.getMessage()); // map.put("msg", "認(rèn)證失敗, 請重新認(rèn)證"); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter().write(JSONUtil.toJsonStr(map)); }); return httpSecurity.build(); } }
注意這兩行代碼, 教你怎么在不使用WebSecurityConfigurerAdapter
的情況下拿到AuthenticationManager
@RestController @Slf4j public class VerifyCodeController { @GetMapping("/verify-code") public void getVerifyCode(HttpServletResponse response, HttpSession session) throws Exception { GifCaptcha captcha = CaptchaUtil.createGifCaptcha(Constants.IMAGE_WIDTH, Constants.IMAGE_HEIGHT); RandomGenerator randomGenerator = new RandomGenerator(Constants.BASE_STR, Constants.RANDOM_LENGTH); captcha.setGenerator(randomGenerator); captcha.createCode(); String code = captcha.getCode(); session.setAttribute(Constants.VERIFY_CODE, code); ServletOutputStream outputStream = response.getOutputStream(); captcha.write(outputStream); outputStream.flush(); outputStream.close(); } }
@Controller @Slf4j public class IndexController { @GetMapping("login") public String login() { return "login"; } @GetMapping("") @ResponseBody public Principal myIndex(Principal principal) { return principal; } }
基于認(rèn)證器方式
public class MyDaoAuthenticationProvider extends DaoAuthenticationProvider { @Resource private ObjectMapper objectMapper; private final String loginUsername; private final String loginPassword; public MyDaoAuthenticationProvider(String loginUsername, String loginPassword) { this.loginUsername = loginUsername; this.loginPassword = loginPassword; } @SneakyThrows @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); assert requestAttributes != null; HttpServletRequest request = requestAttributes.getRequest(); String contentType = request.getContentType(); String verifyCode = (String) request.getSession().getAttribute(Constants.VERIFY_CODE); if (MediaType.APPLICATION_JSON_VALUE.equals(contentType) || MediaType.APPLICATION_JSON_UTF8_VALUE.equals(contentType)) { Map map = this.objectMapper.readValue(request.getInputStream(), Map.class); String code = (String) map.get(Constants.VERIFY_CODE); imageVerifyCode(verifyCode, code); String username = (String) map.get(loginUsername); String password = (String) map.get(loginPassword); UsernamePasswordAuthenticationToken authenticationToken = UsernamePasswordAuthenticationToken .unauthenticated(username, password); return super.authenticate(authenticationToken); } String code = request.getParameter(Constants.VERIFY_CODE); imageVerifyCode(verifyCode, code); return super.authenticate(authentication); } private void imageVerifyCode(String verifyCode, String code) throws ValidateCodeException { if (StrUtil.isBlank(verifyCode) || StrUtil.isBlank(code) || !StrUtil.equalsIgnoreCase(verifyCode, code)) { throw new ValidateCodeException("驗證碼錯誤, 請重新獲取驗證碼"); } } }
@Slf4j @Configuration public class SecurityConfig { private static final String NOOP_PASSWORD_PREFIX = "{noop}"; private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$"); @Resource private SecurityProperties properties; @Bean PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } @Bean public ObjectMapper objectMapper() { return new ObjectMapper(); } @Bean @Lazy public InMemoryUserDetailsManager inMemoryUserDetailsManager() { SecurityProperties.User user = properties.getUser(); List<String> roles = user.getRoles(); return new InMemoryUserDetailsManager( User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder())) .roles(StringUtils.toStringArray(roles)).build()); } private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) { String password = user.getPassword(); if (user.isPasswordGenerated()) { log.warn(String.format( "%n%nUsing generated security password: %s%n%nThis generated password is for development use only. " + "Your security configuration must be updated before running your application in " + "production.%n", user.getPassword())); } if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) { return password; } return NOOP_PASSWORD_PREFIX + password; } @Bean public MyDaoAuthenticationProvider authenticationProvider() throws Exception { MyDaoAuthenticationProvider authenticationProvider = new MyDaoAuthenticationProvider(Constants.LOGIN_USERNAME, Constants.LOGIN_PASSWORD); authenticationProvider.setPasswordEncoder(passwordEncoder()); authenticationProvider.setUserDetailsService(inMemoryUserDetailsManager()); return authenticationProvider; } @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests() .antMatchers(Constants.MATCHERS_LIST) .permitAll() .anyRequest() .authenticated() ; http.formLogin() .loginPage(Constants.LOGIN_PAGE) .loginProcessingUrl(Constants.LOGIN_PROCESSING_URL) .successHandler(new MyAuthenticationSuccessHandler()) .failureHandler(new MyAuthenticationFailureHandler()) .permitAll(); http.logout() .clearAuthentication(true) .invalidateHttpSession(true) .logoutSuccessHandler(new MyLogoutSuccessHandler()); http.csrf() .disable(); http.exceptionHandling(exceptionHandlingConfigurer -> { exceptionHandlingConfigurer.authenticationEntryPoint(new MyAuthenticationEntryPoint()); exceptionHandlingConfigurer.accessDeniedHandler(new MyAccessDeniedHandler()); }) ; return http.build(); } private static class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { HashMap<String, Object> map = new HashMap<>(); map.put("status", 200); map.put("msg", "認(rèn)證成功"); map.put("user_info", authentication.getPrincipal()); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter().write(JSONUtil.toJsonStr(map)); } } private static class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { log.error("認(rèn)證失敗", exception); exception.printStackTrace(); HashMap<String, Object> map = new HashMap<>(); map.put("status", 401); map.put("msg", "認(rèn)證失敗"); map.put("exception", exception.getMessage()); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter().write(JSONUtil.toJsonStr(map)); } } private static class MyAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { log.error("認(rèn)證失效", authException); HashMap<String, Object> map = new HashMap<>(); map.put("status", HttpStatus.UNAUTHORIZED.value()); map.put("msg", "認(rèn)證失敗, 請重新認(rèn)證: " + authException.getMessage()); // map.put("msg", "認(rèn)證失敗, 請重新認(rèn)證"); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter().write(JSONUtil.toJsonStr(map)); } } private static class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { log.error("沒有權(quán)限", accessDeniedException); HashMap<String, Object> map = new HashMap<>(); map.put("status", 401); map.put("msg", "您沒有權(quán)限, 拒絕訪問: " + accessDeniedException.getMessage()); // map.put("msg", "您沒有權(quán)限, 拒絕訪問"); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter().write(JSONUtil.toJsonStr(map)); } } private static class MyLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { HashMap<String, Object> map = new HashMap<>(); map.put("status", 200); map.put("msg", "注銷成功"); map.put("user", authentication); response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter().write(JSONUtil.toJsonStr(map)); } } }
以上就是Spring Security實現(xiàn)添加圖片驗證功能的詳細(xì)內(nèi)容,更多關(guān)于Spring Security添加圖片驗證的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
IDEA將Maven項目中指定文件夾下的xml等文件編譯進(jìn)classes的方法
這篇文章主要介紹了IDEA將Maven項目中指定文件夾下的xml等文件編譯進(jìn)classes的方法,幫助大家更好的利用IDEA進(jìn)行Java的開發(fā)學(xué)習(xí),感興趣的朋友可以了解下2021-01-01SpringBoot如何手寫一個starter并使用這個starter詳解
starter是SpringBoot中的一個新發(fā)明,它有效的降低了項目開發(fā)過程的復(fù)雜程度,對于簡化開發(fā)操作有著非常好的效果,下面這篇文章主要給大家介紹了關(guān)于SpringBoot如何手寫一個starter并使用這個starter的相關(guān)資料,需要的朋友可以參考下2022-12-12SpringBoot實現(xiàn)自定義條件注解的代碼示例
在Spring Boot中,條件注解是一種非常強(qiáng)大的工具,它可以根據(jù)特定的條件來選擇是否加載某個類或某個Bean,文將介紹如何在Spring Boot中實現(xiàn)自定義條件注解,并提供一個示例代碼,需要的朋友可以參考下2023-06-06java文件復(fù)制代碼片斷(java實現(xiàn)文件拷貝)
本文介紹java實現(xiàn)文件拷貝的代碼片斷,大家可以直接放到程序里運行2014-01-01KotlinScript構(gòu)建SpringBootStarter保姆級教程
這篇文章主要為大家介紹了KotlinScript構(gòu)建SpringBootStarter的保姆級教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09java.lang.NullPointerException異常問題解決方案
這篇文章主要介紹了java.lang.NullPointerException異常問題解決方案,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08java jni調(diào)用c函數(shù)實例分享(java調(diào)用c函數(shù))
Java代碼中調(diào)用C/C++代碼,當(dāng)然是使用JNI,JNI是Java native interface的簡寫,可以譯作Java原生接口,下面看實例吧2013-12-12