SpringSecurity身份認(rèn)證的示例分析

這篇文章將為大家詳細(xì)講解有關(guān)Spring Security身份認(rèn)證的示例分析,小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。

創(chuàng)新互聯(lián)公司是一家專業(yè)提供鐵嶺企業(yè)網(wǎng)站建設(shè),專注與成都網(wǎng)站設(shè)計(jì)、成都做網(wǎng)站、外貿(mào)網(wǎng)站建設(shè)、H5頁面制作、小程序制作等業(yè)務(wù)。10年已為鐵嶺眾多企業(yè)、政府機(jī)構(gòu)等服務(wù)。創(chuàng)新互聯(lián)專業(yè)網(wǎng)站建設(shè)公司優(yōu)惠進(jìn)行中。

導(dǎo)言

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

Spring Security 就像一個(gè)行政服務(wù)中心,如果我們?nèi)ダ锩孓k事,可以辦啥事呢?可以小到咨詢簡單問題、查詢社保信息,也可以戶籍登記、補(bǔ)辦身份證,同樣也可以大到企業(yè)事項(xiàng)、各種復(fù)雜的資質(zhì)辦理。但是我們并不需要跑一次行政服務(wù)中心,就挨個(gè)把業(yè)務(wù)全部辦理一遍,現(xiàn)實(shí)中沒有這樣的人吧。

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

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

關(guān)于身份認(rèn)證

Web 身份認(rèn)證是一個(gè)后端工程師永遠(yuǎn)無法避開的領(lǐng)域,身份認(rèn)證Authentication,和授權(quán)Authorization是不同的,Authentication指的是用戶身份的認(rèn)證,并不介入這個(gè)用戶能夠做什么,不能夠做什么,僅僅是確認(rèn)存在這個(gè)用戶而已。而Authorization授權(quán)是建立的認(rèn)證的基礎(chǔ)上的,存在這個(gè)用戶了,再來約定這個(gè)用戶能補(bǔ)能夠做一件事,這點(diǎn)大家要區(qū)分開。本文講的是Authentication的故事,并不會(huì)關(guān)注權(quán)限。

熱熱身,讓我們來溫習(xí)一下身份認(rèn)證的方式演變:

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

隨著移動(dòng)端的崛起,Android和ios占據(jù)主導(dǎo),同樣是用戶登錄認(rèn)證,取到用戶信息,正準(zhǔn)備按圖索驥寫入Session回寫Cookie的時(shí)候,等等!啥?Android不支持Cookie?這聽起來不科學(xué)是吧,有點(diǎn)反人類是吧,有點(diǎn)手足無措是吧。

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

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

Oauth 2.0來了

有問題總要想辦法解決是吧??蛻舳瞬皇菫g覽器,有自己特有的交互約定,Cookie還是放棄掉了。這里就要解決五個(gè)問題:

  • [ ] 只需要簡單的一個(gè)字符串標(biāo)識(shí),不需要遵守Cookie的規(guī)則

  •  [ ] 服務(wù)器端需要能夠輕松認(rèn)證這個(gè)標(biāo)識(shí),最好是做成標(biāo)準(zhǔn)化

  • [ ] 不要讓用戶反復(fù)輸入密碼登錄,能夠自動(dòng)刷新

  • [ ] 這段秘鑰要安全,從網(wǎng)絡(luò)傳輸鏈路層到客戶端本地層都要是安全的,就算被中途捕獲,也可以讓其失效

  • [ ] 多個(gè)子系統(tǒng)的客戶端需要獨(dú)立的認(rèn)證標(biāo)識(shí),讓他們能夠獨(dú)立存在(例如淘寶的認(rèn)證狀態(tài)不會(huì)影響到阿里旺旺的登錄認(rèn)證狀態(tài))

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

  • [x] 首先是標(biāo)識(shí),這個(gè)最簡單了,將用戶標(biāo)識(shí)數(shù)據(jù)進(jìn)行可逆加密,OK,這個(gè)搞定。

  • [x] 然后是標(biāo)識(shí)認(rèn)證的標(biāo)準(zhǔn)化,最好輕量級(jí),并且讓她不干擾請(qǐng)求的表現(xiàn)方式,例如Get和Post數(shù)據(jù),聰明的你想到了吧,沒錯(cuò),就是Header,我們暫且就統(tǒng)一成 Userkey 為Header名,值就是那個(gè)加密過的標(biāo)識(shí),夠簡潔粗暴吧,后端對(duì)每一個(gè)請(qǐng)求都攔截處理,如果能夠解密成功并且表示有效,就告訴后邊排隊(duì)的小伙伴,這個(gè)家伙是自己人,叫xxx,兜里有100塊錢。這個(gè)也搞定了。

  • [x] 自動(dòng)刷新,因?yàn)榧用軜?biāo)識(shí)每次請(qǐng)求都要傳輸,不能放在一起了,而且他們的作用也不一樣,那就頒發(fā)加密標(biāo)識(shí)的時(shí)候順便再頒發(fā)一個(gè)刷新的秘鑰吧,相當(dāng)于入職的時(shí)候給你一張門禁卡,這個(gè)卡需要隨身攜帶,開門簽到少不了它,此外還有一張身份證明,這證明就不需要隨身攜帶了,放家里都行,門禁卡掉了,沒關(guān)系,拿著證明到保安大哥那里再領(lǐng)一張門禁卡,證明一次有效,領(lǐng)的時(shí)候保安大哥貼心的再給你一張證明。

  • [x] 安全問題,加密可以加強(qiáng)一部分安全性。傳輸鏈路還用說嗎?上Https傳輸加密喲。至于客戶端本地的安全是一個(gè)哲學(xué)問題,嗯嗯嗯。哈哈。我們暫時(shí)認(rèn)為本地私有空間存儲(chǔ)是安全的的,俗話說得好,計(jì)算機(jī)都被人破解了,還談個(gè)雞毛安全呀(所以大家沒事還是不要去ROOT手機(jī)了,ROOT之后私有存儲(chǔ)可以被訪問儂造嗎)

  • [x] 子系統(tǒng)獨(dú)立問題,這個(gè)好辦了。身份認(rèn)證過程再加入一個(gè)因子,暫且叫 Client 吧。這樣標(biāo)識(shí)就互不影響了。

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

