1. 程式人生 > >基於SpringBoot搭建應用開發框架(二) —— 登入認證

基於SpringBoot搭建應用開發框架(二) —— 登入認證

零、前言

本文基於《基於SpringBoot搭建應用開發框架(一)——基礎架構》,通過該文,熟悉了SpringBoot的用法,完成了應用框架底層的搭建。

在開始本文之前,底層這塊已經有了很大的調整,主要是SpringBoot由之前的 1.5.9.RELEASE 升級至 2.1.0.RELEASE 版本,其它依賴的三方包基本也都升級到目前最新版了。

其次是整體架構上也做了調整:

  sunny-parent:sunny 專案的頂級父類,sunny-parent 又繼承自 spring-boot-starter-parent ,為所有專案統一 spring 及 springboot 版本。同時,管理專案中將用到的大部分的第三方包,統一管理版本號。

  sunny-starter:專案中開發的元件以 starter 的方式進行整合,按需引入 starter 即可。sunny-starter 下以 module 的形式組織,便於管理、批量打包部署。

    sunny-starter-core:核心包,定義基礎的操作類、異常封裝、工具類等,集成了 mybatis-mapper、druid 資料來源、redis 等。

    sunny-starter-captcha:驗證碼封裝。

  sunny-cloud:spring-cloud 系列服務,微服務基礎框架,本篇文章主要集中在 sunny-cloud-security上,其它的以後再說。

    sunny-cloud-security:認證服務和授權服務。

  sunny-admin:管理端服務,業務中心。

  

 

本篇將會一步步完成系統的登入認證,包括常規的使用者名稱+密碼登入、以及社交方式登入,如QQ、微信授權登入等,一步步分析 spring-security 及 oauth 相關的原始碼。

一、SpringSecurity 簡介

SpringSecurity 是專門針對基於Spring專案的安全框架,充分利用了AOP和Filter來實現安全功能。它提供全面的安全性解決方案,同時在 Web 請求級和方法呼叫級處理身份確認和授權。他提供了強大的企業安全服務,如:認證授權機制、Web資源訪問控制、業務方法呼叫訪問控制、領域物件訪問控制Access Control List(ACL)、單點登入(SSO)等等。

核心功能:認證(你是誰)、授權(你能幹什麼)、攻擊防護(防止偽造身份)。

基本原理:SpringSecurity的核心實質是一個過濾器鏈,即一組Filter,所有的請求都會經過這些過濾器,然後響應返回。每個過濾器都有特定的職責,可通過配置新增、刪除過濾器。過濾器的排序很重要,因為它們之間有依賴關係。有些過濾器也不能刪除,如處在過濾器鏈最後幾環的ExceptionTranslationFilter(處理後者丟擲的異常),FilterSecurityInterceptor(最後一環,根據配置決定請求能不能訪問服務)。

二、標準登入

使用 使用者名稱+密碼 的方式來登入,使用者名稱、密碼儲存在資料庫,並且支援密碼輸入錯誤三次後開啟驗證碼,通過這樣一個過程來熟悉 spring security 的認證流程,掌握 spring security 的原理。

1、基礎環境

① 建立 sunny-cloud-security 模組,埠號設定為 8010,在sunny-cloud-security模組引入security支援以及sunny-starter-core:

② 開發一個TestController

 ③ 不做任何配置,啟動系統,然後訪問 localhost:8010/test 時,會自動跳轉到SpringSecurity預設的登入頁面去進行認證。那這登入的使用者名稱和密碼從哪來呢?

啟動專案時,從控制檯輸出中可以找到生成的 security 密碼,從 UserDetailsServiceAutoConfiguration 可以得知,使用的是基於記憶體的使用者管理器,預設的使用者名稱為 user,密碼是隨機生成的UUID。

我們也可以修改預設的使用者名稱和密碼。

④ 使用 user 和生成的UUID密碼登入成功後即可訪問 /test 資源,最簡單的一個認證就完成了。

