spring-security 自定義登陸校驗

1.爲什麼要作自定義登陸頁面以及校驗
在項目中配置了spring-security的模塊的項目中,spring boot會默認幫咱們生成的一個簡潔的登陸頁面,它會在咱們訪問任何請求的時候彈出來spring

spring-security 自定義登陸校驗

用戶名是默認的:user,密碼是須要咱們找到咱們啓動項目時的日誌,裏面會隨機生成一個默認的密碼
spring-security 自定義登陸校驗
輸入以後就能夠訪問到咱們的資源信息了
spring-security 自定義登陸校驗
但這種場景給咱們的項目場景確定截然不同了,不論是登陸的頁面仍是校驗的規則在實際的項目中都是比較複雜的。
這個時候就須要咱們自定義登陸頁面以及自定義校驗了數據庫

2.如何自定義
須要繼承WebSecurityConfigurerAdapter這個類,並重寫configure方法
2.1 一個簡單的基於表單登陸
可是configure有三個方法(從源碼可知),分別接收不一樣的參數,咱們應該重寫哪個呢?
spring-security 自定義登陸校驗
回到咱們原來的設想,是想讓程序使用表單進行登陸
首先配置一個最簡單的,基於表單認證的一個頁面配置
這個時候咱們須要覆蓋configure(HttpSecurity http)方法瀏覽器

protected void configure(HttpSecurity http) throws Exception {     
        //用表單登陸,進行身份認證,全部的請求都須要進行身份認證才能夠訪問
        http.formLogin()  //表單登陸的意思(指定了身份認證的方式)
        //受權
        .and()
        //對請求進行一個受權
        .authorizeRequests()
        .anyRequest().authenticated();//任何請求都須要身份認證
    }

這個時候 啓動項目,再次訪問請求,會看到一個表單頁面(也是spring security提供給咱們的)
spring-security 自定義登陸校驗
用戶名和密碼仍是上面同樣,user和控制檯輸出的密碼(每次啓動密碼都會變)
這個時候觀察瀏覽器上的地址變化會和http.httpBasic()這種方式不太同樣,咱們訪問的地址是
http://localhost:8080/user/2 在訪問的時候會幫咱們強制跳轉至:http://localhost:8080/login 頁面(登陸頁面),在認證成功以後會自動幫咱們重定向xx/user/2 這個連接。ide

2.2Spring Security核心原理(過濾器鏈)
全部訪問服務的請求都會通過spring security它的過濾器,響應也一樣會。這些過濾器在項目啓動的時候spring boot會自動配進去。post

spring-security 自定義登陸校驗

做用:用來認證用戶的身份,每一個過濾器負責一種認證方式
對於剛纔咱們的登陸而言:對於表單登陸的由UsernamePasswordAuthenticationFilter,對於基本登陸的(也就是http.httpBasic)則由BasicAuthenticationFilter來處理。this

例如:對於表單登陸它會怎麼樣來判斷這個請求會走這個Filter?
對於Filter來講,它會檢查當前的請求中是否有這個Filter所須要的信息,對於UsernamePasswordAuthenticationFilter來講,首先這個請求是不是登陸請求,請求中帶沒帶用戶名(username)和密碼(password),若是帶了,這會嘗試用這個帳戶名和密碼進行登陸,若是沒有帶,則會放行,走到下一個Filter中。加密

任何一個Filter成功完成了用戶登陸之後會在請求上作一個標記(這個用戶認證成功了)3d

最終會到一個FilterSecurityInterceptor過濾器中,是該過濾器鏈的最後一環。
在這個過濾器中它會決定你當前的請求能不能去訪問後面的Controller,依據什麼來判斷呢?依據咱們configure方法中所配置的。
spring-security 自定義登陸校驗
咱們如今的配置是:全部的請求都須要通過身份認證才能訪問,此時這個Filter會判斷當前的請求通過了前面的某一個Filter的身份認證。調試

