開放平臺身份受權認證協議OAuth2

OAuth2簡介

OAuth2是一個受權的開放網絡標準,它可使第三方一個你喲功能程序活客戶端獲取對HTTP服務上(微信)用戶帳戶信息的有限訪問權限。經過將用戶身份驗證委派給託管用戶帳戶的服務以及受權客戶端訪問用戶帳戶進行工做java

單點登陸(SSO)

多個應用系統中,用戶只須要登陸一次就能夠訪問全部相互信任的應用系統。這個過程當中涉及到認證與受權數據庫

當用戶首次訪問系統時,因還未登陸,會被引導到認證系統中進行登陸認證;認證系統根據用戶提供的登陸信息身份驗證,若經過驗證,則返回一個認證的憑據ticket;後續用戶便可經過此ticket訪問應用系統,應用系統訪問驗證服務器對ticket行驗證,驗證其合法性。安全

  1. 訪問服務:SSO客戶端發送請求訪問應用系統提供的服務資源。
  2. 定向認證:SSO客戶端會重定向用戶請求到SSO服務器。
  3. 用戶認證:用戶身份認證。
  4. 發放票據:SSO服務器會長生一個認證的ticket
  5. 驗證票據:SSO服務器驗證ticket的合法性,驗證經過後,容許客戶端訪問服務。
  6. 傳輸用戶信息:SSO服務器驗證票據經過後,返回認證結果信息給客戶端。

單點登陸主要功能:

全部應用系統共享一個身份認證系統。統一的認證系統是SSO的前提之一。服務器

  •     認證系統的主要功能是對用戶進行登陸認證;認證成功後,認證系統應該生成統一的認證標誌(ticket),返回給用戶。
  •     認證系統要對ticket進行有效性的效驗

全部應用系統可以識別和提取ticket信息:要實現SSO的功能,應用系統要能對ticket進行識別和提取,經過與認證系統的通信,能自動判斷當前用戶是否登陸過,從而完成單點登陸的功能。微信

OAuth2.0協議

OAuth角色