在不做任何配置的情況下,security會把服務內所有資源的訪問都保護起來,需要先進行身份證認證才可訪問, 使用預設的表單登入或http basic認證方式。

不過這種預設方式肯定無法滿足我們的需求,我們的使用者名稱和密碼都是存在資料庫的。下面我們就來看看在 spring boot 中我們如何去配置自己的登入頁面以及從資料庫獲取使用者資料來完成使用者登入。

2、自定義登入頁面

① 首先開發一個登入頁面,由於頁面中會使用到一些動態資料,決定使用 thymeleaf 模板引擎,只需在 pom 中引入如下依賴,使用預設配置即可,具體有哪些配置可從 ThymeleafProperties 中瞭解到。

② 同時,在 resources 目錄下,建 static 和 templates 兩個目錄,static 目錄用於存放靜態資源,templates 用於存放 thymeleaf 模板頁面,同時配置MVC的靜態資源對映。

   

③ 開發後臺首頁、登入頁面的跳轉地址,/login 介面用於向登入頁面傳遞登入相關的資料,如使用者名稱、是否啟用驗證碼、錯誤訊息等。

 1 package com.lyyzoo.sunny.security.controller;
 2 
 3 import javax.servlet.http.HttpServletResponse;
 4 import javax.servlet.http.HttpSession;
 5 
 6 import org.apache.commons.lang3.StringUtils;
 7 import org.springframework.beans.factory.annotation.Autowired;
 8 import org.springframework.security.web.WebAttributes;
 9 import org.springframework.stereotype.Controller;
10 import org.springframework.ui.Model;
11 import org.springframework.web.bind.annotation.GetMapping;
12 import org.springframework.web.bind.annotation.RequestMapping;
13 import org.springframework.web.bind.annotation.ResponseBody;
14 
15 import com.lyyzoo.sunny.captcha.CaptchaImageHelper;
16 import com.lyyzoo.sunny.core.base.Result;
17 import com.lyyzoo.sunny.core.message.MessageAccessor;
18 import com.lyyzoo.sunny.core.userdetails.CustomUserDetails;
19 import com.lyyzoo.sunny.core.userdetails.DetailsHelper;
20 import com.lyyzoo.sunny.core.util.Results;
21 import com.lyyzoo.sunny.security.constant.SecurityConstants;
22 import com.lyyzoo.sunny.security.domain.entity.User;
23 import com.lyyzoo.sunny.security.domain.service.ConfigService;
24 import com.lyyzoo.sunny.security.domain.service.UserService;
25 
26 /**
27  *
28  * @author bojiangzhou 2018/03/28
29  */
30 @Controller
31 public class SecurityController {
32 
33     private static final String LOGIN_PAGE = "login";
34 
35     private static final String INDEX_PAGE = "index";
36 
37     private static final String FIELD_ERROR_MSG = "errorMsg";
38     private static final String FIELD_ENABLE_CAPTCHA = "enableCaptcha";
39 
40     @Autowired
41     private CaptchaImageHelper captchaImageHelper;
42     @Autowired
43     private UserService userService;
44     @Autowired
45     private ConfigService configService;
46 
47     @RequestMapping("/index")
48     public String index() {
49         return INDEX_PAGE;
50     }
51 
52     @GetMapping("/login")
53     public String login(HttpSession session, Model model) {
54         String errorMsg = (String) session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
55         String username = (String) session.getAttribute(User.FIELD_USERNAME);
56         if (StringUtils.isNotBlank(errorMsg)) {
57             model.addAttribute(FIELD_ERROR_MSG, errorMsg);
58         }
59         if (StringUtils.isNotBlank(username)) {
60             model.addAttribute(User.FIELD_USERNAME, username);
61             User user = userService.getUserByUsername(username);
62             if (user == null) {
63                 model.addAttribute(FIELD_ERROR_MSG, MessageAccessor.getMessage("login.username-or-password.error"));
64             } else {
65                 if (configService.isEnableCaptcha(user.getPasswordErrorTime())) {
66                     model.addAttribute(FIELD_ENABLE_CAPTCHA, true);
67                 }
68             }
69         }
70         session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
71 
72         return LOGIN_PAGE;
73     }
74 
75     @GetMapping("/public/captcha.jpg")
76     public void captcha(HttpServletResponse response) {
77         captchaImageHelper.generateAndWriteCaptchaImage(response, SecurityConstants.SECURITY_KEY);
78     }
79 
80     @GetMapping("/user/self")
81     @ResponseBody
82     public Result test() {
83         CustomUserDetails details = DetailsHelper.getUserDetails();
84 
85         return Results.successWithData(details);
86     }
87 
88 }
View Code

 ④  從 spring boot 官方文件可以得知,spring security 的核心配置都在 WebSecurityConfigurerAdapter 裡,我們只需繼承該介面卡覆蓋預設配置即可。首先來看看預設的登入頁面以及如何配置登入頁面。