針對複雜場景:針對某些請求,只能有VIP用戶才能訪問,這些規則都會被放在這個FilterSecurityInterceptor裏面,這個過濾器會根據這些規則作判斷,判斷的結果是過仍是不過,不過的話,會根據不一樣的緣由拋出不一樣的異常。好比 若是沒有通過身份認證,則拋出一個沒身份認證的一個異常;若是隻有VIP才能訪問這個請求,那麼也須要拋出異常,由於權限不夠。
在異常拋出去以後,會有一個異常過濾器來攔截ExceptionTranslationFilter,這個異常過濾器就是用來捕獲FilterSecurityInterceptor裏面所拋出來的異常,它會根據裏面拋出來的異常作響應的處理,若是沒有沒有登陸則引導用戶去登陸等。日誌

spring-security 自定義登陸校驗
在過濾器鏈中,除了綠色的過濾器(UsernamePasswordAuthenticationFilter/BasicAuthenticationFilter/...)是能夠經過配置來禁用或者啓用的,對於其餘顏色的過濾器都是不能控制的,必定會在過濾器鏈上,並且位置是不可變的。

2.3經過調試解析spring security登陸的一個過程
咱們須要在四個類裏面打上斷點
1.第一個斷點(Controller層)
spring-security 自定義登陸校驗

2.第二個斷點:(FilterSecurityInterceptor層)

spring-security 自定義登陸校驗

3.第三個斷點:(ExceptionTranslationFilter層)
spring-security 自定義登陸校驗

4.第四個斷點:(UsernamePasswordAuthenticationFilter層)
spring-security 自定義登陸校驗

從UsernamePasswordAuthenticationFilter這個過濾器中,它只會處理/login post的請求
收到請求只會,它會從request中拿到用戶名和密碼,而後作登陸。

spring-security 自定義登陸校驗

發送請求,由於請求的參數中沒有username/password參數,直接就到了最後一個過濾器(FilterSecurityInterceptor)由它來判斷是否被攔截。
spring-security 自定義登陸校驗
由於咱們配置了全部請求都須要進行身份認證,斷點進行下一步時,則會拋出來一個異常(ExceptionTranslationFilter
spring-security 自定義登陸校驗

spring-security 自定義登陸校驗
捕獲到的是未受權異常。這個時候前臺頁面就會回到了登陸頁面
spring-security 自定義登陸校驗

在登陸頁面輸入完,正確的帳戶名和密碼以後,斷點會來到
UsernamePasswordAuthenticationFilter
spring-security 自定義登陸校驗
它會從request中取到用戶名和密碼進行校驗,校驗成功後會繼續回到了
FilterSecurityInterceptor)中的super.beforeInvocation(fi)方法,爲何呢?由於以前的請求是
http://localhost:8080/user/2 它是這個資源訪問(Controller),在登陸成功以後會有個資源的跳轉。

spring-security 自定義登陸校驗

在doFilter以後就調到咱們本身的Controller中的內容了。
spring-security 自定義登陸校驗
所有完成以後,在頁面的前臺就能夠看到請求的信息了。

spring-security 自定義登陸校驗

2.4自定義登陸帳戶校驗
爲何須要自定義呢,由於這個咱們的業務場景不只僅在是個簡單帳戶的校驗,須要根據咱們本身的業務邏輯來實現相關的功能。
相關功能點:

