- 引入依賴:
<dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.5.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-config</artifactId> <version>1.1.6.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.10</version> </dependency>
- 創建驗證碼對象:
public class ImageCode { private BufferedImage image; private String code; private LocalDateTime expireTime; public ImageCode(BufferedImage image, String code, int expireIn) { this.image = image; this.code = code; this.expireTime = LocalDateTime.now().plusSeconds(expireIn); } //image驗證碼圖片,code驗證碼和expireTime過時時間 public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) { this.image = image; this.code = code; this.expireTime = expireTime; } boolean isExpire() { return LocalDateTime.now().isAfter(expireTime); } // get,set 略 }
注: 此處儘可能使用getter/setter代碼,不推薦lombok(偶有bug),建議使用快捷鍵生成便可html
- 生成驗證碼的控制器:
@RestController public class ValidateController { public final static String SESSION_KEY_IMAGE_CODE = "SESSION_KEY"; //HttpSessionSessionStrategy對象封裝了一些處理Session的方法 若是不想使用spring-social-config 可使用HttpSession或者Redis等代替 //代表將驗證碼存儲,用於登陸時驗證碼的校驗 private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @GetMapping("/code/image") public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException { ImageCode imageCode = createImageCode(); //存儲驗證碼 sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode); //將圖片寫入流中返回頁面 ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream()); } // 生成驗證碼對象 private ImageCode createImageCode() { // 能夠自定義 int width = 90; // 驗證碼圖片寬度 int height = 36; // 驗證碼圖片長度 int length = 4; // 驗證碼位數 int expireIn = 60; // 驗證碼有效時間 60s BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics g = image.getGraphics(); Random random = new Random(); g.setColor(getRandColor(200, 250)); g.fillRect(0, 0, width, height); g.setFont(new Font("Times New Roman", Font.ITALIC, 20)); g.setColor(getRandColor(160, 200)); for (int i = 0; i < 155; i++) { int x = random.nextInt(width); int y = random.nextInt(height); int xl = random.nextInt(12); int yl = random.nextInt(12); g.drawLine(x, y, x + xl, y + yl); } StringBuilder sRand = new StringBuilder(); for (int i = 0; i < length; i++) { String rand = String.valueOf(random.nextInt(10)); sRand.append(rand); g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110))); g.drawString(rand, 13 * i + 6, 16); } g.dispose(); return new ImageCode(image, sRand.toString(), expireIn); } private Color getRandColor(int fc, int bc) { Random random = new Random(); if (fc > 255) { fc = 255; } if (bc > 255) { bc = 255; } int r = fc + random.nextInt(bc - fc); int g = fc + random.nextInt(bc - fc); int b = fc + random.nextInt(bc - fc); return new Color(r, g, b); } }
在src/main/resources/resources/創建login.html:java
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>登陸</title> </head> <body> <form class="login-page" action="/login" method="post"> <div class="form"> <h3>帳戶登陸</h3> <input type="text" placeholder="用戶名" name="username" required="required" > <input type="password" placeholder="密碼" name="password" required="required" > <input type="text" name="imageCode" placeholder="驗證碼" style="width: 50%;"/> <img src="/code/image"/> <button type="submit">登陸</button> </div> </form> </body> </html>
- 定義驗證碼異常類型:
// 該exception繼承AuthenticationException public class ValidateCodeException extends AuthenticationException { ValidateCodeException(String message) { super(message); } }
- 定義過濾器
Spring Security其實是由許多過濾器組成的過濾器鏈,mysql
處理用戶登陸邏輯的過濾器爲UsernamePasswordAuthenticationFilter,而驗證碼校驗過程應該是在這個過濾器以前的,git
必須在驗證碼校驗經過後才能去校驗用戶名和密碼。github
Spring Security並無直接提供驗證碼校驗相關的過濾器接口,因此咱們須要本身定義一個驗證碼校驗的過濾器ValidateCodeFilterweb
@Component public class ValidateCodeFilter extends OncePerRequestFilter { @Autowired // 定義的錯誤處理 private AuthenticationFailureHandler authenticationFailureHandler; // 此時能夠經過HttpSessionSessionStrategy 獲取在後端生成驗證碼時候存儲在session中的驗證碼 private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { // 若是請求路徑是/login,而且是get請求的時候 if (StringUtils.equalsIgnoreCase("/login", httpServletRequest.getRequestURI()) && StringUtils.equalsIgnoreCase(httpServletRequest.getMethod(), "get")) { try { validateCode(new ServletWebRequest(httpServletRequest)); } catch (ValidateCodeException e) { //發生錯誤 則將參數傳遞給自定義的authenticationFailureHandler authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e); return; } } //放行請求 filterChain.doFilter(httpServletRequest, httpServletResponse); } private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException { ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(servletWebRequest, ValidateController.SESSION_KEY); String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode"); if (StringUtils.isBlank(codeInRequest)) { throw new ValidateCodeException("驗證碼不能爲空!"); } if (codeInSession == null) { throw new ValidateCodeException("驗證碼不存在!"); } if (codeInSession.isExpire()) { sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY); throw new ValidateCodeException("驗證碼已過時!"); } if (!StringUtils.equalsIgnoreCase(codeInSession.getCode(), codeInRequest)) { throw new ValidateCodeException("驗證碼不正確!"); } sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY); } }
- 修改MySecurityConfig
@Component public class MySecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyAuthenticationFailureHandler authenticationFailureHandler; @Autowired private MyAuthenticationSuccessHandler authenticationSuccessHandler; @Autowired private ValidateCodeFilter validateCodeFilter; @Bean public BCryptPasswordEncoder bCryptPasswordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // 添加驗證碼校驗過濾器 .formLogin() // 表單登陸 // http.httpBasic() // HTTP Basic .loginPage("/authentication/require") // 登陸跳轉 URL .loginProcessingUrl("/login") // 處理表單登陸 URL .failureHandler(authenticationFailureHandler) // 處理登陸失敗 .successHandler(authenticationSuccessHandler) .and() .authorizeRequests() // 受權配置 .antMatchers("/authentication/require", "/login.html", "/code/image").permitAll() // 無需認證的請求路徑 .anyRequest() // 全部請求 .authenticated() // 都須要認證 .and().csrf().disable(); } }
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) 必定要添加spring
訪問http://localhost:8003/login.html 顯示以下:sql
不填寫驗證碼:apache
錯誤驗證碼:後端
正確填寫驗證碼:
注: 其餘的類好比IndexController/MySecurityController/MySecurityConfig/MyUser
以及MyAuthenticationFailureHandler/MyAuthenticationSuccessHandler/UserDetailService
都將放入[SpringSecurity]http://www.javashuo.com/article/p-geyxequz-nx.html 公用類該中
本博客代碼測試正常運行!
源代碼連接:https://github.com/ttdys/springboot/tree/master/springboot_security/03_graphic_captcha