SSO第三方应用SP通过OAUTH2协议请求IDP获取用户信息的授权过程

前言:

 

整个完整的OAUTH2.0授权过程会经过IDP几类接口的交互,认证接口、获取AccessToken接口、用户信息接口,下面把相关接口按请求时序串联来讲解。

目录

1.认证入口接口

2.授权接口,为了获取code,当前未授权

3.授权确认接口

4.前端工程请求后台授权确认接口,POST请求

5.再次来到授权接口,为了获取code,当前已授权

6.用code换取token接口

7.用token请求用户信息接口


OAuth 2.0 认证接口

1.认证入口接口

http://192.168.129.23:9527/sign/authz/oauth/v20/b32834accb544ea7a9a09dcae4a36403

请求参数有

id,也可以说是client_id,也就是IDP(identity provider)分配给SP(service provider)的唯一id,以RESTFUL形式传递参数

  @Operation(summary = "OAuth 2.0 认证接口", description = "传递参数应用ID,自动完成跳转认证拼接", method = "GET")
  @RequestMapping(value = {OAuth2Constants.ENDPOINT.ENDPOINT_BASE + "/{id}"}, method = RequestMethod.GET)
  public ModelAndView authorize(HttpServletRequest request, HttpServletResponse response, @PathVariable("id") String id) {

    ClientDetails clientDetails = getClientDetailsService().loadClientByClientId(id, true);
    _logger.debug("" + clientDetails);
    String authorizationUrl = "";

    try {
      authorizationUrl = String.format(OAUTH_V20_AUTHORIZATION_URL, clientDetails.getClientId(), HttpEncoder.encode(clientDetails.getRegisteredRedirectUri().toArray()[0].toString()));
    } catch (Exception e) {
      e.printStackTrace();
    }

    _logger.debug("authorizationUrl {}", authorizationUrl);
    return WebContext.redirect(authorizationUrl);
  }

这个接口不需要做拦截,只是简单组装一下url再做跳转,感觉不要这个跳转也行。

利用client_id查询数据库获取了ClientDetails,使用模板字符串拼装重定向授权URL

http://192.168.129.23:9527/sign/authz/oauth/v20/authorize?client_id=%s&response_type=code&redirect_uri=%s&approval_prompt=auto

上面url参数中的redirect_uri来源于ClientDetails对象的registeredRedirectUri属性,也就是来自数据库if_apps_oauth_client_details表的WEB_SERVER_REDIRECT_URI字段,这是SSO管理后台WEB界面预先对每个接入SSO的应用做好的配置。

重定向跳转(redirect)到authorizationUrl。

2.授权接口,为了获取code,当前未授权

