整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          若依開發框架解析筆記(4)-登陸驗證部分

          若依開發框架解析筆記(4)-登陸驗證部分

          前的一篇文章介紹了首頁如何生成驗證碼,這篇筆記主要介紹一下登錄請求的發起與后臺的驗證。

          首先還是打開登陸驗證的頁面,輸入用戶名和密碼通過瀏覽器開發者工具進行調試,很容易就能看到登錄發起的請求,如下圖所示:

          我們可以看到請求的url為:

          http://localhost/dev-api/login

          我們可以去后端工程中去尋找/login相關的控制層的代碼。控制層的代碼也在ruoyi-admin工程下面,名字是SysLoginController,其中的部分源碼如下所示:

              /**
               * 登錄方法
               * 
               * @param loginBody 登錄信息
               * @return 結果
               */
              @PostMapping("/login")
              public AjaxResult login(@RequestBody LoginBody loginBody)
              {
                  AjaxResult ajax=AjaxResult.success();
                  // 生成令牌
                  String token=loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                          loginBody.getUuid());
                  ajax.put(Constants.TOKEN, token);
                  return ajax;
              }

          這里分析一下后端介紹到請求之后的邏輯,本文省略掉一些前端部分關于登錄驗證方面的校驗。首先后端接收到登錄的請求,請求體被封裝成了一個對象,控制層中調用service層的方法進行進一步的驗證。

          驗證的核心代碼在控制層對應的服務層中,首先第一步是判斷驗證碼,如果開啟了驗證碼,則會調用一個封裝好的方法,從 redis中讀取答案,其中uuid的用處是用于從 redis中找到本次登錄請求那個驗證碼所對應的正確答案。驗證碼判斷具體的邏輯位于SysLoginService.java中的源碼里面的validateCaptcha這個方法。

          public String login(String username, String password, String code, String uuid)
              {
                  boolean captchaOnOff=configService.selectCaptchaOnOff();
                  // 驗證碼開關
                  if (captchaOnOff)
                  {
                      validateCaptcha(username, code, uuid);
                  }
                  // 用戶驗證
                  Authentication authentication=null;
                  try
                  {
                      // 該方法會去調用UserDetailsServiceImpl.loadUserByUsername
                      authentication=authenticationManager
                              .authenticate(new UsernamePasswordAuthenticationToken(username, password));
                  }
                  catch (Exception e)
                  {
                      if (e instanceof BadCredentialsException)
                      {
                          AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                          throw new UserPasswordNotMatchException();
                      }
                      else
                      {
                          AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
                          throw new ServiceException(e.getMessage());
                      }
                  }
                  AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
                  LoginUser loginUser=(LoginUser) authentication.getPrincipal();
                  recordLoginInfo(loginUser.getUserId());
                  // 生成token
                  return tokenService.createToken(loginUser);
              }

          判斷完驗證碼之后,如果驗證碼正確,下一步就是進行用戶名和密碼的驗證。這部分驗證使用了SpringSecurity,這里是判斷了用戶名和密碼。

          這個框架中驗證和授權都是使用了SpringSecurity。我們首先要關注這么一個配置類SecurityConfig.java,這個類繼承了SpringSecurity的一個WebSecurityConfigurerAdapter,并且重寫了其中的幾個方法,來進行驗證方面的配置,很多配置都是固定的。這里的身份驗證用了jwt,所以這里禁用了SpringSecurity自帶的csrf認證機制,并且通過過濾規則過濾掉了一些頁面。這個項目是前后端分離的項目,這里也沒有專門配置登錄的頁面。

          @Override
              protected void configure(HttpSecurity httpSecurity) throws Exception
              {
                  httpSecurity
                          // CSRF禁用,因為不使用session
                          .csrf().disable()
                          // 認證失敗處理類
                          .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                          // 基于token,所以不需要session
                          .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                          // 過濾請求
                          .authorizeRequests()
                          // 對于登錄login 注冊register 驗證碼captchaImage 允許匿名訪問
                          .antMatchers("/login", "/register", "/captchaImage").anonymous()
                          .antMatchers(
                                  HttpMethod.GET,
                                  "/",
                                  "/*.html",
                                  "/**/*.html",
                                  "/**/*.css",
                                  "/**/*.js",
                                  "/profile/**"
                          ).permitAll()
                          .antMatchers("/swagger-ui.html").anonymous()
                          .antMatchers("/swagger-resources/**").anonymous()
                          .antMatchers("/webjars/**").anonymous()
                          .antMatchers("/*/api-docs").anonymous()
                          .antMatchers("/druid/**").anonymous()
                          // 除上面外的所有請求全部需要鑒權認證
                          .anyRequest().authenticated()
                          .and()
                          .headers().frameOptions().disable();
                  httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
                  // 添加JWT filter
                  httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
                  // 添加CORS filter
                  httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
                  httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
              }

          這里調用自定義用戶認證邏輯,并且對加密過的密碼做一個解密。

          //認證  
          		@Override
              protected void configure(AuthenticationManagerBuilder auth) throws Exception
              {
                  auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
              }

          另外一個需要關注的點,是自定義的用戶認證邏輯,框架中作者實現了UserDetailsService這個接口,具體的代碼在UserDetailsServiceImpl.java中,會通過查詢數據庫判斷用戶是否存在。

           @Override
              public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
              {
                  SysUser user=userService.selectUserByUserName(username);
                  if (StringUtils.isNull(user))
                  {
                      log.info("登錄用戶:{} 不存在.", username);
                      throw new ServiceException("登錄用戶:" + username + " 不存在");
                  }
                  else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
                  {
                      log.info("登錄用戶:{} 已被刪除.", username);
                      throw new ServiceException("對不起,您的賬號:" + username + " 已被刪除");
                  }
                  else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
                  {
                      log.info("登錄用戶:{} 已被停用.", username);
                      throw new ServiceException("對不起,您的賬號:" + username + " 已停用");
                  }
          
                  return createLoginUser(user);
              }


          雖然在登錄驗證密碼的service層中,在判斷完驗證碼之后,進行用戶名和密碼的比對,在其service層中僅僅只是調用了springSecurity一個攔截器方法,但是對于整個項目本身我們還是要進行很多的配置才能正常的使用。

          try
                  {
                      // 該方法會去調用UserDetailsServiceImpl.loadUserByUsername
                      authentication=authenticationManager
                              .authenticate(new UsernamePasswordAuthenticationToken(username, password));
                  }

          整個框架對于驗證的判斷是首先判斷驗證碼,然后是調用SpringSecurity框架進行用戶名和密碼的判斷,并且還會對于用戶名做個自定義的校驗。

          題中的 “傳統Web應用” 這一說法并沒有什么官方定義,只是為了與“現代化Web應用”做比較而自擬的一個概念。所謂“現代化Web應用”指的是那些基于分布式架構思想設計的,面向多個端提供穩定可靠的高可用服務,并且在需要時能夠橫向擴展的Web應用。相對而言,傳統Web應用則主要是直接面向PC用戶的Web應用程序,采用單體架構較多,也可能在內部采用SOA的分布式運算技術。

          一直以來,傳統Web應用為構成互聯網發揮了重要作用。因此傳統Web應用中的身份驗證技術經過幾代的發展,已經解決了不少實際問題,并最終沉淀了一些實踐模式。

          在講述多種身份鑒權技術之前,要強調一點:在構建互聯網Web應用過程中,無論使用哪種技術,在傳輸用戶名和密碼時,請一定要采用安全連接模式。因為無論采用何種鑒權模型,都無法保護用戶憑據在傳輸過程中不被竊取。

          Basic和Digest鑒權

          基于HTTP的Web應用離不開HTTP本身的安全特性中關于身份鑒權的部分。雖然HTTP標準定義了好幾種鑒權方式,但真正供Web應用開發者選擇的并不多,這里簡要回顧一下曾經被廣泛運用過的Basic 和 Digest鑒權。

          不知道讀者是否熟悉一種最直接向服務器提供身份的方式,即在URL中直接寫上用戶名和密碼:

          http://user:passwd@www.server.com/index.html

          這就是Basic鑒權的一種形式。

          Basic和Digest是通過在HTTP請求中直接包含用戶名和密碼,或者它們的哈希值來向服務器傳輸用戶憑據的方法。Basic鑒權直接在每個請求的頭部或URL中包含明文的用戶名或密碼,或者經過Base64編碼過的用戶名或密碼;而Digest則會使用服務器返回的隨機值,對用戶名和密碼拼裝后,使用多次MD5哈希處理后再向服務器傳輸。服務器在處理每個請求之前,讀取收到的憑據,并鑒定用戶的身份。

          Basic和Digest鑒權有一系列的缺陷。它們需要在每個請求中提供憑據,因此提供“記住登錄狀態”功能的網站中,不得不將用戶憑據緩存在瀏覽器中,增加了用戶的安全風險。Basic鑒權基本不對用戶名和密碼等敏感信息進行預處理,所以只適合于較安全的安全環境,如通過HTTPS安全連接傳輸,或者局域網。

          看起來更安全的Digest在非安全連接傳輸過程中,也無法抵御中間人通過篡改響應來要求客戶端降級為Basic鑒權的攻擊。Digest鑒權還有一個缺陷:由于在服務器端需要核對收到的、由客戶端經過多次MD5哈希值的合法性,需要使用原始密碼做相同的運算,這讓服務器無法在存儲密碼之前對其進行不可逆的加密。Basic 和Digest鑒權的缺陷決定了它們不可能在互聯網Web應用中被大量采用。

          簡單實用的登錄技術

          對于互聯網Web應用來說,不采用Basic或Digest鑒權的理由主要有兩個:

          1. 不能接受在每個請求中發送用戶名和密碼憑據

          2. 需要在服務器端對密碼進行不可逆的加密

          因此,互聯網Web應用開發已經形成了一個基本的實踐模式,能夠在服務端對密碼強加密之后存儲,并且盡量減少鑒權過程中對憑據的傳輸。其過程如下圖所示:

          這一過程的原理很簡單,專門發送一個鑒權請求,只在這個請求頭中包含原始用戶名和密碼憑據,經服務器驗證合法之后,由服務器發給一個會話標識(Session ID),客戶端將會話標識存儲在 Cookie 中,服務器記錄會話標識與經過驗證的用戶的對應關系;后續客戶端使用會話標識、而不是原始憑據去與服務器交互,服務器讀取到會話標識后從自身的會話存儲中讀取已在第一個鑒權請求中驗證過的用戶身份。為了保護用戶的原始憑據在傳輸中的安全,只需要為第一個鑒權請求構建安全連接支持。

          服務端的代碼包含首次鑒權和后續檢查并授權訪問的過程:

          IUser _user_;

          if( validateLogin( nameFromReq, pwdFromReq, out _user _)){

          Session["CurrentUser"]=_user_;

          }

          (首次鑒權)

          IUser _user_=Session["CurrentUser"] as IUser;

          if( _user_==null ){

          Response.Redirect( "/login?return_uri=" +

          Request.Url.ToString() );

          return;

          }

          (后續檢查并拒絕未識別的用戶)

          類似這樣的技術簡易方便,容易操作,因此大量被運用于很多互聯網Web應用中。它在客戶端和傳輸憑據過程中幾乎沒有做特殊處理,所以在這兩個環節尤其要注意對用戶憑據的保護。不過,隨著我們對系統的要求越來越復雜,這樣簡易的實現方式也有一些明顯的不足。比如,如果不加以封裝,很容易出現在服務器應用程序代碼中出現大量對用戶身份的重復檢查、錯誤的重定向等;不過最明顯的問題可能是對服務器會話存儲的依賴,服務器程序的會話存儲往往在服務器程序重啟之后丟失,因此可能會導致用戶突然被登出的情況。雖然可以引入單獨的會話存儲程序來避免這類問題,但引入一個新的中間件就會增加系統的復雜性。

          傳統Web應用中身份驗證最佳實踐

          上文提到的簡單實用的登錄技術已經可以幫助建立對用戶身份驗證的基本圖景,在一些簡單的應用場景中已經足夠滿足需求了。然而,用戶鑒權就是有那種“你可以有很多種方法,就是不怎么優雅” 的問題。

          最佳實踐指的是那些經過了大量驗證、被證明有用的方法。而用戶鑒權的最佳實踐就是使用自包含的、含有加密內容的 Cookie 作為替代憑據。其鑒權過程與上文所提到基于會話標識的技術沒有什么區別,而主要區別在于不再頒發會話標識,取而代之的是一個代表身份的、經過加密的 “身份 Cookie”。

          1. 只在鑒權請求中發送一次用戶名和密碼憑據

          2. 成功憑據之后,由服務器生成代表用戶身份的 Cookie,發送給客戶端

          3. 客戶端在后續請求中攜帶上一步中收到的 “身份 Cookie”

          4. 服務器解密”身份 Cookie”,并對需要訪問的資源予以授權

          這樣,我們消除了對服務器會話存儲的依賴,Cookie本身就有有效期的概念,因此順便能夠輕松提供“記住登錄狀態”的功能。

          另外,由于解密Cookie、既而檢查用戶身份的操作相對繁瑣,工程師不得不考慮對其抽取專門的服務,最終采用了面向切面的模式對身份驗證的過程進行了封裝,而開發時只需要使用一些特性標注(Attribute Annotation)對特定資源予以標記,即可輕松完成身份驗證預處理。

          傳統Web應用中的單點登錄

          單點登錄的需求在向用戶提供多種服務的企業普遍存在,出發點是希望用戶在一個站點中登錄之后,在其他兄弟站點中就不需要再次登錄。

          如果多個子站所在的頂級域名一致,基于上文所述的實踐,可以基于Cookie共享實現最簡單的單點登錄:在多個子站中使用相同的加密、解密配置,并且在用戶登錄成功后設置身份 Cookie時將domain值設置為頂級域名即可。這樣,只要在其中一個網站登錄,其身份 Cookie將在用戶訪問其他子站時也一起帶上。不過實際情況中,這個方案的應用場景很有限,畢竟各個子站使用的用戶數據模型可能不完全一致,而加密密鑰多處共享也增加了服務器應用程序的安全風險。另外,這種方式與“在多個網站中分別存儲相同的用戶名與密碼”的做法相似,可以說是一種“相同的登錄”(Same Sign-On),而不是“單點登錄”(Single Sign-On)。

          對于單點登錄需求來說,域名相同與否并不是最大的挑戰,集成登錄系統對各個子站點的系統在設計上的影響才是。我們希望便利用戶的同時,也期待各個子系統仍擁有獨立用戶身份、獨立管理和運維的靈活性。因此我們引入獨立的鑒權子站點。

          當用戶到達業務站點A時,被重定向到鑒權站點;登錄成功之后,用戶被重定向回到業務站點 A、同時附加一個指示“已有用戶登錄”的令牌串——此時業務站點A使用令牌串,在服務器端從鑒權子站點查詢并記錄當前已登錄的用戶。當用戶到達業務站點B時,執行相同流程。由于已有用戶登錄,所以用戶登錄的過程會被自動省略。

          這樣的單點登錄系統能夠較好地解決在多個站點中共享用戶登錄狀態的需求。不過,如果在編程實踐過程中略有差池,就會讓用戶陷入巨大的安全風險中。例如,在上述重定向過程中,一旦鑒權系統未能驗證返回URL的合法性,就容易導致用戶被釣魚網站利用。在傳統Web應用開發實踐中,被廣泛部署的身份驗證體系是比較重量級的WS-Federation 和 SMAL 等鑒權協議和相對輕量級的 OpenID 等技術。

          總結

          本文簡要總結了在傳統Web應用中,被廣泛使用的幾種典型用戶登錄時的鑒權處理流程。總體來說,在單體 Web 應用中,身份驗證過程并不復雜,只要稍加管理,可以較輕松地解決用戶鑒權的問題。但在傳統 Web 應用中,為了解決單點登錄的需求,人們也嘗試了多種方式,最終仍然只有使用一些較復雜的方案才能較好地解決問題。

          在現代化 Web 應用中,圍繞登錄這一需求,儼然已經衍生出了一個新的工程。“登錄工程” 并不簡單,在后續篇目中將會介紹現代化 Web 應用的典型需求及解決方法。

          朋友就職于某大型互聯網公司。前不久,在閑聊間我問他日常工作的內容,他說他所在部門只負責一件事,即用戶與登錄。

          而他的具體工作則是為各個業務子網站提供友好的登錄部件(Widget),從而統一整個網站群的登錄體驗,同時也能令業務開發者不用花費額外的精力去關注用戶鑒權。這很有趣。

          可以看出,在一個現代Web應用中,圍繞“登錄”這一需求,儼然已經衍生出了一個新的工程。不管是我們面臨的需求,還是解決這些需求所運用的方法與工具,都已經超出了傳統Web應用身份驗證技術的范疇。

          在之前一篇文章中,我聊到傳統Web應用中的身份驗證技術,文章中列出的一些方法在之前很長一段時間內,為滿足大量的Web應用中身份驗證的需求提供了思路。在這篇文章里,我將簡要介紹現代Web應用中幾種典型的身份驗證需求。

          形式多樣的鑒權

          考慮這樣一個場景:我們在電腦上登錄了微軟賬號,電腦里的“郵件”應用能夠自動同步郵件;我們登錄Web版本的Outlook郵件服務,如果在郵件里發現了重要的工作安排,將其添加到日歷中,很快電腦里的“日歷”應用便能夠將這些日程顯示到Windows桌面上。

          這個場景包含了多個鑒權過程。至少涉及了對Web版本Outlook服務的鑒權,也涉及了對離線版本的郵件應用的鑒權。要能夠支持同一批用戶既能夠在瀏覽器中登錄,又能夠在移動端或本地應用登錄(例如 Windows UWP 應用程序),就需要開發出能夠為兩種應用程序服務的鑒權體系。

          在瀏覽器里,我們通常假設用戶不信任瀏覽器,用戶通過與服務器建立的臨時瀏覽器會話完成操作。會話開始時,用戶被重定向到特定頁面進行登錄。登錄完成后,用戶通過持續與服務器交互來延續臨時會話的時長;一旦用戶一段時間不與服務器交互,則他的會話很快就會過期(被服務器強制登出)。

          在移動應用中,情況有所不同。相對來說,安裝在移動設備中的應用程序更受用戶信任,移動設備本身的安全性也比瀏覽器更好。另一方面,將用戶重定向到一個網頁去登錄的做法,并不能提供很好的用戶體驗——更重要的是,用戶在使用移動設備時,時間是碎片化的。我們無法要求用戶必須在特定時間內完成操作,也就基本沒有會話的概念:我們需要找到一種能夠安全地在設備中相對持久地存儲用戶憑據的方法,并且Web應用服務器可能需要配合這種方式來完成鑒權。此外,移動設備也不是絕對安全的,一旦設備丟失,將給用戶帶來安全風險。所以需要在服務器端提供一種機制來取消已登錄設備的訪問權限。

          (圖片來自:http://docs.identityserver.io/en/release/intro/big_picture.html)

          方便用戶的多種登錄方式

          “輸入用戶名和密碼”作為標準的登錄憑據被廣泛用于各種登錄場景。不過,在Web應用、尤其是互聯網應用中,網站運營方越來越發現使用用戶名作為用戶標識確實給網站提供了便利,但對用戶來說卻并不是那么有幫助:用戶很可能會忘記自己的用戶名。

          用戶在使用不同網站的過程中,為了不忘記用戶名,只好使用相同的用戶名。如果恰好在某個網站遇到了該用戶名被占用的情況,他就不得不臨時為這個網站擬一個新的用戶名,于是這個新用戶名很快就被忘記了。

          在注冊時,越來越多的網站要求用戶提供電子郵箱地址或者手機號碼,有的網站還支持讓用戶以多種方式登錄。比如,提供一種讓用戶在使用了一種方式注冊之后,還能綁定其他登錄方式的功能。綁定完成之后,用戶可以選用他喜歡的登錄方式。它隱含了一個網站與用戶共同的認知:聯系方式的擁有者即為用戶本人,這種“從屬”關系能夠用于證實用戶的身份。當用戶下次在注冊新網站時遇到“郵件地址已被注冊”,或者“手機號已被注冊”的時候,基本可以確定自己曾經注冊過這個網站了。

          (圖片來自:http://cargocollective.com/)

          另外,登錄過程中所支持的聯系方式也呈現出多樣性。電子郵件服務在很多場景中逐漸被形式多樣的其他聯系方式(比如手機、微信等)所取代,不少人根本沒有使用郵件的習慣,如果網站只提供郵箱注冊的途徑,有時候還會遭到那些不經常使用電子郵箱的用戶的反感。所以支持多種登錄方式成為了很多網站的迫切需求。

          雙因子鑒權:增強型登錄過程

          上一節中提到的“從屬”關系不光可以幫助用戶判斷自己是否注冊過一個網站,也可以幫助網站在忘記密碼時進行臨時認證,從而幫助用戶完成新密碼的設置。如果將這種從屬關系用于正常登錄過程中的進一步驗證,就構成了雙因子鑒權。

          雙因子鑒權要求用戶在登錄過程中提供兩種形式不同的憑據,只有兩種驗證都成功才能繼續操作。現代化Web應用正在越來越多地使用這種增強型驗證方式來保護關鍵操作的安全性。例如,查看和修改個人信息,以及修改登錄密碼等。

          相信不少人還記得QQ密碼保護問題的機制,它使得盜號者即使盜取了QQ密碼,在不知道密碼保護問題的情況下,也無法修改現有密碼,讓賬號擁有者得以及時挽回損失。

          雙因子的原理在于:兩種驗證因子性質不一致,冒用身份者同時獲得用戶這兩種信息的機率十分低,從而能有效地保護賬號的安全。在QQ密碼保護的例子里,密碼是一種每次登錄時都會使用的固定文本、相對容易被盜;而密碼保護問題卻是不怎么頻繁設置和更改的、隱秘的、個人關聯性極強的,不容易被盜。

          (圖片來自:http://bit.ly/2kFc492)

          現代化Web應用形式多樣,設備種類繁多,場景復雜多變,而為了更好地保護用戶賬號的安全,很多應用開始將雙因子驗證作為登錄過程中的鑒權步驟。而為了兼具安全和便利的特點,一些應用還要求運用一些優化策略以提高用戶體驗。比如,僅在用戶在新的設備上登錄、一段時間未登錄之后的再次登錄、在不常用的地點登錄、修改聯系信息和密碼、轉移賬戶資產等關鍵操作時要求雙因子鑒權。

          單點登錄:還是需要精心設計

          以前,一般只有大型網站、向用戶提供多種服務的時候(比如,網易公司運營網易門戶和網易郵箱等多種服務),才會有單點登錄的迫切需求。但在現代化Web系統中,無論是從業務的多元化還是從架構的服務化來考慮,對服務的劃分都更細致了。

          從整個企業的業務模式(例如網易門戶和網易郵箱),到某項業務的具體流程(例如京東訂單和京東支付),再到某個流程中的具體步驟(例如短信驗證與支付扣款),“服務”這一概念越來越輕量級,于是人們不得不創造了“微服務”這個新的品類詞匯來拓展認知空間。

          (圖片來自:http://cargocollective.com/)

          在這整個的演變過程中,出于安全的需要,身份驗證的需求都是一直存在的,而且粒度越來越細。以前我們更關注用戶在多個子站點的統一登錄體驗,現在我們還需要關注用戶在多個子流程中的統一登錄體驗,以及在多個步驟中的統一登錄體驗。而這些流程和步驟,很可能是獨立的Web系統(微服務),也有可能是一個用戶界面(獨立應用),還有可能是一個第三方系統(接口集成)。

          可以說,單點登錄的需求有增無減,只不過當開發者對這種模式已經習以為常,不再意識到這也是一個能夠專門討論的話題。

          考慮與用戶系統集成,與業務系統分離

          在討論安全時,分不開的兩個部分就是鑒權(Authentication)與授權(Authorization)。

          鑒權的過程是向用戶發起質詢(Challenge),完成身份驗證工作。這正是登錄所解決的問題。通常在登錄系統成功識別用戶之后,就會將接下來的工作直接交給業務系統來完成。由于各個系統中的授權模型可能與業務形態有關系,因此登錄與業務系統分離是很自然的設計。

          在對安全要求更嚴格的企業或企業應用中,可能需要專門的訪問管理機制,不過,這樣的做法在互聯網應用中很少見。但在互聯網Web應用中,授權的范疇也包含一個很小的公有部分,是各個業務系統所共有的:即用戶狀態。我們希望在各業務子系統之間共享用戶狀態:用戶被鎖定之后,他在所有業務系統都被鎖定;用戶被注銷之后,所有業務系統中有關他的數據都被封存。

          (圖片來自:http://cargocollective.com/)

          另外在多個業務系統中,還可能會共用用戶的基本資料和偏好設置等數據。比如,類似于郵件地址這樣的資料,它可以作為登錄憑據,也可以作為一個基本的聯系方式。如果用戶在一個子系統設置了偏好語言,其他子系統則直接使用該設置即可。這樣,開發一個“用戶”系統的想法也就應運而生了。由于與用戶的狀態等基礎信息的關系很緊密,登錄與用戶系統之間的集成是很自然的,將登錄子系統直接作為這個用戶系統的一部分也不失為一種不錯的實踐。

          與第三方集成:迎接更多用戶

          “即得”是一個開放式文檔共享應用,特點是“無需登錄,即傳即得”,它利用長時間有效的Cookie來標識用戶,從而免除了人們使用應用之前必須注冊登錄的繁瑣步驟。

          這種做法的風險是,如果用戶有及時清理瀏覽器Cookie的習慣,那很可能導致用戶再一次登錄時不再被識別。不過從這樣一個小例子中,卻容易看出登錄的真正作用,就是Web應用識別用戶的過程,當下次同一個用戶再次使用時,Web應用就能夠知道“這就是上次來過的那個用戶”。

          如果識別用戶這一需求能夠在不需要用戶注冊的前提下搞定,豈不兩全齊美?基于第三方身份提供方的接口來識別已經在其他平臺注冊的用戶,并將其轉化為自己應用中的用戶,這種方式完全可行,并且大量的開發人員已經有了豐富的實踐。

          從 2010 年開始就有不少的大型互聯網公司開始推出開放平臺服務,讓第三方應用通過Web接口與這些互聯網服務交互,從而為他們提供更豐富多彩的功能。在這個過程中,一些應用不為這些平臺提供擴展,卻巧辟蹊徑地利用了這些開放平臺的身份識別接口來免除新用戶注冊的過程,從而為自己的產品快速導入用戶。不少網站都提供“使用微博賬號登錄”功能,相信讀者一定體驗過。

          (圖片來自:http://bit.ly/2kFi3e8)

          如果你的應用需要向第三方提供用戶,那么我們的角色就由“從上下文中讀取用戶身份”變成了“向上下文中寫入用戶身份”了。如果你正好有過與各互聯網公司開放平臺的接口打交道的經歷,這時候,你就可以體驗一把提供開放、安全上下文的挑戰了。如果……你的平臺既希望讓其他平臺的用戶能夠平滑接入,又希望向其他平臺公開自己的用戶,那可能是另一番更有趣的挑戰。這個過程,也可以作為生物驗證之外的另一種間接消除密碼的實踐方式吧。

          登錄,現在實實在在地成為了一個獨立的工程。尤其在形態多樣的基于Web的應用,以及這些Web應用本身所依賴的各色后端服務快速生長的過程中,各種鑒權需求隨之而來。如何在保障各個環節中安全的同時,又為用戶提供良好的體驗,成為一個挑戰。

          另外,個人信息泄露的事件頻繁被曝光,它們導致的社會問題也開始被更多人關注和重視,作為IT系統支撐者的工程師們有責任了解事關安全的基礎知識,并掌握必要的技能去保護用戶數據和企業利益。

          我會在接下來的文章中介紹解決典型登錄需求的具體技術方案,以及相關領域的安全實踐常識。

          登錄系統

          首先,我們要為“登錄”做一個簡要的定義,令后續的講述更準確。之前的兩篇文章有意無意地混淆了“登錄”與“身份驗證”的說法,因為在本篇之前,不少“傳統Web應用”都將對身份的識別看作整個登錄的過程,很少出現像企業應用環境中那樣復雜的情景和需求。但從之前的文章中我們看到,現代Web應用對身份驗證相關的需求已經向復雜化發展了。

          我們有必要重新認識一下登錄系統。登錄指的是從識別用戶身份,到允許用戶訪問其權限相應的資源的過程。舉個例子,在網上買好了票之后去影院觀影的過程就是一個典型的登錄過程:我們先去取票機,輸入驗證碼取票;接著拿到票去影廳檢票進入。取票的過程即身份驗證,它能夠證明我們擁有這張票;而后面檢票的過程,則是授權訪問的過程。之所以要分成這兩個過程,最直接的原因還是業務形態本身具有復雜性——如果觀景過程是免費匿名的,也就免去了這些過程。

          在登錄的過程中,“鑒權”與“授權”是兩個最關鍵的過程。接下來要介紹的一些技術和實踐,也包含在這兩個方面中。雖然現代Web應用的登錄需求比較復雜,但只要處理好了鑒權和授權兩個方面,其余各個方面的問題也將迎刃而解。在現代Web應用的登錄工程實踐中,需要結合傳統Web應用的典型實踐,以及一些新的思路,才能既解決好登錄需求,又能符合Web的輕量級架構思路。

          解析常見的登錄場景

          在簡單的Web系統中,典型的鑒權也就是要求用戶輸入并比對用戶名和密碼的過程,而授權則是確保會話Cookie存在。而在稍微復雜的Web系統中,則需要考慮多種鑒權方式,以及多種授權場景。上一篇文章中所述的“多種登錄方式”和“雙因子鑒權”就是多種鑒權方式的例子。有經驗的人經常調侃說,只要理解了鑒權與授權,就能清晰地理解登錄系統了。不光如此,這也是安全登錄系統的基礎所在。

          鑒權的形式豐富多彩,有傳統的用戶名密碼對、客戶端證書,有人們越來越熟悉的第三方登錄、手機驗證,以及新興的掃碼和指紋等方式,它們都能用于對用戶的身份進行識別。在成功識別用戶之后,在用戶訪問資源或執行操作之前,我們還需要對用戶的操作進行授權。

          在一些特別簡單的情形中——用戶一經識別,就可以無限制地訪問資源、執行所有操作——系統直接對所有“已登錄的人”放行。比如高速公路收費站,只要車輛有合法的號牌即可放行,不需要給駕駛員發一張用于指示“允許行駛的方向或時間”的票據。除了這類特別簡單的情形之外,授權更多時候是比較復雜的工作。

          在單一的傳統Web應用中,授權的過程通常由會話Cookie來完成——只要服務器發現瀏覽器攜帶了對應的Cookie,即允許用戶訪問資源、執行操作。而在瀏覽器之外,例如在Web API調用、移動應用和富 Web 應用等場景中,要提供安全又不失靈活的授權方式,就需要借助令牌技術。

          令牌

          令牌是一個在各種介紹登錄技術的文章中常被提及的概念,也是現代Web應用系統中非常關鍵的技術。令牌是一個非常簡單的概念,它指的是在用戶通過身份驗證之后,為用戶分配的一個臨時憑證。在系統內部,各個子系統只需要以統一的方式正確識別和處理這個憑證即可完成對用戶的訪問和操作進行授權。在上文所提到的例子中,電影票就是一個典型的令牌。影廳門口的工作人員只需要確認來客手持印有對應場次的電影票即視為合法訪問,而不需要理會客戶是從何種渠道取得了電影票(比如自行購買、朋友贈予等),電影票在本場次范圍內可以持續使用(比如可以中場出去休息等)、過期作廢。通過電影票這樣一個簡單的令牌機制,電影票的出售渠道可以豐富多樣,檢票人員的工作卻仍然簡單輕松。

          從這個例子也可以看出令牌并非什么神奇的機制,只是一種很常見的做法。還記得第一篇文章中所述的“自包含的Cookie”嗎?那實際上就是一個令牌而已,而且在令牌中寫有關于有效性的內容——正如一個電影票上會寫明場次與影廳編號一樣。可見,在Web安全系統中引入令牌的做法,有著與傳統場合一樣的妙用。在安全系統中,令牌經常用于包含安全上下文信息,例如被識別的用戶信息、令牌的頒發來源、令牌本身的有效期等。另外,在必要時可以由系統廢止令牌,在它下次被使用用于訪問、操作時,用戶被禁止。

          由于令牌有這些特殊的妙用,因此安全行業對令牌標準的制定工作一直沒有停止過。在現代化Web系統的演進過程中,流行的方式是選用基于Web技術的“簡單”的技術來代替相對復雜、重量級的技術。典型地,比如使用JSON-RPC或REST接口代替了SOAP格式的服務調用,用微服務架構代替了SOA架構等等。而適用于Web技術的令牌標準就是Json Web Token(JWT),它規范了一種基于JSON的令牌的簡單格式,可用于安全地封裝安全上下文信息。

          OAuth 2、Open ID Connect

          令牌在廣為使用的OAuth技術中被采用來完成授權的過程。OAuth是一種開放的授權模型,它規定了一種供資源擁有方與消費方之間簡單又直觀的交互方法,即從消費方向資源擁有方發起使用AccessToken(訪問令牌)簽名的HTTP請求。這種方式讓消費方應用在無需(也無法)獲得用戶憑據的情況下,只要用戶完成鑒權過程并同意消費方以自己的身份調用數據和操作,消費方就可以獲得能夠完成功能的訪問令牌。OAuth簡單的流程和自由的編程模型讓它很好地滿足了開放平臺場景中授權第三方應用使用用戶數據的需求。不少互聯網公司建設開放平臺,將它們的用戶在其平臺上的數據以 API 的形式開放給第三方應用來使用,從而讓用戶享受更豐富的服務。

          OAuth在各個開放平臺的成功使用,令更多開發者了解到它,并被它簡單明確的流程所吸引。此外,OAuth協議規定的是授權模型,并不規定訪問令牌的數據格式,也不限制在整個登錄過程中需要使用的鑒權方法。人們很快發現,只要對OAuth進行合適的利用即可將其用于各種自有系統中的場景。例如,將 Web 服務視作資源擁有方,而將富Web應用或者移動應用視作消費方應用,就與開放平臺的場景完全吻合。

          另一個大量實踐的場景是基于OAuth的單點登錄。OAuth并沒有對鑒權的部分做規定,也不要求在握手交互過程中包含用戶的身份信息,因此它并不適合作為單點登錄系統來使用。然而,由于OAuth的流程中隱含了鑒權的步驟,因而仍然有不少開發者將這一鑒權的步驟用作單點登錄系統,這也儼然衍生成為一種實踐模式。更有人將這個實踐進行了標準化,它就是Open ID Connect——基于OAuth的身份上下文協議,通過它即可以JWT的形式安全地在多個應用中共享用戶身份。接下來,只要讓鑒權服務器支持較長的會話時間,就可以利用OAuth為多個業務系統提供單點登錄功能了。

          我們還沒有討論OAuth對鑒權系統的影響。實際上,OAuth對鑒權系統沒有影響,在它的框架內,只是假設已經存在了一種可用于識別用戶的有效機制,而這種機制具體是怎么工作的,OAuth并不關心。因此我們既可以使用用戶名密碼(大多數開放平臺提供商都是這種方式),也可以使用掃碼登錄來識別用戶,更可以提供諸如“記住密碼”,或者雙因子驗證等其他功能。

          匯總

          上面羅列了大量術語和解釋,那么具體到一個典型的Web系統中,又應該如何對安全系統進行設計呢?綜合這些技術,從端到云,從Web門戶到內部服務,本文給出如下架構方案建議:

          推薦為整個應用的所有系統、子系統都部署全程的HTTPS,如果出于性能和成本考慮做不到,那么至少要保證在用戶或設備直接訪問的Web應用中全程使用HTTPS。

          用不同的系統分別用作身份和登錄,以及業務服務。當用戶登錄成功之后,使用OpenID Connect向業務系統頒發JWT格式的訪問令牌和身份信息。如果需要,登錄系統可以提供多種登錄方式,或者雙因子登錄等增強功能。作為安全令牌服務(STS),它還負責頒發、刷新、驗證和取消令牌的操作。在身份驗證的整個流程的每一個步驟,都使用OAuth及JWT中內置的機制來驗證數據的來源方是可信的:登錄系統要確保登錄請求來自受認可的業務應用,而業務在獲得令牌之后也需要驗證令牌的有效性。

          在Web頁面應用中,應該申請時效較短的令牌。將獲取到的令牌向客戶端頁面中以httponly的方式寫入會話Cookie,以用于后續請求的授權;在后緒請求到達時,驗證請求中所攜帶的令牌,并延長其時效。基于JWT自包含的特性,輔以完備的簽名認證,Web 應用無需額外地維護會話狀態。

          在富客戶端Web應用(單頁應用),或者移動端、客戶端應用中,可按照應用業務形態申請時效較長的令牌,或者用較短時效的令牌、配合專用的刷新令牌使用。

          在Web應用的子系統之間,調用其他子服務時,可靈活使用“應用程序身份”(如果該服務完全不直接對用戶提供調用),或者將用戶傳入的令牌直接傳遞到受調用的服務,以這種方式進行授權。各個業務系統可結合基于角色的訪問控制(RBAC)開發自有專用權限系統。

          作為工程師,我們不免會考慮,既然登錄系統的需求可能如此復雜,而大家面臨的需求在很多時候又是如此類似,那么有沒有什么現成(Out of Box)的解決方案呢?自然是有的。IdentityServer是一個完整的開發框架,提供了普通登錄到OAuth和Open ID Connect的完整實現;Open AM是一個開源的單點登錄與訪問管理軟件平臺;而Microsoft Azure AD和AWS IAM則是公有云上的身份服務。幾乎在各個層次都有現成的方案可用。使用現成的產品和服務,能夠極大地縮減開發成本,尤其為創業團隊快速構建產品和靈活變化提供更有力的保障。

          本文簡單解釋了登錄過程中所涉及的基本原理,以及現代Web應用中用于身份驗證的幾種實用技術,希望為您在開發身份驗證系統時提供幫助。現代Web應用的身份驗證需求多變,應用本身的結構也比傳統的Web應用更復雜,需要架構師在明確了登錄系統的基本原理的基礎之上,靈活利用各項技術的優勢,恰到好處地解決問題。

          什么我們把網站驗證到百度搜索資源平臺

          百度搜索資源平臺推薦站長添加主站(您網站的鏈接也許會使用www 和非 www 兩種網址,建議添加用戶能夠真實訪問到的網址),添加并驗證后,可證明您是該域名的擁有者,可以快捷批量添加子站點,查看所有子站數據,無需再一一驗證您的子站點。

          如何驗證網站

          首先如果您的網站已使用了百度統計,您可以使用統計賬號登錄平臺,或者綁定站長平臺與百度統計賬號,站長平臺支持您批量導入百度統計中的站點,您不需要再對網站進行驗證。

          百度站長平臺為未使用百度統計的站點提供三種驗證方式:文件驗證、html標簽驗證、CNAME驗證。

            1.文件驗證:您需要下載驗證文件,將文件上傳至您的服務器,放置于域名根目錄下。

            2.html標簽驗證:將html標簽添加至網站首頁html代碼的標簽與標簽之間。

            3.CNAME驗證:您需要登錄域名提供商或托管服務提供商的網站,添加新的DNS記錄。

            驗證完成后,我們將會認為您是網站的擁有者。為使您的網站一直保持驗證通過的狀態,請保留驗證的文件、html標簽或CNAME記錄,我們會去定期檢查驗證記錄


          網站驗證常見錯誤及解決辦法


          主站蜘蛛池模板: 亚洲啪啪综合AV一区| 亚洲国产精品一区二区三区在线观看| 日韩人妻一区二区三区蜜桃视频| 99久久精品午夜一区二区| 本免费AV无码专区一区| 久久久老熟女一区二区三区 | 中文字幕精品无码一区二区三区| 乱码人妻一区二区三区| 看电影来5566一区.二区| 狠狠做深爱婷婷综合一区 | 一区二区三区日本视频| 精品3d动漫视频一区在线观看| 中文字幕一区视频| 日韩免费一区二区三区在线| 国产精品香蕉一区二区三区 | 日本高清一区二区三区 | 精品国产日韩亚洲一区| 高清无码一区二区在线观看吞精 | 日本一区二区三区在线观看 | 国内精品一区二区三区最新| 久久精品一区二区三区AV| 精品无码一区二区三区亚洲桃色| 亚洲AV本道一区二区三区四区| 无码少妇一区二区| 亚洲AV无码一区东京热| 一区二区三区午夜视频| 久久er99热精品一区二区| 亚洲国产日韩在线一区| 无码中文字幕乱码一区 | 亚洲性日韩精品一区二区三区| 国产一区视频在线免费观看| 国产一区高清视频| 国产精品成人国产乱一区| 一本AV高清一区二区三区| 国产suv精品一区二区33| 亚洲狠狠狠一区二区三区| 亚洲国产AV一区二区三区四区| 国产高清在线精品一区二区| 亚洲中文字幕丝袜制服一区| 国产精品合集一区二区三区| 亚洲色偷精品一区二区三区|