1. 程式人生 > >spring-security 自定義登入校驗

spring-security 自定義登入校驗

1.為何要做自定義登入頁面以及校驗
在專案中配置了spring-security的模組的專案中,spring boot會預設幫我們生成的一個簡潔的登入頁面,它會在我們訪問任何請求的時候彈出來

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 這個連結。

2.2Spring Security核心原理(過濾器鏈)
所有訪問服務的請求都會經過spring security它的過濾器,響應也同樣會。這些過濾器在專案啟動的時候spring boot會自動配進去。

spring-security 自定義登入校驗

作用:用來認證使用者的身份,每個過濾器負責一種認證方式
對於剛才我們的登入而言:對於表單登入的由UsernamePasswordAuthenticationFilter,對於基本登入的(也就是http.httpBasic)則由BasicAuthenticationFilter來處理。

例如:對於表單登入它會怎麼樣來判斷這個請求會走這個Filter?
對於Filter來說,它會檢查當前的請求中是否有這個Filter所需要的資訊,對於UsernamePasswordAuthenticationFilter來說,首先這個請求是否是登入請求,請求中帶沒帶使用者名稱(username)和密碼(password),如果帶了,這會嘗試用這個賬戶名和密碼進行登入,如果沒有帶,則會放行,走到下一個Filter中。

任何一個Filter成功完成了使用者登入以後會在請求上做一個標記(這個使用者認證成功了)

最終會到一個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 自定義登入校驗
會發現,兩次的加密後的密碼是不一致的。
原理
同樣的一個密碼,隨機生成一個鹽值,並且在最後生成密碼串的時候把隨機生成的鹽混到這個串裡面,每次判斷的是可以用隨機生成的鹽+生成的串去比對,最終來判斷密碼是否匹配。這樣就可以避免同一個密碼被反覆盜用。