http://192.168.129.23:9527/sign/authz/oauth/v20/authorize?client_id=%s&response_type=code&redirect_uri=%s&approval_prompt=auto

  @Operation(summary = "OAuth 2.0 认证接口", description = "传递参数client_id,response_type,redirect_uri等", method = "GET")
  @RequestMapping(value = {OAuth2Constants.ENDPOINT.ENDPOINT_AUTHORIZE, OAuth2Constants.ENDPOINT.ENDPOINT_TENCENT_IOA_AUTHORIZE}, method = RequestMethod.GET)
  public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters, @CurrentUser UserInfo currentUser, SessionStatus sessionStatus) {

    Principal principal = (Principal) AuthorizationUtils.getAuthentication();
    // Pull out the authorization request first, using the OAuth2RequestFactory. All further logic should
    // query off of the authorization request instead of referring back to the parameters map. The contents of the
    // parameters map will be stored without change in the AuthorizationRequest object once it is created.
    AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);

    Set<String> responseTypes = authorizationRequest.getResponseTypes();
    if (!responseTypes.contains(OAuth2Constants.PARAMETER.TOKEN) && !responseTypes.contains(OAuth2Constants.PARAMETER.CODE)) {
      throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
    }

    if (authorizationRequest.getClientId() == null) {
      throw new InvalidClientException("A client id must be provided");
    }

    try {
      if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
        throw new InsufficientAuthenticationException("User must be authenticated with Spring Security before authorization can be completed.");
      }

      ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId(), true);

      // The resolved redirect URI is either the redirect_uri from the parameters or the one from
      // clientDetails. Either way we need to store it on the AuthorizationRequest.
      String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Constants.PARAMETER.REDIRECT_URI);
      String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
      if (!StringUtils.hasText(resolvedRedirect)) {
        logger.info("Client redirectUri " + resolvedRedirect);
        logger.info("Parameter redirectUri " + redirectUriParameter);
        throw new RedirectMismatchException("A redirectUri must be either supplied or preconfigured in the ClientDetails");
      }
      authorizationRequest.setRedirectUri(resolvedRedirect);

      // We intentionally only validate the parameters requested by the client (ignoring any data that may have
      // been added to the request by the manager).
      oauth2RequestValidator.validateScope(authorizationRequest, client);

      // Some systems may allow for approval decisions to be remembered or approved by default. Check for
      // such logic here, and set the approved flag on the authorization request accordingly.
      authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest, (Authentication) principal);
      // is this call necessary?
      boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
      authorizationRequest.setApproved(approved);

      // Validation is all done, so we can check for auto approval...
      if (authorizationRequest.isApproved()) {
        if (responseTypes.contains(OAuth2Constants.PARAMETER.TOKEN)) {
          return new ModelAndView(getImplicitGrantResponse(authorizationRequest));
        }
        if (responseTypes.contains(OAuth2Constants.PARAMETER.CODE)) {
          return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
              (Authentication) principal));
        }
      }
      Apps app = (Apps) WebContext.getAttribute(WebConstants.AUTHORIZE_SIGN_ON_APP);
      //session中为空或者id不一致重新加载
      if (app == null || !app.getId().equalsIgnoreCase(authorizationRequest.getClientId())) {
        app = appsService.get(authorizationRequest.getClientId());
        WebContext.setAttribute(WebConstants.AUTHORIZE_SIGN_ON_APP, app);
      }

      // Place auth request into the model so that it is stored in the session
      // for approveOrDeny to use. That way we make sure that auth request comes from the session,
      // so any auth request parameters passed to approveOrDeny will be ignored and retrieved from the session.
      momentaryService.put(currentUser.getSessionId(), "authorizationRequest", authorizationRequest);
      return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);

    } catch (RuntimeException e) {
      sessionStatus.setComplete();
      throw e;
    }
  }

SPRING MVC对这个接口做了拦截,会先进入SingleSignOnInterceptor拦截器类处理,检测cookie键值对中名为congress的值

/**
 * 单点登录拦截器器
 */
@Component
public class SingleSignOnInterceptor implements AsyncHandlerInterceptor {

  private static final Logger logger = LoggerFactory.getLogger(SingleSignOnInterceptor.class);
  @Autowired
  ApplicationConfig applicationConfig;
  @Autowired
  SessionManager sessionManager;
  @Autowired
  AuthTokenService authTokenService;
  @Autowired
  AppsService appsService;
  @Autowired
  AppsCasDetailsService casDetailsService;

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    logger.trace("Single Sign On Interceptor");

    AuthorizationUtils.authenticateWithCookie(request, authTokenService, sessionManager);

    if (AuthorizationUtils.isNotAuthenticated()) {
      String loginUrl = applicationConfig.getFrontendUri() + "/#/passport/login?redirect_uri=%s";
      String redirect_uri = UrlUtils.buildFullRequestUrl(request);
      String base64RequestUrl = Base64Utils.base64UrlEncode(redirect_uri.getBytes());

      logger.debug("No Authentication ... Redirect to /passport/login , redirect_uri {} , base64 {}", redirect_uri, base64RequestUrl);
      response.sendRedirect(String.format(loginUrl, base64RequestUrl));
      return false;
    }

