国产片侵犯亲女视频播放_亚洲精品二区_在线免费国产视频_欧美精品一区二区三区在线_少妇久久久_在线观看av不卡

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務器之家 - 編程語言 - Java教程 - 詳解最簡單易懂的Spring Security 身份認證流程講解

詳解最簡單易懂的Spring Security 身份認證流程講解

2021-07-27 11:40曾俊杰的專欄 Java教程

這篇文章主要介紹了詳解最簡單易懂的Spring Security 身份認證流程講解,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧

最簡單易懂的spring security 身份認證流程講解

導言

相信大伙對spring security這個框架又愛又恨,愛它的強大,恨它的繁瑣,其實這是一個誤區(qū),spring security確實非常繁瑣,繁瑣到讓人生厭。討厭也木有辦法呀,作為javaee的工程師們還是要面對的,在開始之前,先打一下比方(比方好可憐):

spring security 就像一個行政服務中心,如果我們去里面辦事,可以辦啥事呢?可以小到咨詢簡單問題、查詢社保信息,也可以戶籍登記、補辦身份證,同樣也可以大到企業(yè)事項、各種復雜的資質辦理。但是我們并不需要跑一次行政服務中心,就挨個把業(yè)務全部辦理一遍,現(xiàn)實中沒有這樣的人吧。

啥意思呢,就是說選擇您需要的服務(功能),無視那些不需要的,等有需要的時候再了解不遲。這也是給眾多工程師們的一個建議,特別是體系異常龐大的java系,別動不動就精通,擼遍源碼之類的,真沒啥意義,我大腦的存儲比較小,人生苦短,沒必要。

回到正題!本文會以一種比較輕松的方式展開,不會是堆代碼。

關于身份認證

web 身份認證是一個后端工程師永遠無法避開的領域,身份認證authentication,和授權authorization是不同的,authentication指的是用戶身份的認證,并不介入這個用戶能夠做什么,不能夠做什么,僅僅是確認存在這個用戶而已。而authorization授權是建立的認證的基礎上的,存在這個用戶了,再來約定這個用戶能補能夠做一件事,這點大家要區(qū)分開。本文講的是authentication的故事,并不會關注權限。

熱熱身,讓我們來溫習一下身份認證的方式演變:

先是最著名的入門留言板程序,相信很多做后端的工程師都做過留言板,那是一個基本沒有框架的階段,回想一下是怎么認證的。表單輸入用戶名密碼submit,然后后端取到數(shù)據(jù)數(shù)據(jù)庫查詢,查不到的話無情地拋出一個異常,哦,密碼錯了;查到了,愉快的將用戶id和相關信息加密寫入到session標識中存起來,響應寫入cookie,后續(xù)的請求都解密后驗證就行了,對吧。是的,身認證真可以簡單到僅僅是匹配session標識而已。令人沮喪的是現(xiàn)代互聯(lián)網(wǎng)的發(fā)展早已經過了 web2.0 的時代,客戶端的出現(xiàn)讓身份認證更加復雜。我們繼續(xù)

隨著移動端的崛起,android和ios占據(jù)主導,同樣是用戶登錄認證,取到用戶信息,正準備按圖索驥寫入session回寫cookie的時候,等等!啥?android不支持cookie?這聽起來不科學是吧,有點反人類是吧,有點手足無措是吧。

嘿嘿,聰明的人兒也許想到了辦法,嗯,android客戶端不是有本地存儲嗎?把回傳的數(shù)據(jù)存起來不就行了嗎?又要抱歉了,android本地存儲并沒有瀏覽器cookie那么人性化,不會自動過期。沒事,再注明過期時間,每次讀取的時候判斷就行啦,貌似可以了。

等等。客戶端的api接口要求輕量級,某一天一個隊友想實現(xiàn)個性化的事情,竟然往cookie了回傳了一串字符串,貌似很方便,嗯。于是其他隊友也效仿,然后cookie變得更加復雜。此時android隊友一聲吼,你們夠了!stop!我只要一個認證標識而已,夠簡單你們知道嗎?還有cookie過期了就要重新登陸,用戶體驗極差,產品經理都找我談了幾十次了,用戶都快跑光了,你們還在往cookie里加一些奇怪的東西。

oauth 2.0來了

