1.爲什麼要作自定義登陸頁面以及校驗
在項目中配置了spring-security的模塊的項目中,spring boot會默認幫咱們生成的一個簡潔的登陸頁面,它會在咱們訪問任何請求的時候彈出來spring
用戶名是默認的:user,密碼是須要咱們找到咱們啓動項目時的日誌,裏面會隨機生成一個默認的密碼
輸入以後就能夠訪問到咱們的資源信息了
但這種場景給咱們的項目場景確定截然不同了,不論是登陸的頁面仍是校驗的規則在實際的項目中都是比較複雜的。
這個時候就須要咱們自定義登陸頁面以及自定義校驗了數據庫
2.如何自定義
須要繼承WebSecurityConfigurerAdapter這個類,並重寫configure方法
2.1 一個簡單的基於表單登陸
可是configure有三個方法(從源碼可知),分別接收不一樣的參數,咱們應該重寫哪個呢?
回到咱們原來的設想,是想讓程序使用表單進行登陸
首先配置一個最簡單的,基於表單認證的一個頁面配置
這個時候咱們須要覆蓋configure(HttpSecurity http)方法瀏覽器
protected void configure(HttpSecurity http) throws Exception { //用表單登陸,進行身份認證,全部的請求都須要進行身份認證才能夠訪問 http.formLogin() //表單登陸的意思(指定了身份認證的方式) //受權 .and() //對請求進行一個受權 .authorizeRequests() .anyRequest().authenticated();//任何請求都須要身份認證 }
這個時候 啓動項目,再次訪問請求,會看到一個表單頁面(也是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
做用:用來認證用戶的身份,每一個過濾器負責一種認證方式
對於剛纔咱們的登陸而言:對於表單登陸的由UsernamePasswordAuthenticationFilter,對於基本登陸的(也就是http.httpBasic)則由BasicAuthenticationFilter來處理。this
例如:對於表單登陸它會怎麼樣來判斷這個請求會走這個Filter?
對於Filter來講,它會檢查當前的請求中是否有這個Filter所須要的信息,對於UsernamePasswordAuthenticationFilter來講,首先這個請求是不是登陸請求,請求中帶沒帶用戶名(username)和密碼(password),若是帶了,這會嘗試用這個帳戶名和密碼進行登陸,若是沒有帶,則會放行,走到下一個Filter中。加密
任何一個Filter成功完成了用戶登陸之後會在請求上作一個標記(這個用戶認證成功了)3d
最終會到一個FilterSecurityInterceptor過濾器中,是該過濾器鏈的最後一環。
在這個過濾器中它會決定你當前的請求能不能去訪問後面的Controller,依據什麼來判斷呢?依據咱們configure方法中所配置的。
咱們如今的配置是:全部的請求都須要通過身份認證才能訪問,此時這個Filter會判斷當前的請求通過了前面的某一個Filter的身份認證。調試
針對複雜場景:針對某些請求,只能有VIP用戶才能訪問,這些規則都會被放在這個FilterSecurityInterceptor裏面,這個過濾器會根據這些規則作判斷,判斷的結果是過仍是不過,不過的話,會根據不一樣的緣由拋出不一樣的異常。好比 若是沒有通過身份認證,則拋出一個沒身份認證的一個異常;若是隻有VIP才能訪問這個請求,那麼也須要拋出異常,由於權限不夠。
在異常拋出去以後,會有一個異常過濾器來攔截ExceptionTranslationFilter,這個異常過濾器就是用來捕獲FilterSecurityInterceptor裏面所拋出來的異常,它會根據裏面拋出來的異常作響應的處理,若是沒有沒有登陸則引導用戶去登陸等。日誌
在過濾器鏈中,除了綠色的過濾器(UsernamePasswordAuthenticationFilter/BasicAuthenticationFilter/...)是能夠經過配置來禁用或者啓用的,對於其餘顏色的過濾器都是不能控制的,必定會在過濾器鏈上,並且位置是不可變的。
2.3經過調試解析spring security登陸的一個過程
咱們須要在四個類裏面打上斷點
1.第一個斷點(Controller層)
2.第二個斷點:(FilterSecurityInterceptor層)
3.第三個斷點:(ExceptionTranslationFilter層)
4.第四個斷點:(UsernamePasswordAuthenticationFilter層)
從UsernamePasswordAuthenticationFilter這個過濾器中,它只會處理/login post的請求
收到請求只會,它會從request中拿到用戶名和密碼,而後作登陸。
發送請求,由於請求的參數中沒有username/password參數,直接就到了最後一個過濾器(FilterSecurityInterceptor)由它來判斷是否被攔截。
由於咱們配置了全部請求都須要進行身份認證,斷點進行下一步時,則會拋出來一個異常(ExceptionTranslationFilter)
捕獲到的是未受權異常。這個時候前臺頁面就會回到了登陸頁面
在登陸頁面輸入完,正確的帳戶名和密碼以後,斷點會來到
UsernamePasswordAuthenticationFilter
它會從request中取到用戶名和密碼進行校驗,校驗成功後會繼續回到了
(FilterSecurityInterceptor)中的super.beforeInvocation(fi)方法,爲何呢?由於以前的請求是
http://localhost:8080/user/2 它是這個資源訪問(Controller),在登陸成功以後會有個資源的跳轉。
在doFilter以後就調到咱們本身的Controller中的內容了。
所有完成以後,在頁面的前臺就能夠看到請求的信息了。
2.4自定義登陸帳戶校驗
爲何須要自定義呢,由於這個咱們的業務場景不只僅在是個簡單帳戶的校驗,須要根據咱們本身的業務邏輯來實現相關的功能。
相關功能點:
用戶信息的獲取邏輯在spring security中是被封裝在一個接口中(UserDetailsService)
在此接口中只有一個loadUserByUsername方法
根據用戶在前臺傳輸過來的用戶名來查詢用戶信息,用戶的信息被封裝在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接口
咱們須要用到它的方法
第一個參數就是用戶名,第二個參數就是密碼,第三個參數就是咱們的角色(作受權)。
此時咱們就能夠在瀏覽器上訪問咱們的請求地址了:http://localhost:8080/login
帳號隨便,密碼爲123456,輸入錯誤的密碼則會被拋出來異常
密碼正確,則被正常放行。
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);
再次啓動,登陸的用戶信息都會是已經失效的用戶
處理密碼加密解密
處理加密和解密是一個新的接口(PasswordEncoder)。
是org.springframework.security.crypto.password包下面的類。
加密方法是咱們本身調用的(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加密後的密碼:
當咱們使用同一帳戶進行反覆登陸時繼續觀察日誌
會發現,兩次的加密後的密碼是不一致的。
原理一樣的一個密碼,隨機生成一個鹽值,而且在最後生成密碼串的時候把隨機生成的鹽混到這個串裏面,每次判斷的是能夠用隨機生成的鹽+生成的串去比對,最終來判斷密碼是否匹配。這樣就能夠避免同一個密碼被反覆盜用。