    //判断应用访问权限
    if (AuthorizationUtils.isAuthenticated()) {

      logger.debug("preHandle {}", request.getRequestURI());
      Apps app = (Apps) WebContext.getAttribute(WebConstants.AUTHORIZE_SIGN_ON_APP);
      if (app == null) {

        String requestURI = request.getRequestURI();
        if (requestURI.contains("/authz/cas/login")) {//for CAS service

          app = casDetailsService.getAppDetails(request.getParameter(CasConstants.PARAMETER.SERVICE), true);
        } else if (requestURI.contains("/authz/jwt/")
            || requestURI.contains("/authz/api/")
            || requestURI.contains("/authz/formbased/")
            || requestURI.contains("/authz/tokenbased/")
            || requestURI.contains("/authz/api/")
            || requestURI.contains("/authz/saml20/consumer/")
            || requestURI.contains("/authz/saml20/idpinit/")
            || requestURI.contains("/authz/cas/")) { //for id end of URL

          String[] requestURIs = requestURI.split("/");
          String appId = requestURIs[requestURIs.length - 1];

          logger.debug("appId {}", appId);
          app = appsService.get(appId, true);

        } else if (requestURI.contains("/authz/oauth/v20/authorize")) {//oauth
          app = appsService.get(request.getParameter(OAuth2Constants.PARAMETER.CLIENT_ID), true);
        }
      }

      SignPrincipal principal = AuthorizationUtils.getPrincipal();
      if (principal != null && app != null) {
        if (principal.getGrantedAuthorityApps().contains(new SimpleGrantedAuthority(app.getId()))) {
          logger.trace("preHandle have authority access {}", app);
          return true;
        }
      }

      logger.debug("preHandle not have authority access " + app);
      response.sendRedirect(request.getContextPath() + "/authz/refused");
    }
    return true;
  }
}

如果没有登录,则跳转到登录页面完成登录才能继续跳转回这个接口,登录URL

http://192.168.129.23:8522/intellifsheild/#/passport/login?redirect_uri=%s

登录是一个很重要的用户跟IDP交互环节,在后面会详细介绍到,这里为了讲述逻辑利索,假设用户已经输入账号密码顺利完成了登录,先把Oauth2授权的粗线条逻辑说完整。

接口构建了一个AuthorizationRequest对象,以sessionId为key,存放到momentaryService存储类中(后面接口会从中取出使用),一个Caffeine结构的内存缓存,过期时间为5分钟。

AuthorizationRequest对象的关键属性有

clientId:客户端ID,代表请求来自的应用

state:客户端传过来的token,必须无修改传回去

scope:授权范围,枚举值有read,write,trust,openid,profile,email,phone,address,all

responseType:到底是code还是token,如果认证成功根据这个类型参数返回不同的视图

approved:是否用户已经授权,如果已授权,根据上面类型参数跳转到不同视图,否则跳转到授权页面

redirectUri:重定向到应用URL,就是上面步骤1中的redirect_uri

这时approved=false,还没经过用户授权,服务器端跳转(forward)到授权确认接口

3.授权确认接口

http://192.168.129.23:9527/sign/authz/oauth/v20/approval_confirm

  @RequestMapping(OAuth2Constants.ENDPOINT.ENDPOINT_APPROVAL_CONFIRM)
  public ModelAndView getAccessConfirmation(@RequestParam Map<String, Object> model, @CurrentUser UserInfo currentUser) {
    try {
      // Map<String, Object> model
      AuthorizationRequest clientAuth = (AuthorizationRequest) momentaryService.get(currentUser.getSessionId(), "authorizationRequest");
      ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId(), true);
      model.put("oauth_approval", authTokenService.genRandomJwt());
      model.put("auth_request", clientAuth);
      model.put("client", client);
      model.put("oauth_version", "oauth 2.0");
      Map<String, String> scopes = new LinkedHashMap<String, String>();
      for (String scope : clientAuth.getScope()) {
        scopes.put(OAuth2Constants.PARAMETER.SCOPE_PREFIX + scope, "false");
      }
      String principal = AuthorizationUtils.getPrincipal().getUsername();
      for (Approval approval : approvalStore.getApprovals(principal, client.getClientId())) {
        if (clientAuth.getScope().contains(approval.getScope())) {
          scopes.put(OAuth2Constants.PARAMETER.SCOPE_PREFIX + approval.getScope(),
              approval.getStatus() == ApprovalStatus.APPROVED ? "true" : "false");
        }
      }

      model.put("scopes", scopes);
      if (!model.containsKey(OAuth2Constants.PARAMETER.APPROVAL_PROMPT)) {
        model.put(OAuth2Constants.PARAMETER.APPROVAL_PROMPT, client.getApprovalPrompt());
      }
    } catch (Exception e) {
      _logger.debug("OAuth Access Confirmation process error.", e);
    }

    ModelAndView modelAndView = new ModelAndView("authorize/oauth_access_confirmation");
    _logger.trace("Confirmation details ");
    for (Object key : model.keySet()) {
      _logger.trace("key " + key + "=" + model.get(key));
    }

    model.put("authorizeApproveUri", applicationConfig.getFrontendUri() + "/#/authz/oauth2approve");

    modelAndView.addObject("model", model);
    return modelAndView;
  }