有問題總要想辦法解決是吧。客戶端不是瀏覽器,有自己特有的交互約定,cookie還是放棄掉了。這里就要解決五個問題:

  • [ ] 只需要簡單的一個字符串標識,不需要遵守cookie的規(guī)則
  •  [ ] 服務器端需要能夠輕松認證這個標識,最好是做成標準化
  • [ ] 不要讓用戶反復輸入密碼登錄,能夠自動刷新
  • [ ] 這段秘鑰要安全,從網(wǎng)絡傳輸鏈路層到客戶端本地層都要是安全的,就算被中途捕獲,也可以讓其失效
  • [ ] 多個子系統(tǒng)的客戶端需要獨立的認證標識,讓他們能夠獨立存在(例如淘寶的認證狀態(tài)不會影響到阿里旺旺的登錄認證狀態(tài))

需求一旦確定,方案呼之欲出,讓我們來簡單構思一下。

  • [x] 首先是標識,這個最簡單了,將用戶標識數(shù)據(jù)進行可逆加密,ok,這個搞定。
  • [x] 然后是標識認證的標準化,最好輕量級,并且讓她不干擾請求的表現(xiàn)方式,例如get和post數(shù)據(jù),聰明的你想到了吧,沒錯,就是header,我們暫且就統(tǒng)一成 userkey 為header名,值就是那個加密過的標識,夠簡潔粗暴吧,后端對每一個請求都攔截處理,如果能夠解密成功并且表示有效,就告訴后邊排隊的小伙伴,這個家伙是自己人,叫xxx,兜里有100塊錢。這個也搞定了。
  • [x] 自動刷新,因為加密標識每次請求都要傳輸,不能放在一起了,而且他們的作用也不一樣,那就頒發(fā)加密標識的時候順便再頒發(fā)一個刷新的秘鑰吧,相當于入職的時候給你一張門禁卡,這個卡需要隨身攜帶,開門簽到少不了它,此外還有一張身份證明,這證明就不需要隨身攜帶了,放家里都行,門禁卡掉了,沒關系,拿著證明到保安大哥那里再領一張門禁卡,證明一次有效,領的時候保安大哥貼心的再給你一張證明。
  • [x] 安全問題,加密可以加強一部分安全性。傳輸鏈路還用說嗎?上https傳輸加密喲。至于客戶端本地的安全是一個哲學問題,嗯嗯嗯。哈哈。我們暫時認為本地私有空間存儲是安全的的,俗話說得好,計算機都被人破解了,還談個雞毛安全呀(所以大家沒事還是不要去root手機了,root之后私有存儲可以被訪問儂造嗎)
  • [x] 子系統(tǒng)獨立問題,這個好辦了。身份認證過程再加入一個因子,暫且叫 client 吧。這樣標識就互不影響了。

打完收工,要開始實現(xiàn)這套系統(tǒng)了。先別急呀,難道沒覺得似曾相識嗎?沒錯就是 oauth 2.0 的 password grant 模式!

spring security 是怎么認證的

先來一段大家很熟悉的代碼:

?
1
2
3
4
5
http.formlogin()
          .loginpage("/auth/login")
          .permitall()
          .failurehandler(loginfailurehandler)
          .successhandler(loginsuccesshandler);

spring security 就像一個害羞的大姑娘,就這么一段鬼知道他是怎么認證的,封裝的有點過哈。不著急先看一張圖:

詳解最簡單易懂的Spring Security 身份認證流程講解

這里做了一個簡化,

根據(jù)javaee的流程,本質就是filter過濾請求,轉發(fā)到不同處理模塊處理,最后經過業(yè)務邏輯處理,返回response的過程。