資源擁有者(resource owner:能受權訪問受保護資源的一個實體,能夠是一我的,那咱們稱之爲最終用戶;如新浪微博用戶yangty;網絡

資源服務器(resource server:存儲受保護資源,客戶端經過access token請求資源,資源服務器響應受保護資源給客戶端;存儲着用戶yangty的微博等信息。ide

受權服務器(authorization server:成功驗證資源擁有者並獲取受權以後,受權服務器頒發受權令牌(Access Token)給客戶端。網站

客戶端(client:如新浪微博客戶端weibo等第三方應用,也能夠是它本身的官方應用;其自己不存儲資源,而是資源擁有者受權經過後,使用它的受權(受權令牌)訪問受保護資源,而後客戶端把相應的數據展現出來/提交到服務器。「客戶端」術語不表明任何特定實現(如應用運行在一臺服務器、桌面、手機或其餘設備)ui

一、客戶端從資源擁有者那請求受權。受權請求能夠直接發給資源擁有者,或間接的經過受權服務器這種中介,後者更可取。url

二、客戶端收到一個受權許可,表明資源服務器提供的受權。

三、客戶端使用它本身的私有證書及受權許可到受權服務器驗證。

四、若是驗證成功,則下發一個訪問令牌。

五、客戶端使用訪問令牌向資源服務器請求受保護資源。

六、資源服務器會驗證訪問令牌的有效性,若是成功則下發受保護資源。

服務器端

通常把受權服務器和資源服務器整合在一塊兒實現,引入authzserver(受權服務器依賴)和resourceserver(資源服務器依賴)

數據庫創建用戶表存儲着認證/資源服務器的用戶信息,即資源擁有者,如用戶名/密碼;客戶端表存儲客戶端的的客戶端id及客戶端安全key,在進行受權時使用

受權控制器實現流程

//構建OAuth 受權請求
      OAuthAuthzRequest oauthRequest = new OAuthAuthzRequest(request);

      //檢查傳入的客戶端id是否正確
      if (!oAuthService.checkClientId(oauthRequest.getClientId())) {
        OAuthResponse response = OAuthASResponse
             .errorResponse(HttpServletResponse.SC_BAD_REQUEST)
             .setError(OAuthError.TokenResponse.INVALID_CLIENT)
             .setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)
             .buildJSONMessage();
        return new ResponseEntity(
           response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
      }

      Subject subject = SecurityUtils.getSubject();
      //若是用戶沒有登陸,跳轉到登錄頁面
      if(!subject.isAuthenticated()) {
        //登陸失敗時跳轉到登錄頁面
        if(!login(subject, request)) {
          model.addAttribute("client",    
              clientService.findByClientId(oauthRequest.getClientId()));
          return "oauth2login";
        }
      }

      String username = (String)subject.getPrincipal();
      //生成受權碼
      String authorizationCode = null;
      //responseType目前僅支持CODE,另外還有TOKEN
      String responseType = oauthRequest.getParam(OAuth.OAUTH_RESPONSE_TYPE);
      if (responseType.equals(ResponseType.CODE.toString())) {
        OAuthIssuerImpl oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());
        authorizationCode = oauthIssuerImpl.authorizationCode();
        oAuthService.addAuthCode(authorizationCode, username);
      }
      //進行OAuth響應構建
      OAuthASResponse.OAuthAuthorizationResponseBuilder builder =
        OAuthASResponse.authorizationResponse(request, 
                                           HttpServletResponse.SC_FOUND);
      //設置受權碼
      builder.setCode(authorizationCode);
      //獲得到客戶端重定向地址
      String redirectURI = oauthRequest.getParam(OAuth.OAUTH_REDIRECT_URI);

      //構建響應
      final OAuthResponse response = builder.location(redirectURI).buildQueryMessage();
      //根據OAuthResponse返回ResponseEntity響應
      HttpHeaders headers = new HttpHeaders();
      headers.setLocation(new URI(response.getLocationUri()));
      return new ResponseEntity(headers, HttpStatus.valueOf(response.getResponseStatus()));
    } catch (OAuthProblemException e) {
      //出錯處理
      String redirectUri = e.getRedirectUri();
      if (OAuthUtils.isEmpty(redirectUri)) {
        //告訴客戶端沒有傳入redirectUri直接報錯
        return new ResponseEntity(
          "OAuth callback url needs to be provided by client!!!", HttpStatus.NOT_FOUND);
      }
      //返回錯誤消息(如?error=)
      final OAuthResponse response =
              OAuthASResponse.errorResponse(HttpServletResponse.SC_FOUND)
                      .error(e).location(redirectUri).buildQueryMessage();
      HttpHeaders headers = new HttpHeaders();
      headers.setLocation(new URI(response.getLocationUri()));
      return new ResponseEntity(headers, HttpStatus.valueOf(response.getResponseStatus()));
    }
  }

一、經過受權訪問路徑http://localhost:8080/oauth-server/authorize

?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee&response_type=code&redirect_uri=http://localhost:9080/oauth-client/oauth2-login訪問受權頁面;

二、該控制器首先檢查clientId是否正確;若是錯誤將返回相應的錯誤信息;

三、而後判斷用戶是否登陸了,若是沒有登陸首先到登陸頁面登陸;

四、登陸成功後生成相應的auth code即受權碼,而後重定向到客戶端地址,

http://localhost:9080/oauth-client/oauth2-login?code=52b1832f5dff68122f4f00ae995da0ed;

在重定向到的地址中會帶上code參數(受權碼),接着客戶端能夠根據受權碼去換取access token

訪問令牌控制器實現流程

try {
      //構建OAuth請求
      OAuthTokenRequest oauthRequest = new OAuthTokenRequest(request);

      //檢查提交的客戶端id是否正確
      if (!oAuthService.checkClientId(oauthRequest.getClientId())) {
        OAuthResponse response = OAuthASResponse
                .errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                .setError(OAuthError.TokenResponse.INVALID_CLIENT)
                .setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)
                .buildJSONMessage();
       return new ResponseEntity(
         response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
      }

      // 檢查客戶端安全KEY是否正確
      if (!oAuthService.checkClientSecret(oauthRequest.getClientSecret())) {
        OAuthResponse response = OAuthASResponse
              .errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
              .setError(OAuthError.TokenResponse.UNAUTHORIZED_CLIENT)
              .setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)
              .buildJSONMessage();
      return new ResponseEntity(
          response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
      }
  
      String authCode = oauthRequest.getParam(OAuth.OAUTH_CODE);
      // 檢查驗證類型,此處只檢查AUTHORIZATION_CODE類型,其餘的還有PASSWORD或REFRESH_TOKEN
      if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(
         GrantType.AUTHORIZATION_CODE.toString())) {
         if (!oAuthService.checkAuthCode(authCode)) {
            OAuthResponse response = OAuthASResponse
                .errorResponse(HttpServletResponse.SC_BAD_REQUEST)
                .setError(OAuthError.TokenResponse.INVALID_GRANT)
                .setErrorDescription("錯誤的受權碼")
              .buildJSONMessage();
           return new ResponseEntity(
             response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
         }
      }

      //生成Access Token
      OAuthIssuer oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());
      final String accessToken = oauthIssuerImpl.accessToken();
      oAuthService.addAccessToken(accessToken,
          oAuthService.getUsernameByAuthCode(authCode));

      //生成OAuth響應
      OAuthResponse response = OAuthASResponse
              .tokenResponse(HttpServletResponse.SC_OK)
              .setAccessToken(accessToken)
              .setExpiresIn(String.valueOf(oAuthService.getExpireIn()))
              .buildJSONMessage();

      //根據OAuthResponse生成ResponseEntity
      return new ResponseEntity(
          response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
 } catch (OAuthProblemException e) {
      //構建錯誤響應
      OAuthResponse res = OAuthASResponse
              .errorResponse(HttpServletResponse.SC_BAD_REQUEST).error(e)
              .buildJSONMessage();
     return new ResponseEntity(res.getBody(), HttpStatus.valueOf(res.getResponseStatus()));
   }
 }