这个接口主要为后面页面渲染构建了一个map容器,用来放下面几个数据:

oauth_approval:利用配置文件的过期时间参数900秒,生成一个token字符串

auth_request:: 从momentaryService取出的AuthorizationRequest对象,刚才在步骤2授权接口存放进去的

client:应用对应的ClientDetails对象,对应if_apps_oauth_client_details表的数据

oauth_version: 代码里写死为oauth 2.0

scopes:略

authorizeApproveUri:

前端工程的授权页面http://192.168.129.23:8522/intellifsheild/#/authz/oauth2approve

携带上面map数据,匹配逻辑视图authorize/oauth_access_confirmation,这是一个freemark模板内嵌页面,给用户确定授权的页面,使用上面组装的map数据来渲染

<!DOCTYPE html>
<html >
<head>
	<#include  "authorize_common.ftl">
	<script type="text/javascript">
	   window.top.location.href = "${model.authorizeApproveUri}?oauth_approval=${model.oauth_approval}&clientId=${model.client.clientId!}";
	</script>
</head>

<body  style="display:none;">
	<div id="top">

	</div>
	<div class="container">	
		<#if 'oauth 2.0'==model.oauth_version>
			 <!-- oauth 2.0 -->
			 <table  class="table table-bordered">
                        <tr>
                            <td colspan="2">
                                <div style="text-align: center;">                            
                                    <!--<p>You hereby authorize "${model.client.clientId!}" to access your protected resources.</p>-->
                                    <form id="confirmationForm" name="confirmationForm" action="<@base/>/authz/oauth/v20/authorize" method="post">
                                        <input id="user_oauth_approval" name="user_oauth_approval" value="true" type="hidden"/>
                                        <input class="button btn btn-primary mr-3" name="authorize"
                                            value='' type="submit"/>
                                    </form>
                                </div>
                            </td>
                        </tr>
              </table>   
	    </#if>
    </div>
</body>
</html>

这页面是一个form表单,action="<@base/>/authz/oauth/v20/authorize"

表单隐藏了一个字段user_oauth_approval=true,当用户点击同意授权后,会跳转到上面第2步的URL:

http://192.168.129.23:9527/sign/authz/oauth/v20/authorize

4.前端工程请求后台授权确认接口,POST请求

http://192.168.129.23:9527/sign/authz/oauth/v20/authorize/approval

请求参数有

user_oauth_approval=true或者false

  @RequestMapping(value = {OAuth2Constants.ENDPOINT.ENDPOINT_AUTHORIZE + "/approval"}, params = OAuth2Constants.PARAMETER.USER_OAUTH_APPROVAL, method = RequestMethod.POST)
  public ResponseEntity<?> authorizeApproveOrDeny(@RequestParam Map<String, String> approvalParameters, @CurrentUser UserInfo currentUser, SessionStatus sessionStatus) {

    Principal principal = (Principal) AuthorizationUtils.getAuthentication();
    if (!(principal instanceof Authentication)) {
      sessionStatus.setComplete();
      throw new InsufficientAuthenticationException("User must be authenticated with Spring Security before authorizing an access token.");
    }

    AuthorizationRequest authorizationRequest = (AuthorizationRequest) momentaryService.get(currentUser.getSessionId(), "authorizationRequest");
    if (authorizationRequest == null) {
      sessionStatus.setComplete();
      throw new InvalidRequestException("Cannot approve uninitialized authorization request.");
    }

    try {
      Set<String> responseTypes = authorizationRequest.getResponseTypes();
      authorizationRequest.setApprovalParameters(approvalParameters);
      authorizationRequest = userApprovalHandler.updateAfterApproval(authorizationRequest, (Authentication) principal);
      boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
      authorizationRequest.setApproved(approved);

      if (authorizationRequest.getRedirectUri() == null) {
        sessionStatus.setComplete();
        throw new InvalidRequestException("Cannot approve request when no redirect URI is provided.");
      }

      if (!authorizationRequest.isApproved()) {
        return new Message<Object>(Message.FAIL, (Object) getUnsuccessfulRedirect(authorizationRequest, new UserDeniedAuthorizationException("User denied access"), responseTypes.contains(OAuth2Constants.PARAMETER.TOKEN))).buildResponse();
      }

      if (responseTypes.contains(OAuth2Constants.PARAMETER.TOKEN)) {
        return new Message<Object>((Object) getImplicitGrantResponse(authorizationRequest)).buildResponse();
      }

      return new Message<Object>((Object) getAuthorizationCodeResponse(authorizationRequest, (Authentication) principal)).buildResponse();
    } finally {
      sessionStatus.setComplete();
    }
  }