當請求匹配了我們定義的security filter的時候,就會導向security 模塊進行處理,例如usernamepasswordauthenticationfilter,源碼獻上:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class usernamepasswordauthenticationfilter extends abstractauthenticationprocessingfilter {
  public static final string spring_security_form_username_key = "username";
  public static final string spring_security_form_password_key = "password";
  private string usernameparameter = "username";
  private string passwordparameter = "password";
  private boolean postonly = true;
 
  public usernamepasswordauthenticationfilter() {
    super(new antpathrequestmatcher("/login", "post"));
  }
 
  public authentication attemptauthentication(httpservletrequest request, httpservletresponse response) throws authenticationexception {
    if (this.postonly && !request.getmethod().equals("post")) {
      throw new authenticationserviceexception("authentication method not supported: " + request.getmethod());
    } else {
      string username = this.obtainusername(request);
      string password = this.obtainpassword(request);
      if (username == null) {
        username = "";
      }
 
      if (password == null) {
        password = "";
      }
 
      username = username.trim();
      usernamepasswordauthenticationtoken authrequest = new usernamepasswordauthenticationtoken(username, password);
      this.setdetails(request, authrequest);
      return this.getauthenticationmanager().authenticate(authrequest);
    }
  }
 
  protected string obtainpassword(httpservletrequest request) {
    return request.getparameter(this.passwordparameter);
  }
 
  protected string obtainusername(httpservletrequest request) {
    return request.getparameter(this.usernameparameter);
  }
 
  protected void setdetails(httpservletrequest request, usernamepasswordauthenticationtoken authrequest) {
    authrequest.setdetails(this.authenticationdetailssource.builddetails(request));
  }
 
  public void setusernameparameter(string usernameparameter) {
    assert.hastext(usernameparameter, "username parameter must not be empty or null");
    this.usernameparameter = usernameparameter;
  }
 
  public void setpasswordparameter(string passwordparameter) {
    assert.hastext(passwordparameter, "password parameter must not be empty or null");
    this.passwordparameter = passwordparameter;
  }
 
  public void setpostonly(boolean postonly) {
    this.postonly = postonly;
  }
 
  public final string getusernameparameter() {
    return this.usernameparameter;
  }
 
  public final string getpasswordparameter() {
    return this.passwordparameter;
  }
}

有點復雜是吧,不用擔心,我來做一些偽代碼,讓他看起來更友善,更好理解。注意我寫的單行注釋

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class usernamepasswordauthenticationfilter extends abstractauthenticationprocessingfilter {
  public static final string spring_security_form_username_key = "username";
  public static final string spring_security_form_password_key = "password";
  private string usernameparameter = "username";
  private string passwordparameter = "password";
  private boolean postonly = true;
 
  public usernamepasswordauthenticationfilter() {
    //1.匹配url和method
    super(new antpathrequestmatcher("/login", "post"));
  }
 
  public authentication attemptauthentication(httpservletrequest request, httpservletresponse response) throws authenticationexception {
    if (this.postonly && !request.getmethod().equals("post")) {
      //啥?你沒有用post方法,給你一個異常,自己反思去
      throw new authenticationserviceexception("authentication method not supported: " + request.getmethod());
    } else {
      //從請求中獲取參數(shù)
      string username = this.obtainusername(request);
      string password = this.obtainpassword(request);
      //我不知道用戶名密碼是不是對的,所以構造一個未認證的token先
      usernamepasswordauthenticationtoken token = new usernamepasswordauthenticationtoken(username, password);
      //順便把請求和token存起來
      this.setdetails(request, token);
      //token給誰處理呢?當然是給當前的authenticationmanager嘍
      return this.getauthenticationmanager().authenticate(token);
    }
  }
}

是不是很清晰,問題又來了,token是什么鬼?為啥還有已認證和未認證的區(qū)別?別著急,咱們順藤摸瓜,來看看token長啥樣。上usernamepasswordauthenticationtoken:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class usernamepasswordauthenticationtoken extends abstractauthenticationtoken {
  private static final long serialversionuid = 510l;
  private final object principal;
  private object credentials;
 
  public usernamepasswordauthenticationtoken(object principal, object credentials) {
    super((collection)null);
    this.principal = principal;
    this.credentials = credentials;
    this.setauthenticated(false);
  }
 
  public usernamepasswordauthenticationtoken(object principal, object credentials, collection<? extends grantedauthority> authorities) {
    super(authorities);
    this.principal = principal;
    this.credentials = credentials;
    super.setauthenticated(true);
  }
 
  public object getcredentials() {
    return this.credentials;
  }
 
  public object getprincipal() {
    return this.principal;
  }
 
  public void setauthenticated(boolean isauthenticated) throws illegalargumentexception {
    if (isauthenticated) {
      throw new illegalargumentexception("cannot set this token to trusted - use constructor which takes a grantedauthority list instead");
    } else {
      super.setauthenticated(false);
    }
  }
 
  public void erasecredentials() {
    super.erasecredentials();
    this.credentials = null;
  }
}

