开放平台身份授权认证协议OAuth2

a 夏天 提交于 2019-12-04 16:28:23

OAuth2简介

OAuth2是一个授权的开放网络标准,它可以使第三方一个你哟功能程序活客户端获取对HTTP服务上(微信)用户账户信息的有限访问权限。通过将用户身份验证委派给托管用户账户的服务以及授权客户端访问用户账户进行工作

单点登录(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的微博等信息。

授权服务器(authorization server:成功验证资源拥有者并获取授权之后,授权服务器颁发授权令牌(Access Token)给客户端。

客户端(client:如新浪微博客户端weibo等第三方应用,也可以是它自己的官方应用;其本身不存储资源,而是资源拥有者授权通过后,使用它的授权(授权令牌)访问受保护资源,然后客户端把相应的数据展示出来/提交到服务器。“客户端”术语不代表任何特定实现(如应用运行在一台服务器、桌面、手机或其他设备)

1、客户端从资源拥有者那请求授权。授权请求可以直接发给资源拥有者,或间接的通过授权服务器这种中介,后者更可取。

2、客户端收到一个授权许可,代表资源服务器提供的授权。

3、客户端使用它自己的私有证书及授权许可到授权服务器验证。

4、如果验证成功,则下发一个访问令牌。

5、客户端使用访问令牌向资源服务器请求受保护资源。

6、资源服务器会验证访问令牌的有效性,如果成功则下发受保护资源。

服务器端

一般把授权服务器和资源服务器整合在一起实现,引入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()));
    }
  }

1、通过授权访问路径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访问授权页面;

2、该控制器首先检查clientId是否正确;如果错误将返回相应的错误信息;

3、然后判断用户是否登录了,如果没有登录首先到登录页面登录;

4、登录成功后生成相应的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()));
   }
 }

1、首先通过如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访问;

2、该控制器会验证client_id、client_secret、auth code的正确性,如果错误会返回相应的错误;

3、如果验证通过会生成并返回相应的访问令牌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);
    }

1、通过

http://localhost:8080/oauth-server/userInfo? access_token=828beda907066d058584f37bcfd597b6进行访问;

2、该控制器会验证access token的有效性;如果无效了将返回相应的错误,客户端再重新进行授权;

3、如果有效,则返回当前登录用户的用户名。

服务器维护

访问localhost:8080/oauth-server/,登录后进行客户端管理和用户管理。

客户端管理就是进行客户端的注册,如新浪微博的第三方应用就需要到新浪微博开发平台进行注册;用户管理就是进行如新浪微博用户的管理。

客户端

客户端流程:如果需要登录首先跳到oauth2服务端进行登录授权,成功后服务端返回auth code,然后客户端使用auth code去服务器端换取access token,最后根据access token获取用户信息进行客户端的登录绑定。这个可以参照如很多网站的新浪微博登录功能,或其他的第三方帐号登录功能。

客户端拦截器实现

1、首先判断有没有服务端返回的error参数,如果有则直接重定向到失败页面;

2、接着如果用户还没有身份验证,判断是否有auth code参数(即是不是服务端授权之后返回的),如果没有则重定向到服务端进行授权;

3、否则调用executeLogin进行登录,通过auth code创建OAuth2Token提交给Subject进行登录;

4、登录成功将回调onLoginSuccess方法重定向到成功页面;

5、登录失败则回调onLoginFailure重定向到失败页面。

     该filter的作用类似于FormAuthenticationFilter用于oauth2客户端的身份验证控制;如果当前用户还没有身份验证,首先会判断url中是否有code(服务端返回的auth code),如果没有则重定向到服务端进行登录并授权,然后返回auth code;接着OAuth2AuthenticationFilter会用auth code创建OAuth2Token,然后提交给Subject.login进行登录;接着OAuth2Realm会根据OAuth2Token进行相应的登录逻辑。

oauth集成身份授权认证流程

1、首先访问http://localhost:9080/oauth-client/,然后点击登录按钮进行登录,会跳到授权验证页面: 

2、输入用户名进行登录并授权;

3、如果登录成功,服务端会重定向到客户端,即之前客户端提供的地址http://localhost:9080/oauth-client/oauth2-login?code=473d56015bcf576f2ca03eac1a5bcc11,并带着auth code过去;

4、客户端的OAuth2AuthenticationFilter会收集此auth code,并创建OAuth2Token提交给Subject进行客户端登录;

5、客户端的Subject会委托给OAuth2Realm进行身份验证;此时OAuth2Realm会根据auth code换取access token,再根据access token获取受保护的用户信息;然后进行客户端登录。

 

 

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!