momentaryService缓存类中取出对应用户sessionId的AuthorizationRequest对象,

把前端传来参数user_oauth_approval的值true或者false设置给AuthorizationRequest对象的approved属性

5.再次来到授权接口,为了获取code,当前已授权

http://192.168.129.23:9527/sign/authz/oauth/v20/authorize

经过上面用户同意授权后,这时approved=true,跳转到AuthorizationRequest对象的redirectUri地址

这里假设授权类型是code,先忽略token类型

利用OAuth2Authentication对象生成一个authorizationCode ,这是给客户端放发的code,保存到authorizationCodeStore中,这是一个Caffeine结构的内存缓存,过期时间为3分钟。

>>>授权成功会带上几个参数

code:上面生成的authorizationCode字符串

state:客户端传过来的,要无修改返回给客户端

service:

>>>授权失败会带上几个参数

error

error_description

state

OAuth 2.0 获取AccessToken接口

6.用code换取token接口

http://192.168.129.23:9527/sign/authz/oauth/v20/token

请求参数有

grant_type=authorization_code,另外还有一种是刷新token使用的类型:refresh_token

code=上面步骤4中生成的authorizationCode

springboot配置类Oauth20AutoConfiguration对这个接口配置了filter,request会先来到TokenEndpointAuthenticationFilter处理

  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
      ServletException {

    _logger.debug("Authentication TokenEndpoint ");
    if (authenticationManager == null) {
      authenticationManager = WebContext.getBean("oauth20UserAuthenticationManager",
          AuthenticationManager.class);
    }
    if (oAuth2RequestFactory == null) {
      oAuth2RequestFactory = WebContext.getBean("oAuth2RequestFactory", OAuth2RequestFactory.class);
    }
    if (oauth20ClientAuthenticationManager == null) {
      oauth20ClientAuthenticationManager = WebContext.getBean("oauth20ClientAuthenticationManager",
          AuthenticationManager.class);
    }

    final boolean debug = _logger.isDebugEnabled();
    final HttpServletRequest request = (HttpServletRequest) req;
    final HttpServletResponse response = (HttpServletResponse) res;

    try {
      String grantType = request.getParameter(OAuth2Constants.PARAMETER.GRANT_TYPE);
      if (grantType != null && grantType.equals(OAuth2Constants.PARAMETER.GRANT_TYPE_PASSWORD)) {
        //password
        usernamepassword(request, response);
      } else {
        Authentication authentication = ClientCredentials(request, response);
        _logger.trace("getPrincipal " + authentication.getPrincipal().getClass());
        SignPrincipal auth = null;
        if (authentication.getPrincipal() instanceof SignPrincipal) {
          //authorization_code
          auth = (SignPrincipal) authentication.getPrincipal();
        } else {
          //client_credentials
          auth = new SignPrincipal((User) authentication.getPrincipal());
        }
        auth.setAuthenticated(true);
        UsernamePasswordAuthenticationToken simpleUserAuthentication = new UsernamePasswordAuthenticationToken(
            auth, authentication.getCredentials(), authentication.getAuthorities());
        AuthorizationUtils.setAuthentication(simpleUserAuthentication);
      }

    } catch (AuthenticationException failed) {
      SecurityContextHolder.clearContext();
      if (debug) {
        _logger.debug("Authentication request for failed: " + failed);
      }
      onUnsuccessfulAuthentication(request, response, failed);
      return;
    }

    chain.doFilter(request, response);
  }

filter里主要是看看有没有经过认证,取出Authentication对象,如果没有,则使用请求参数client_id和client_secret来构建认证请求UsernamePasswordAuthenticationToken对象,来完成认证。

需要保证Authentication对象存在,且属性authenticated=true才能在接口主体代码顺利执行。