一坨坨的真鬧心,我再備注一下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class usernamepasswordauthenticationtoken extends abstractauthenticationtoken {
  private static final long serialversionuid = 510l;
  //隨便怎么理解吧,暫且理解為認證標識吧,沒看到是一個object么
  private final object principal;
  //同上
  private object credentials;
 
  //這個構造方法用來初始化一個沒有認證的token實例
  public usernamepasswordauthenticationtoken(object principal, object credentials) {
    super((collection)null);
    this.principal = principal;
    this.credentials = credentials;
    this.setauthenticated(false);
  }
  //這個構造方法用來初始化一個已經認證的token實例,為啥要多此一舉,不能直接set狀態(tài)么,不著急,往后看
  public usernamepasswordauthenticationtoken(object principal, object credentials, collection<? extends grantedauthority> authorities) {
    super(authorities);
    this.principal = principal;
    this.credentials = credentials;
    super.setauthenticated(true);
  }
  //便于理解無視他
  public object getcredentials() {
    return this.credentials;
  }
  //便于理解無視他
  public object getprincipal() {
    return this.principal;
  }
 
  public void setauthenticated(boolean isauthenticated) throws illegalargumentexception {
    if (isauthenticated) {
      //如果是set認證狀態(tài),就無情的給一個異常,意思是:
      //不要在這里設置已認證,不要在這里設置已認證,不要在這里設置已認證
      //應該從構造方法里創(chuàng)建,別忘了要帶上用戶信息和權限列表哦
      //原來如此,是避免犯錯吧
      throw new illegalargumentexception("cannot set this token to trusted - use constructor which takes a grantedauthority list instead");
    } else {
      super.setauthenticated(false);
    }
  }
 
  public void erasecredentials() {
    super.erasecredentials();
    this.credentials = null;
  }
}

搞清楚了token是什么鬼,其實只是一個載體而已啦。接下來進入核心環(huán)節(jié),authenticationmanager是怎么處理的。這里我簡單的過渡一下,但是會讓你明白。

authenticationmanager會注冊多種authenticationprovider,例如usernamepassword對應的daoauthenticationprovider,既然有多種選擇,那怎么確定使用哪個provider呢?我截取了一段源碼,大家一看便知:

?
1
2
3
4
5
public interface authenticationprovider {
  authentication authenticate(authentication var1) throws authenticationexception;
 
  boolean supports(class<?> var1);
}

這是一個接口,我喜歡接口,簡潔明了。里面有一個supports方法,返回時一個boolean值,參數(shù)是一個class,沒錯,這里就是根據(jù)token的類來確定用什么provider來處理,大家還記得前面的那段代碼嗎?

?
1
2
//token給誰處理呢?當然是給當前的authenticationmanager嘍
 return this.getauthenticationmanager().authenticate(token);