一、首先經過如http://localhost:8080/oauth-server/accessToken,POST提交以下數據:

client_id= c1ebe466-1cdc-4bd3-ab69-77c3561b9dee& client_secret= d8346ea2-6017-43ed-ad68-19c0f971738b&grant_type=authorization_code&code=828beda907066d058584f37bcfd597b6&redirect_uri=http://localhost:9080/oauth-client/oauth2-login訪問;

二、該控制器會驗證client_id、client_secret、auth code的正確性,若是錯誤會返回相應的錯誤;

三、若是驗證經過會生成並返回相應的訪問令牌access token。

資源控制器實現流程

try {

      //構建OAuth資源請求
      OAuthAccessResourceRequest oauthRequest = 
            new OAuthAccessResourceRequest(request, ParameterStyle.QUERY);

      //獲取Access Token
      String accessToken = oauthRequest.getAccessToken();

      //驗證Access Token
      if (!oAuthService.checkAccessToken(accessToken)) {

       
         // 若是不存在/過時了,返回未驗證錯誤,需從新驗證
         OAuthResponse oauthResponse = OAuthRSResponse
              .errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
              .setRealm(Constants.RESOURCE_SERVER_NAME)
              .setError(OAuthError.ResourceResponse.INVALID_TOKEN)
              .buildHeaderMessage();

         HttpHeaders headers = new HttpHeaders();
         headers.add(OAuth.HeaderType.WWW_AUTHENTICATE, 
           oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
         return new ResponseEntity(headers, HttpStatus.UNAUTHORIZED);
      }

      //返回用戶名
      String username = oAuthService.getUsernameByAccessToken(accessToken);
      return new ResponseEntity(username, HttpStatus.OK);
    } catch (OAuthProblemException e) {
      //檢查是否設置了錯誤碼
      String errorCode = e.getError();
      if (OAuthUtils.isEmpty(errorCode)) {
        OAuthResponse oauthResponse = OAuthRSResponse
               .errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
               .setRealm(Constants.RESOURCE_SERVER_NAME)
               .buildHeaderMessage();

        HttpHeaders headers = new HttpHeaders();
        headers.add(OAuth.HeaderType.WWW_AUTHENTICATE, 
          oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
        return new ResponseEntity(headers, HttpStatus.UNAUTHORIZED);
      }

      OAuthResponse oauthResponse = OAuthRSResponse
               .errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
               .setRealm(Constants.RESOURCE_SERVER_NAME)
               .setError(e.getError())
               .setErrorDescription(e.getDescription())
               .setErrorUri(e.getUri())
               .buildHeaderMessage();

      HttpHeaders headers = new HttpHeaders();
      headers.add(OAuth.HeaderType.WWW_AUTHENTICATE, 、
        oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
      return new ResponseEntity(HttpStatus.BAD_REQUEST);
    }

一、經過

http://localhost:8080/oauth-server/userInfo? access_token=828beda907066d058584f37bcfd597b6進行訪問;

二、該控制器會驗證access token的有效性;若是無效了將返回相應的錯誤,客戶端再從新進行受權;

三、若是有效,則返回當前登陸用戶的用戶名。

服務器維護

訪問localhost:8080/oauth-server/,登陸後進行客戶端管理和用戶管理。

客戶端管理就是進行客戶端的註冊,如新浪微博的第三方應用就須要到新浪微博開發平臺進行註冊;用戶管理就是進行如新浪微博用戶的管理。

客戶端

客戶端流程:若是須要登陸首先跳到oauth2服務端進行登陸受權,成功後服務端返回auth code,而後客戶端使用auth code去服務器端換取access token,最後根據access token獲取用戶信息進行客戶端的登陸綁定。這個能夠參照如不少網站的新浪微博登陸功能,或其餘的第三方賬號登陸功能。

客戶端攔截器實現

一、首先判斷有沒有服務端返回的error參數,若是有則直接重定向到失敗頁面;

二、接着若是用戶尚未身份驗證,判斷是否有auth code參數(便是不是服務端受權以後返回的),若是沒有則重定向到服務端進行受權;

三、不然調用executeLogin進行登陸,經過auth code建立OAuth2Token提交給Subject進行登陸;

四、登陸成功將回調onLoginSuccess方法重定向到成功頁面;

五、登陸失敗則回調onLoginFailure重定向到失敗頁面。

     該filter的做用相似於FormAuthenticationFilter用於oauth2客戶端的身份驗證控制;若是當前用戶尚未身份驗證,首先會判斷url中是否有code(服務端返回的auth code),若是沒有則重定向到服務端進行登陸並受權,而後返回auth code;接着OAuth2AuthenticationFilter會用auth code建立OAuth2Token,而後提交給Subject.login進行登陸;接着OAuth2Realm會根據OAuth2Token進行相應的登陸邏輯。

oauth集成身份受權認證流程

一、首先訪問http://localhost:9080/oauth-client/,而後點擊登陸按鈕進行登陸,會跳到受權驗證頁面: 

二、輸入用戶名進行登陸並受權;

三、若是登陸成功,服務端會重定向到客戶端,即以前客戶端提供的地址http://localhost:9080/oauth-client/oauth2-login?code=473d56015bcf576f2ca03eac1a5bcc11,並帶着auth code過去;

四、客戶端的OAuth2AuthenticationFilter會收集此auth code,並建立OAuth2Token提交給Subject進行客戶端登陸;

五、客戶端的Subject會委託給OAuth2Realm進行身份驗證;此時OAuth2Realm會根據auth code換取access token,再根據access token獲取受保護的用戶信息;而後進行客戶端登陸。

相關文章
相關標籤/搜索