通過 HttpSecurity 配置安全策略,首先開放了允許匿名訪問的地址,除此之外都需要認證,通過 formLogin() 來啟用表單登入,並配置了預設的登入頁面,以及登入成功後的首頁地址。

啟動系統,訪問資源跳轉到自定義的登入頁面了:

⑤ 那麼預設的登入頁面是怎麼來的呢,以及做了哪些預設配置?

從 formLogin() 可以看出,啟用表單登入即啟用了表單登入的配置 FormLoginConfigurer:

從 FormLoginConfigurer 的建構函式中可以看出,表單登入使用者名稱和密碼的引數預設配置為 username 和 password,所以,我們的登入頁面中需和這兩個引數配置成一樣,當然了,我們也可以在 formLogin() 後自定義這兩個引數。

同時,可以看出開啟了 UsernamePasswordAuthenticationFilter 過濾器,用於 使用者名稱+密碼 登入方式的認證,這個之後再說明。

從初始化配置中可以看出,預設建立了 DefaultLoginPageGeneratingFilter 過濾器用於生成預設的登入頁面,從該過濾器的初始化方法中我們也可以瞭解到一些預設的配置。這個過濾器只有在未配置自定義登入頁面時才會生效。

3、SpringSecurity基本原理

在進行後面的開發前,先來了解下 spring security 的基本原理。

spring security 的核心是過濾器鏈,即一組 Filter。所有服務資源的請求都會經過 spring security 的過濾器鏈,並響應返回。

我們從控制檯中可以找到輸出過濾器鏈的類 DefaultSecurityFilterChain,在現有的配置上,可以看到當前過濾器鏈共有13個過濾器。

每個過濾器主要做什麼可以參考:Spring Security 核心過濾器鏈分析

過濾器鏈的建立是通過 HttpSecurity 的配置而來,實際上,每個 HttpSecurity 的配置都會建立相應的過濾器鏈來處理對應的請求,每個請求都會進入 FilterChainProxy 過濾器,根據請求選擇一個合適的過濾器鏈來處理該請求。

過濾器的順序我們可以從 FilterComparator 中得知,並且可以看出 spring security 預設有25個過濾器(自行檢視):

 不難發現,幾乎所有的過濾器都直接或間接繼承自 GenericFilterBean,通過這個基礎過濾器可以看到都有哪些過濾器,通過每個過濾器的名稱我們能大概瞭解到 spring security 為我們提供了哪些功能,要啟用這些功能,只需通過配置加入相應的過濾器即可,比如 oauth 認證。

過濾器鏈中,綠色框出的這類過濾器主要用於使用者認證,這些過濾器會根據當前的請求檢查是否有這個過濾器所需的資訊,如果有則進入該過濾器,沒有則不會進入下一個過濾器。

比如這裡,如果是表單登入,要求必須是[POST /login],則進入 UsernamePasswordAuthenticationFilter 過濾器,使用使用者名稱和密碼進行認證,不會再進入BasicAuthenticationFilter;