经过filter来到接口主体代码

  @Operation(summary = "OAuth 2.0 获取AccessToken接口", description = "传递参数token等", method = "POST")
  @RequestMapping(value = {OAuth2Constants.ENDPOINT.ENDPOINT_TOKEN, OAuth2Constants.ENDPOINT.ENDPOINT_TENCENT_IOA_TOKEN}, method = RequestMethod.POST)
  public ResponseEntity<OAuth2AccessToken> postAccessToken(@RequestParam
  Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
    // TokenEndpointAuthenticationFilter
    OAuth2AccessToken token = null;
    try {
      Object principal = AuthorizationUtils.getAuthentication();

      if (!(principal instanceof Authentication)) {
        throw new InsufficientAuthenticationException(
            "There is no client authentication. Try adding an appropriate authentication.");
      }

      String clientId = getClientId((Authentication) principal);
      ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId, true);

      TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
      if (clientId != null && !clientId.equals("")) {
        // Only validate the client details if a client authenticated during this
        // request.
        if (!clientId.equals(tokenRequest.getClientId())) {
          // double check to make sure that the client ID in the token request is the same as that in the
          // authenticated client
          throw new InvalidClientException("Given client ID does not match authenticated client");
        }
      }
      if (authenticatedClient != null) {
        oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
      }
      if (!StringUtils.hasText(tokenRequest.getGrantType())) {
        throw new InvalidRequestException("Missing grant type");
      }
      if (tokenRequest.getGrantType().equals(OAuth2Constants.PARAMETER.GRANT_TYPE_IMPLICIT)) {
        throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
      }

      if (isAuthCodeRequest(parameters)) {
        // The scope was requested or determined during the authorization step
        if (!tokenRequest.getScope().isEmpty()) {
          logger.debug("Clearing scope of incoming token request");
          tokenRequest.setScope(Collections.<String>emptySet());
        }
      }

      logger.debug("request parameters " + parameters);
      // The scope was requested or determined during the authorization step
      if (parameters.get(OAuth2Constants.PARAMETER.CODE) != null
          && !StringGenerator.uuidMatches(parameters.get(OAuth2Constants.PARAMETER.CODE))) {
        throw new InvalidRequestException("The code is not valid format .");
      }

      if (isRefreshTokenRequest(parameters)) {
        // A refresh token has its own default scopes, so we should ignore any added by the factory here.
        tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Constants.PARAMETER.SCOPE)));
      }
      //granter grant access token
      token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
      if (token == null) {
        throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
      }
    } catch (OAuth2Exception oauth2Exception) {
      token = new DefaultOAuth2AccessToken(oauth2Exception);
    } catch (InsufficientAuthenticationException authenticationException) {
      token = new DefaultOAuth2AccessToken(
          new OAuth2Exception(authenticationException.getMessage()));
    }
    return getResponse(token);
  }

在session里获取Authentication认证对象,然后得到clientId,进而得到ClientDetails对象

利用ClientDetails对象构建TokenRequest对象,TokenRequest对象的一些属性

requestParameters

clientId

scopes

grantType:授权枚举值有
authorization_code,password,implicit,client_credentials,refresh_token,id_token,token

在AbstractTokenGranter类里调用grant方法对TokenRequest对象授权

先构建OAuth2Request对象

再构建OAuth2Authentication对象

再构建OAuth2AccessToken对象

tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));

这行代码里面分别创建了refreshTokenaccessToken对象,accessToken中的value属性就是客户端需要的code,把它们保存到tokenStore抽象存储对象中,以code作为key

当前springboot配置文件中tokenStore接口的实现类是InMemoryTokenStore,也就是把refreshToken和accessToken对象保存到java内存ConcurrentHashMap容器中。

grant方法返回OAuth2AccessToken类型对象accessToken,返回给客户端。

OAuth 2.0 用户信息接口

7.用token请求用户信息接口

http://192.168.129.23:9527/sign/api/oauth/v20/me

请求参数有