Spring Security 是怎么認(rèn)證的

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

http.formLogin()
          .loginPage("/auth/login")
          .permitAll()
          .failureHandler(loginFailureHandler)
          .successHandler(loginSuccessHandler);

Spring Security 就像一個(gè)害羞的大姑娘,就這么一段鬼知道他是怎么認(rèn)證的,封裝的有點(diǎn)過哈。不著急先看一張圖:

Spring Security身份認(rèn)證的示例分析

這里做了一個(gè)簡化,

根據(jù)JavaEE的流程,本質(zhì)就是Filter過濾請(qǐng)求,轉(zhuǎn)發(fā)到不同處理模塊處理,最后經(jīng)過業(yè)務(wù)邏輯處理,返回Response的過程。

當(dāng)請(qǐng)求匹配了我們定義的Security Filter的時(shí)候,就會(huì)導(dǎo)向Security 模塊進(jìn)行處理,例如UsernamePasswordAuthenticationFilter,源碼獻(xiàn)上:

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;
  }
}

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

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方法,給你一個(gè)異常,自己反思去
      throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    } else {
      //從請(qǐng)求中獲取參數(shù)
      String username = this.obtainUsername(request);
      String password = this.obtainPassword(request);
      //我不知道用戶名密碼是不是對(duì)的,所以構(gòu)造一個(gè)未認(rèn)證的Token先
      UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
      //順便把請(qǐng)求和Token存起來
      this.setDetails(request, token);
      //Token給誰處理呢?當(dāng)然是給當(dāng)前的AuthenticationManager嘍
      return this.getAuthenticationManager().authenticate(token);
    }
  }
}

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

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;
  }
}

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

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
  private static final long serialVersionUID = 510L;
  //隨便怎么理解吧,暫且理解為認(rèn)證標(biāo)識(shí)吧,沒看到是一個(gè)Object么
  private final Object principal;
  //同上
  private Object credentials;

  //這個(gè)構(gòu)造方法用來初始化一個(gè)沒有認(rèn)證的Token實(shí)例
  public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
    super((Collection)null);
    this.principal = principal;
    this.credentials = credentials;
    this.setAuthenticated(false);
  }
  //這個(gè)構(gòu)造方法用來初始化一個(gè)已經(jīng)認(rèn)證的Token實(shí)例,為啥要多此一舉,不能直接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認(rèn)證狀態(tài),就無情的給一個(gè)異常,意思是:
      //不要在這里設(shè)置已認(rèn)證,不要在這里設(shè)置已認(rèn)證,不要在這里設(shè)置已認(rèn)證
      //應(yīng)該從構(gòu)造方法里創(chuàng)建,別忘了要帶上用戶信息和權(quán)限列表哦
      //原來如此,是避免犯錯(cuò)吧
      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是什么鬼,其實(shí)只是一個(gè)載體而已啦。接下來進(jìn)入核心環(huán)節(jié),AuthenticationManager是怎么處理的。這里我簡單的過渡一下,但是會(huì)讓你明白。

AuthenticationManager會(huì)注冊(cè)多種AuthenticationProvider,例如UsernamePassword對(duì)應(yīng)的DaoAuthenticationProvider,既然有多種選擇,那怎么確定使用哪個(gè)Provider呢?我截取了一段源碼,大家一看便知:

public interface AuthenticationProvider {
  Authentication authenticate(Authentication var1) throws AuthenticationException;

  boolean supports(Class<?> var1);
}

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

//Token給誰處理呢?當(dāng)然是給當(dāng)前的AuthenticationManager嘍
 return this.getAuthenticationManager().authenticate(token);

因此我們進(jìn)入下一步,DaoAuthenticationProvider,繼承了AbstractUserDetailsAuthenticationProvider,恭喜您再堅(jiān)持一會(huì)就到曙光啦。這個(gè)比較復(fù)雜,為了不讓你跑掉,我將兩個(gè)復(fù)雜的類合并,摘取直接觸達(dá)接口核心的邏輯,直接上代碼,會(huì)有所刪減,讓你看得更清楚,注意看注釋:

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);
    }
  }
  //檢驗(yàn)密碼
  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"));
      }
    }
  }
}

關(guān)于“Spring Security身份認(rèn)證的示例分析”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,使各位可以學(xué)到更多知識(shí),如果覺得文章不錯(cuò),請(qǐng)把它分享出去讓更多的人看到。

當(dāng)前名稱:SpringSecurity身份認(rèn)證的示例分析
當(dāng)前網(wǎng)址:http://muchs.cn/article38/pdjipp.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供移動(dòng)網(wǎng)站建設(shè)用戶體驗(yàn)、品牌網(wǎng)站建設(shè)、網(wǎng)站設(shè)計(jì)、電子商務(wù)、網(wǎng)站設(shè)計(jì)公司

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

手機(jī)網(wǎng)站建設(shè)