如果使用 http basic 的方式進行認證,要求請求頭必須包含 Authorization,且值以 basic 打頭,則進入 BasicAuthenticationFilter 進行認證。

經過前面的過濾器後,最後會進入到 FilterSecurityInterceptor,這是整個 spring security 過濾器鏈的最後一環,在它身後就是服務的API。

這個過濾器會去根據配置決定當前的請求能不能訪問真正的資源,主要一些實現功能在其父類AbstractSecurityInterceptor中。

[1] 拿到的是許可權配置,會根據這些配置決定訪問的API能否通過。

[2] 當前上下文必須有使用者認證資訊 Authentication,就算是匿名訪問也會有相應的過濾器來生成 Authentication。不難發現,不同型別的認證過濾器對應了不同的 Authentication。使用使用者名稱和密碼登入時,就會生成 UsernamePasswordAuthenticationToken。

[3] 使用者認證,首先判斷使用者是否已認證通過,認證通過則直接返回 Authentication,否則呼叫認證器進行認證。認證通過之後將 Authentication 放到 Security 的上下文,這就是為何我們能從 SecurityContextHolder 中取到 Authentication 的源頭。

認證管理器是預設配置的 ProviderManager,ProviderManager 則管理者多個 AuthenticationProvider 認證器 ,認證的時候,只要其中一個認證器認證通過,則標識認證通過。

認證器:表單登入預設使用 DaoAuthenticationProvider,我們想要實現從資料庫獲取使用者名稱和密碼就得從這裡入手。

[4] 認證通過後,使用許可權決定管理器 AccessDecisionManager 判斷是否有許可權,管理器則管理者多個 許可權投票器 AccessDecisionVoter,通過投票器來決定是否有許可權訪問資源。因此,我們也可以自定義投票器來判斷使用者是否有許可權訪問某個API。

 

最後,如果未認證通過或沒有許可權,FilterSecurityInterceptor 則丟擲相應的異常,異常會被 ExceptionTranslationFilter 捕捉到,進行統一的異常處理分流,比如未登入時,重定向到登入頁面;沒有許可權的時候丟擲403異常等。

4、使用者認證流程

從 spring security 基本原理的分析中不難發現,使用者的認證過程涉及到三個主要的元件:

AbstractAuthenticationProcessingFilter:它在基於web的認證請求中用於處理包含認證資訊的請求,建立一個部分完整的Authentication物件以在鏈中傳遞憑證資訊。

AuthenticationManager:它用來校驗使用者的憑證資訊,或者會丟擲一個特定的異常(校驗失敗的情況)或者完整填充Authentication物件,將會包含了許可權資訊。

AuthenticationProvider:它為AuthenticationManager提供憑證校驗。一些AuthenticationProvider的實現基於憑證資訊的儲存,如資料庫,來判定憑證資訊是否可以被認可。

我們從核心的 AbstractAuthenticationProcessingFilter 入手,來分析下使用者認證的流程。

[1] 可以看到,首先會呼叫 attemptAuthentication 來獲取認證後的 Authentication。attemptAuthentication 是一個抽象方法,在其子類中實現。

 前面提到過,啟用表單登入時,就會建立 UsernamePasswordAuthenticationFilter 用於處理表單登入。後面開發 oauth2 認證的時候則會用到 OAuth2 相關的過濾器。

從 attemptAuthentication 的實現中可以看出,主要是將 username 和 password 封裝到 UsernamePasswordAuthenticationToken。

從當前 UsernamePasswordAuthenticationToken 的構造方法中可以看出,此時的 Authentication 設定了未認證狀態。

 【#】通過 setDetails 可以向 UsernamePasswordAuthenticationToken  中加入 Details 用於後續流程的處理,稍後我會實現AuthenticationDetailsSource 將驗證碼放進去用於後面的認證。

之後,通過 AuthenticationManager 進行認證,實際是 ProviderManager 管理著一些認證器,這些配置都可以通過 setter 方法找到相應配置的位置,這裡就不贅述了。