用戶信息的獲取邏輯在spring security中是被封裝在一個接口中(UserDetailsService

在此接口中只有一個loadUserByUsername方法
spring-security 自定義登陸校驗
根據用戶在前臺傳輸過來的用戶名來查詢用戶信息,用戶的信息被封裝在UserDetails的實現類裏面,這個實現類返回之後,作一下響應的處理,校驗都經過了 就會把這個用戶放到Session裏面,就會認爲你的登陸成功了。
找不到用戶則會拋異常UsernameNotFoundException,也會被響應的處理類接收。

實現代碼

  • 處理用戶信息的獲取(從數據庫查詢)
@Component
public class MyUserDetailsService implements UserDetailsService{

    private static final Logger logger = LoggerFactory.getLogger(MyUserDetailsService.class);

    //@Autowired
    //private XXService xxService; 

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("根據用戶名查詢用戶信息"+username);
        //查詢用戶信息
        //xxService.query(username);
        return new User(username, "123456", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }

}

拿到用戶信息以後須要組裝成UserDetails對象,可是它自己就是一個接口 應該怎麼返回呢?
這個時候須要用到spring security中給咱們提供的一個User對象,這個對象已經實現了UserDetails接口
spring-security 自定義登陸校驗
咱們須要用到它的方法
spring-security 自定義登陸校驗

第一個參數就是用戶名,第二個參數就是密碼,第三個參數就是咱們的角色(作受權)。

此時咱們就能夠在瀏覽器上訪問咱們的請求地址了:http://localhost:8080/login

帳號隨便,密碼爲123456,輸入錯誤的密碼則會被拋出來異常
spring-security 自定義登陸校驗

密碼正確,則被正常放行。

  • 處理用戶校驗邏輯(是否凍結,是否過時等)
    常規業務狀況須要返回UserDetails的實現類去實現用戶的鎖定,過時,註銷等操做,這些方法是UserDetails中幫咱們定義的,也可使用User實現類。
public class MyUser implements UserDetails {

    private String username;
    private String password;

    private boolean accountNonExpired;
    private boolean accountNonLocked;
    private boolean credentialsNonExpired;
    private boolean enabled;

    public MyUser(String username, String password, boolean accountNonExpired, boolean accountNonLocked,
            boolean credentialsNonExpired, boolean enabled) {
        this.username = username;
        this.password = password;
        this.accountNonExpired = accountNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.credentialsNonExpired = credentialsNonExpired;
        this.enabled = enabled;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

在處理失效的屬性時,置爲無效;

return new MyUser(username, "123123", true, true, true, false);

再次啓動,登陸的用戶信息都會是已經失效的用戶
spring-security 自定義登陸校驗

  • 處理密碼加密解密

    處理加密和解密是一個新的接口(PasswordEncoder)。
    org.springframework.security.crypto.password包下面的類。
    spring-security 自定義登陸校驗
    加密方法是咱們本身調用的(encode),而解密方法是spring security是自動調用的,根據UserDetails的實現類中的密碼是自動調用的。
    配置加密

    BrowserSecurityConfig類中配置密碼加密

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    BCryptPasswordEncoder是spring security推薦的一個加密方法,也能夠實現本身的加密方法,只須要實現PasswordEncoder接口的實現類便可。

    這個時候咱們的登陸接口則須要修改成:

//常規狀況下,passwordEncoder.encode("123123")是註冊時須要作的方法,
 //而登陸的時候則只須要傳遞password便可
        return new MyUser(username, passwordEncoder.encode("123123"), true, true, true, false);

爲了更好的觀察到 BCryptPasswordEncoder的強大之處,打印出帳號的密碼。

String password = passwordEncoder.encode("123123");

 logger.info(username+"的密碼是:"+password);

此時咱們啓動服務,輸入咱們的請求地址。
登陸成功以後能夠看到後臺會輸出 BCryptPasswordEncoder加密後的密碼:
spring-security 自定義登陸校驗
當咱們使用同一帳戶進行反覆登陸時繼續觀察日誌
spring-security 自定義登陸校驗
會發現,兩次的加密後的密碼是不一致的。
原理一樣的一個密碼,隨機生成一個鹽值,而且在最後生成密碼串的時候把隨機生成的鹽混到這個串裏面,每次判斷的是能夠用隨機生成的鹽+生成的串去比對,最終來判斷密碼是否匹配。這樣就能夠避免同一個密碼被反覆盜用。

相關文章
相關標籤/搜索