access_token=上面步骤5中返回的accessToken对象的value属性

  @Operation(summary = "OAuth 2.0 用户信息接口", description = "传递参数access_token", method = "GET")
  @RequestMapping(value = OAuth2Constants.ENDPOINT.ENDPOINT_USERINFO, method = {RequestMethod.POST, RequestMethod.GET})
  public void apiV20UserInfo(@RequestParam(value = "access_token", required = false) String access_token, HttpServletRequest request,
      HttpServletResponse response) {

    if (StringUtils.isBlank(access_token)) {
      //for header authorization bearer
      access_token = AuthorizationHeaderUtils.resolveBearer(request);
    }

    if (!StringGenerator.uuidMatches(access_token)) {
      httpResponseAdapter.write(response, JsonUtils.gson2Json(accessTokenFormatError(access_token)), "json");
    }

    OAuth2Authentication oAuth2Authentication = null;
    try {
      oAuth2Authentication = oauth20tokenServices.loadAuthentication(access_token);
      String client_id = oAuth2Authentication.getOAuth2Request().getClientId();
      ClientDetails clientDetails = clientDetailsService.loadClientByClientId(client_id, true);
      Apps app = appsService.get(client_id);

      AbstractAuthorizeAdapter adapter;
      if (ConstsBoolean.isTrue(app.getIsAdapter())) {
        adapter = (AbstractAuthorizeAdapter) Instance.newInstance(app.getAdapter());
        try {
          BeanUtils.setProperty(adapter, "clientDetails", clientDetails);
        } catch (IllegalAccessException | InvocationTargetException e) {
          _logger.error("setProperty error . ", e);
        }
      } else {
        adapter = (AbstractAuthorizeAdapter) new OAuthDefaultUserInfoAdapter(clientDetails);
      }

      adapter.setPrincipal((SignPrincipal) oAuth2Authentication.getUserAuthentication().getPrincipal());
      adapter.setApp(app);

      Object jsonData = adapter.generateInfo();
      httpResponseAdapter.write(response, jsonData.toString(), "json");
    } catch (OAuth2Exception e) {
      HashMap<String, Object> authzException = new HashMap<String, Object>();
      authzException.put(OAuth2Exception.ERROR, e.getOAuth2ErrorCode());
      authzException.put(OAuth2Exception.DESCRIPTION, e.getMessage());
      httpResponseAdapter.write(response, JsonUtils.gson2Json(authzException), "json");
    }
  }

验证access_token 是否存在tokenStore的ConcurrentHashMap容器中,不存在抛出错误Invalid access token

如果存在则在tokenStore中取出OAuth2AccessToken对象和OAuth2Authentication对象,并销毁当前请求的access_token,因为只能一次请求有效,然后得到clientId,进而得到ClientDetails对象和Apps对象,这两个对象分别对应数据库表if_apps_oauth_client_details表和if_apps表

从Apps对象取得adapter属性字符串,这是一个类路径字符串,利用java反射机制得到adapter对象,调用adapter对象的generateInfo方法得到用户信息返回

如果Apps对象adapter属性为空,这时默认实例化OauthDefaultUserInfoAdapter对象,调用它的generateInfo方法得到用户信息返回给客户端。

默认adapter返回的具体用户信息有

  @Override
  public Object generateInfo() {

    String subject = AbstractAuthorizeAdapter.getValueByUserAttr(userInfo, clientDetails.getSubject());
    _logger.debug("userId : {} , username : {} , displayName : {} , subject : {}",
        userInfo.getId(),
        userInfo.getUsername(),
        userInfo.getDisplayName(),
        subject);

    HashMap<String, Object> beanMap = new HashMap<String, Object>();
    beanMap.put("randomId", (new StringGenerator()).uuidGenerate());
    beanMap.put("userId", userInfo.getId());
    //for spring security oauth2
    beanMap.put("user", subject);
    beanMap.put("username", subject);

    beanMap.put("displayName", userInfo.getDisplayName());
    beanMap.put("employeeNumber", userInfo.getEmployeeNumber());
    beanMap.put("email", userInfo.getEmail());
    beanMap.put("mobile", userInfo.getMobile());
    beanMap.put("realname", userInfo.getDisplayName());
    beanMap.put("birthday", userInfo.getBirthDate());
    beanMap.put("departmentId", userInfo.getDepartmentId());
    beanMap.put("department", userInfo.getDepartment());
    beanMap.put("createdate", userInfo.getCreatedDate());
    beanMap.put("title", userInfo.getJobTitle());
    beanMap.put("state", userInfo.getWorkRegion());
    beanMap.put("gender", userInfo.getGender());
    beanMap.put("institution", userInfo.getInstId());
    beanMap.put(WebConstants.ONLINE_TICKET_NAME, principal.getSession().getFormattedId());

    String info = JsonUtils.object2Json(beanMap);
    return info;
  }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值