不難發現,使用者認證器使用的是 AbstractUserDetailsAuthenticationProvider,流程主要涉及到 retrieveUser  和 additionalAuthenticationChecks 兩個抽象方法。

【#】AbstractUserDetailsAuthenticationProvider 預設只有一個實現類 DaoAuthenticationProvider,獲取使用者資訊、使用者密碼校驗都是在這個實現類裡,因此我們也可以實現自己的 AbstractUserDetailsAuthenticationProvider 來處理相關業務。

【#】從 retrieveUser 中可以發現,主要使用 UserDetailsService 來獲取使用者資訊,該介面只有一個方法 loadUserByUsername,我們也會實現該介面來從資料庫獲取使用者資訊。如果有複雜的業務邏輯,比如鎖定使用者等,還可以覆蓋 retrieveUser 方法。

 使用者返回成功後,就會通過 PasswordEncoder 來校驗使用者輸入的密碼和資料庫密碼是否匹配。注意資料庫存入的密碼是加密後的密碼,且不可逆。

 使用者、密碼都校驗通過後,就會建立已認證的 Authentication,從此時 UsernamePasswordAuthenticationToken 的構造方法可以看出,構造的是一個已認證的 Authentication。

[2] 如果使用者認證失敗,會呼叫 AuthenticationFailureHandler 的 onAuthenticationFailure 方法進行認證失敗後的處理,我們也會實現這個介面來做一些失敗後邏輯處理。

[3] 使用者認證成功,將 Authentication 放入 security 上下文,呼叫 AuthenticationSuccessHandler 做認證成功的一些後續邏輯處理,我們也會實現這個介面。

5、使用者認證程式碼實現

通過 spring security 基本原理分析和使用者認證流程分析,我們已經能夠梳理出完成認證需要做哪些工作了。

① 首先設計並建立系統使用者表:

② CustomUserDetails

自定義 UserDetails,根據自己的需求將一些常用的使用者資訊封裝到 UserDetails 中,便於快速獲取使用者資訊,比如使用者ID、暱稱等。

 1 package com.lyyzoo.sunny.core.userdetails;
 2 
 3 import java.util.Collection;
 4 import java.util.Objects;
 5 
 6 import org.springframework.security.core.GrantedAuthority;
 7 import org.springframework.security.core.userdetails.User;
 8 
 9 
10 /**
11  * 定製的UserDetail物件
12  *
13  * @author bojiangzhou 2018/09/02
14  */
15 public class CustomUserDetails extends User {
16     private static final long serialVersionUID = -4461471539260584625L;
17 
18     private Long userId;
19 
20     private String nickname;
21 
22     private String language;
23 
24     public CustomUserDetails(String username, String password, Long userId, String nickname, String language,
25                              Collection<? extends GrantedAuthority> authorities) {
26         super(username, password, authorities);
27         this.userId = userId;
28         this.nickname = nickname;
29         this.language = language;
30     }
31 
32     public Long getUserId() {
33         return userId;
34     }
35 
36     public void setUserId(Long userId) {
37         this.userId = userId;
38     }
39 
40     public String getNickname() {
41         return nickname;
42     }
43 
44     public void setNickname(String nickname) {
45         this.nickname = nickname;
46     }
47 
48     public String getLanguage() {
49         return language;
50     }
51 
52     public void setLanguage(String language) {
53         this.language = language;
54     }
55 
56     @Override
57     public boolean equals(Object o) {
58         if (this == o) {
59             return true;
60         }
61         if (!(o instanceof CustomUserDetails)) {
62             return false;
63         }
64         if (!super.equals(o)) {
65             return false;
66         }
67 
68         CustomUserDetails that = (CustomUserDetails) o;
69 
70         if (!Objects.equals(userId, that.userId)) {
71             return false;
72         }
73         return false;
74     }
75 
76     @Override
77     public int hashCode() {
78         int result = super.hashCode();
79         result = 31 * result + userId.hashCode();
80         result = 31 * result + nickname.hashCode();
81         result = 31 * result + language.hashCode();
82         return result;
83     }
84 
85 }
View Code