因此我們進入下一步,daoauthenticationprovider,繼承了abstractuserdetailsauthenticationprovider,恭喜您再堅持一會就到曙光啦。這個比較復雜,為了不讓你跑掉,我將兩個復雜的類合并,摘取直接觸達接口核心的邏輯,直接上代碼,會有所刪減,讓你看得更清楚,注意看注釋:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class daoauthenticationprovider extends abstractuserdetailsauthenticationprovider {
  //熟悉的supports,需要usernamepasswordauthenticationtoken
  public boolean supports(class<?> authentication) {
      return usernamepasswordauthenticationtoken.class.isassignablefrom(authentication);
    }
 
  public authentication authenticate(authentication authentication) throws authenticationexception {
      //取出token里保存的值
      string username = authentication.getprincipal() == null ? "none_provided" : authentication.getname();
      boolean cachewasused = true;
      //從緩存取
      userdetails user = this.usercache.getuserfromcache(username);
      if (user == null) {
        cachewasused = false;
 
        //啥,沒緩存?使用retrieveuser方法獲取呀
        user = this.retrieveuser(username, (usernamepasswordauthenticationtoken)authentication);
      }
      //...刪減了一大部分,這樣更簡潔
      object principaltoreturn = user;
      if (this.forceprincipalasstring) {
        principaltoreturn = user.getusername();
      }
 
      return this.createsuccessauthentication(principaltoreturn, authentication, user);
    }
     protected final userdetails retrieveuser(string username, usernamepasswordauthenticationtoken authentication) throws authenticationexception {
    try {
      //熟悉的loaduserbyusername
      userdetails loadeduser = this.getuserdetailsservice().loaduserbyusername(username);
      if (loadeduser == null) {
        throw new internalauthenticationserviceexception("userdetailsservice returned null, which is an interface contract violation");
      } else {
        return loadeduser;
      }
    } catch (usernamenotfoundexception var4) {
      this.mitigateagainsttimingattack(authentication);
      throw var4;
    } catch (internalauthenticationserviceexception var5) {
      throw var5;
    } catch (exception var6) {
      throw new internalauthenticationserviceexception(var6.getmessage(), var6);
    }
  }
  //檢驗密碼
  protected void additionalauthenticationchecks(userdetails userdetails, usernamepasswordauthenticationtoken authentication) throws authenticationexception {
    if (authentication.getcredentials() == null) {
      this.logger.debug("authentication failed: no credentials provided");
      throw new badcredentialsexception(this.messages.getmessage("abstractuserdetailsauthenticationprovider.badcredentials", "bad credentials"));
    } else {
      string presentedpassword = authentication.getcredentials().tostring();
      if (!this.passwordencoder.matches(presentedpassword, userdetails.getpassword())) {
        this.logger.debug("authentication failed: password does not match stored value");
        throw new badcredentialsexception(this.messages.getmessage("abstractuserdetailsauthenticationprovider.badcredentials", "bad credentials"));
      }
    }
  }
}

到此為止,就完成了用戶名密碼的認證校驗邏輯,根據(jù)認證用戶的信息,系統(tǒng)做相應的session持久化和cookie回寫操作。

spring security的基本認證流程先寫到這里,其實復雜的背后是一些預定,熟悉了之后就不難了。

filter->構造token->authenticationmanager->轉給provider處理->認證處理成功后續(xù)操作或者不通過拋異常

有了這些基礎,后面我們再來擴展短信驗證碼登錄,以及基于oauth 2.0 的短信驗證碼登錄。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。

延伸 · 閱讀

精彩推薦
Weibo Article 1 Weibo Article 2 Weibo Article 3 Weibo Article 4 Weibo Article 5 Weibo Article 6 Weibo Article 7 Weibo Article 8 Weibo Article 9 Weibo Article 10 Weibo Article 11 Weibo Article 12 Weibo Article 13 Weibo Article 14 Weibo Article 15 Weibo Article 16 Weibo Article 17 Weibo Article 18 Weibo Article 19 Weibo Article 20 Weibo Article 21 Weibo Article 22 Weibo Article 23 Weibo Article 24 Weibo Article 25 Weibo Article 26 Weibo Article 27 Weibo Article 28 Weibo Article 29 Weibo Article 30 Weibo Article 31 Weibo Article 32 Weibo Article 33 Weibo Article 34 Weibo Article 35 Weibo Article 36 Weibo Article 37 Weibo Article 38 Weibo Article 39 Weibo Article 40
主站蜘蛛池模板: 久久不色| 日本中文字幕在线 | 久久久精品综合 | 久久精品国产亚洲一区二区三区 | 午夜视频免费在线观看 | 激情综合网婷婷 | 亚洲va欧美va人人爽成人影院 | 在线观看中文字幕 | 中文字幕免费视频 | 欧美视频在线观看免费 | 一区二区av在线 | 欧美成人免费在线视频 | 国产精品99久久 | 日韩美女毛片 | 欧美在线亚洲 | 欧美日一区 | 激情久久久 | 99国产精品久久久久久久久久 | 欧美国产一区二区 | 在线观看亚洲 | 91视频8mav | 九色在线观看 | 俺去俺来也在线www色官网 | 97av在线 | 久久视精品 | 久久老妇 | 欧美黄色一级片免费看 | 亚洲成人久久久 | 久久久精品日本 | 久久国产精品二区 | av成人在线观看 | 亚洲欧美视频在线播放 | 亚洲精品在线视频观看 | 在线观看日韩精品 | 一区二区在线视频 | 亚洲欧美日韩精品 | 韩日一区二区 | 亚洲国产成人精品女 | 免费一区| 日韩欧美在线一区 | 欧美精品理论片大全 |