③ CustomUserDetailsService

自定義 UserDetailsService 來從資料庫獲取使用者資訊,並將使用者資訊封裝到 CustomUserDetails

 1 package com.lyyzoo.sunny.security.core;
 2 
 3 import java.util.ArrayList;
 4 import java.util.Collection;
 5 
 6 import org.springframework.beans.factory.annotation.Autowired;
 7 import org.springframework.security.core.GrantedAuthority;
 8 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 9 import org.springframework.security.core.userdetails.UserDetails;
10 import org.springframework.security.core.userdetails.UserDetailsService;
11 import org.springframework.security.core.userdetails.UsernameNotFoundException;
12 import org.springframework.stereotype.Component;
13 
14 import com.lyyzoo.sunny.core.message.MessageAccessor;
15 import com.lyyzoo.sunny.core.userdetails.CustomUserDetails;
16 import com.lyyzoo.sunny.security.domain.entity.User;
17 import com.lyyzoo.sunny.security.domain.service.UserService;
18 
19 /**
20  * 載入使用者資訊實現類
21  *
22  * @author bojiangzhou 2018/03/25
23  */
24 @Component
25 public class CustomUserDetailsService implements UserDetailsService {
26 
27     @Autowired
28     private UserService userService;
29 
30     @Override
31     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
32         User user = userService.getUserByUsername(username);
33         if (user == null) {
34             throw new UsernameNotFoundException(MessageAccessor.getMessage("login.username-or-password.error"));
35         }
36 
37         Collection<GrantedAuthority> authorities = new ArrayList<>();
38         authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
39 
40         return new CustomUserDetails(username, user.getPassword(), user.getId(),
41                 user.getNickname(), user.getLanguage(), authorities);
42     }
43 
44 }
View Code

④ CustomWebAuthenticationDetails

自定義 WebAuthenticationDetails 用於封裝傳入的驗證碼以及快取的驗證碼,用於後續校驗。

  1 package com.lyyzoo.sunny.security.core;
  2 
  3 import javax.servlet.http.HttpServletRequest;
  4 
  5 import com.lyyzoo.sunny.captcha.CaptchaResult;
  6 import org.springframework.security.web.authentication.WebAuthenticationDetails;
  7 
  8 /**
  9  * 封裝驗證碼
 10  *
 11  * @author bojiangzhou 2018/09/18
 12  */
 13 public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {
 14 
 15     public static final String FIELD_CACHE_CAPTCHA = "cacheCaptcha";
 16 
 17     private String inputCaptcha;
 18     private String cacheCaptcha;
 19 
 20     public CustomWebAuthenticationDetails(HttpServletRequest request) {
 21         super(request);
 22         cacheCaptcha = (String) request.getAttribute(FIELD_CACHE_CAPTCHA);
 23         inputCaptcha = request.getParameter(CaptchaResult.FIELD_CAPTCHA);
 24     }
 25 
 26     public String getInputCaptcha() {
 27         return inputCaptcha;
 28     }
 29 
 30     public String getCacheCaptcha() {
 31         return cacheCaptcha;
 32     }
 33 
 34     @Override
 35     public boolean equals(Object object) {
 36         if (this == object) {
 37             return true;
 38         }
 39         if (object == null || getClass() != object.getClass()) {
 40             return false;
 41         }
 42         if (!super.equals(object)) {
 43             return false;
 44         }
 45 
 46         CustomWebAuthenticationDetails that = (CustomWebAuthenticationDetails) object;
 47 
 48         return inputCaptcha != null ? inputCaptcha.equals(that.inputCaptcha) : that.inputCaptcha == null;
 49     }
 50 
 51     @Override
 52     public int hashCode() {
 53         int result = super.hashCode();
 54         result = 31 * result + (inputCaptcha != null ? inputCaptcha.hashCode() : 0);
 55         return result;
 56     }
 57 }
 58 package com.lyyzoo.sunny.security.core;
 59 
 60 import javax.servlet.http.HttpServletRequest;
 61 
 62 import com.lyyzoo.sunny.captcha.CaptchaResult;
 63 import org.springframework.security.web.authentication.WebAuthenticationDetails;
 64 
 65 /**
 66  * 封裝驗證碼
 67  *
 68  * @author bojiangzhou 2018/09/18
 69  */
 70 public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {
 71 
 72     public static final String FIELD_CACHE_CAPTCHA = "cacheCaptcha";
 73 
 74     private String inputCaptcha;
 75     private String cacheCaptcha;
 76 
 77     public CustomWebAuthenticationDetails(HttpServletRequest request) {
 78         super(request);
 79         cacheCaptcha = (String) request.getAttribute(FIELD_CACHE_CAPTCHA);
 80         inputCaptcha = request.getParameter(CaptchaResult.FIELD_CAPTCHA);
 81     }
 82 
 83     public String getInputCaptcha() {
 84         return inputCaptcha;
 85     }
 86 
 87     public String getCacheCaptcha() {
 88         return cacheCaptcha;
 89     }
 90 
 91     @Override
 92     public boolean equals(Object object) {
 93         if (this == object) {
 94             return true;
 95         }
 96         if (object == null || getClass() != object.getClass()) {
 97             return false;
 98         }
 99         if (!super.equals(object)) {
100             return false;
101         }
102 
103         CustomWebAuthenticationDetails that = (CustomWebAuthenticationDetails) object;
104 
105         return inputCaptcha != null ? inputCaptcha.equals(that.inputCaptcha) : that.inputCaptcha == null;
106     }
107 
108     @Override
109     public int hashCode() {
110         int result = super.hashCode();
111         result = 31 * result + (inputCaptcha != null ? inputCaptcha.hashCode() : 0);
112         return result;
113     }
114 }
View Code

⑤ CustomAuthenticationDetailsSource

當然了,還需要一個構造驗證碼的 AuthenticationDetailsSource

 1 package com.lyyzoo.sunny.security.core;
 2 
 3 import javax.servlet.http.HttpServletRequest;
 4 
 5 import org.springframework.beans.factory.annotation.Autowired;
 6 import org.springframework.security.authentication.AuthenticationDetailsSource;
 7 import org.springframework.security.web.authentication.WebAuthenticationDetails;
 8 import org.springframework.stereotype.Component;
 9 
10 import com.lyyzoo.sunny.captcha.CaptchaImageHelper;
11 import com.lyyzoo.sunny.security.constant.SecurityConstants;
12 
13 /**
14  * 自定義獲取AuthenticationDetails 用於封裝傳進來的驗證碼
15  *
16  * @author bojiangzhou 2018/09/18
17  */
18 @Component
19 public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
20 
21     @Autowired
22     private CaptchaImageHelper captchaImageHelper;
23 
24     @Override
25     public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
26         String cacheCaptcha = captchaImageHelper.getCaptcha(request, SecurityConstants.SECURITY_KEY);
27         request.setAttribute(CustomWebAuthenticationDetails.FIELD_CACHE_CAPTCHA, cacheCaptcha);
28         return new CustomWebAuthenticationDetails(request);
29     }
30 
31 }
View Code

⑥ CustomAuthenticationProvider

自定義認證處理器,主要加入了驗證碼的檢查,如果使用者密碼輸入錯誤三次以上,則需要驗證碼。

 1 package com.lyyzoo.sunny.security.core;
 2 
 3 import org.apache.commons.lang3.StringUtils;
 4 import org.springframework.beans.factory.annotation.Autowired;
 5 import org.springframework.security.authentication.AuthenticationServiceException;
 6 import org.springframework.security.authentication.BadCredentialsException;
 7 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 8 import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
 9 import org.springframework.security.core.AuthenticationException;
10 import org.springframework.security.core.userdetails.UserDetails;
11 import org.springframework.security.crypto.password.PasswordEncoder;
12 import org.springframework.stereotype.Component;
13 
14 import com.lyyzoo.sunny.security.domain.entity.User;
15 import com.lyyzoo.sunny.security.domain.service.ConfigService;
16 import com.lyyzoo.sunny.security.domain.service.UserService;
17 
18 /**
19  * 自定義認證器
20  *
21  * @author bojiangzhou 2018/09/09
22  */
23 @Component
24 public class CustomAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
25 
26     @Autowired
27     private UserService userService;
28     @Autowired
29     private CustomUserDetailsService detailsService;
30     @Autowired
31     private PasswordEncoder passwordEncoder;
32     @Autowired
33     private ConfigService configService;
34 
35 
36     @Override
37     protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
38         // 如有其它邏輯處理,可在此處進行邏輯處理...
39         return detailsService.loadUserByUsername(username);
40     }
41 
42     @Override
43     protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
44         String username = userDetails.getUsername();
45         User user = userService.getUserByUsername(username);
46 
47         // 檢查驗證碼
48         if (authentication.getDetails() instanceof CustomWebAuthenticationDetails) {
49             if (configService.isEnableCaptcha(user.getPasswordErrorTime())) {
50                 CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails();
51                 String inputCaptcha = details.getInputCaptcha();
52                 String cacheCaptcha = details.getCacheCaptcha();
53                 if (StringUtils.isEmpty(inputCaptcha) || !StringUtils.equalsIgnoreCase(inputCaptcha, cacheCaptcha)) {
54                     throw new AuthenticationServiceException("login.captcha.error");
55                 }
56                 authentication.setDetails(null);
57             }
58         }
59 
60         // 檢查密碼是否正確
61         String password = userDetails.getPassword();
62         String rawPassword = authentication.getCredentials().toString();
63 
64         boolean match = passwordEncoder.matches(rawPassword, password);
65         if (!match) {
66             throw new BadCredentialsException("login.username-or-password.error");
67         }
68     }
69 }
View Code

⑦ CustomAuthenticationSuccessHandler

自定義認證成功處理器,使用者認證成功,將密碼錯誤次數置零。

 1 package com.lyyzoo.sunny.security.core;
 2 
 3 import java.io.IOException;
 4 
 5 import javax.servlet.ServletException;
 6 import javax.servlet.http.HttpServletRequest;
 7 import javax.servlet.http.HttpServletResponse;
 8 
 9 import org.springframework.beans.factory.annotation.Autowired;
10 import org.springframework.security.core.Authentication;
11 import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
12 import org.springframework.stereotype.Component;
13 
14 import com.lyyzoo.sunny.security.domain.entity.User;
15 import com.lyyzoo.sunny.security.domain.service.UserService;
16 
17 /**
18  * 登入認證成功處理器
19  * 
20  * @author bojiangzhou 2018/03/29
21  */
22 @Component
23 public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
24 
25     @Autowired
26     private UserService userService;
27 
28     @Override
29     public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
30                     Authentication authentication) throws IOException, ServletException {
31         String username = request.getParameter("username");
32         User user = userService.getUserByUsername(username);
33         userService.loginSuccess(user.getId());
34         super.onAuthenticationSuccess(request, response, authentication);
35     }
36 }
View Code

⑧ CustomAuthenticationFailureHandler

使用者認證失敗,記錄密碼錯誤次數,並重定向到登入頁面。

 1 package com.lyyzoo.sunny.security.core;
 2 
 3 import java.io.IOException;
 4 
 5 import javax.servlet.ServletException;
 6 import javax.servlet.http.HttpServletRequest;
 7 import javax.servlet.http.HttpServletResponse;
 8 import javax.servlet.http.HttpSession;
 9 
10 import org.springframework.beans.factory.annotation.Autowired;
11 import org.springframework.security.authentication.BadCredentialsException;
12 import org.springframework.security.core.AuthenticationException;
13 import org.springframework.security.web.DefaultRedirectStrategy;
14 import org.springframework.security.web.RedirectStrategy;
15 import org.springframework.security.web.