Skip to content

用户中心文档 V1.3.3

1. 概述

文档助力用户中心侧统一管控第三方访问权限,提升对接效率与系统安全性。 主要涵盖主流鉴权方式(OAuth2.0 等)的适用场景、具体对接流程(含凭证申请、鉴权请求、令牌刷新等步骤)、参数格式要求、错误码说明及安全合规规范,帮助第三方开发者快速理解并合规完成鉴权对接。


2. 前置准备/步骤

  1. 第三方申请时需提供:应用名称、回调地址
  2. 用户中心运维/开发收到请求后,在应用管理中创建应用并返回:clientId、clientSecret

3. 名词解释

  • 回调地址:单点登录后跳转的地址,由第三方应用开发提供
  • clientId及clientSecret:第三方应用在用户中心的ID及密钥,务必妥善保存,禁止向第三方泄露

4. 流程图


5. 接口文档

5.1 认证接口

  • URL/api/smartpark-oauth/oauth/authorize
  • 请求方法:GET

5.1.2 请求头参数

参数key类型是否必填描述
AuthorizationStringBasic dGVzdDp0ZXN0(client_id:client_secret 转base64)
Content-TypeStringapplication/json

5.1.3 请求参数

参数key类型是否必填描述
client_idString应用id
redirect_uriString回调地址(需与申请一致),成功后携带code
response_typeString固定值:code

5.1.4 返回参数

无,重定向至redirect_uri并携带code

5.1.5 请求样例

https://spup-uat.icloudcity.com/api/smartpark-oauth/oauth/authorize?client_id=test&redirect_uri=https://www.baidu.com&response_type=code

5.2 code换取token

根据重定向携带的code换取access_token

  • URL/api/smartpark-oauth/oauth/token
  • 请求方法:POST

5.2.2 请求头参数

参数key类型是否必填描述
AuthorizationStringBasic dGVzdDp0ZXN0
Content-TypeStringapplication/json

5.2.3 请求参数

参数key类型是否必填描述
codeString5.1接口返回的code
redirect_uriString回调地址(需与申请一致)
grant_typeString固定值:authorization_code

5.2.4 返回参数

参数key类型描述
access_tokenString当前用户token
token_typeStringToken类型
refresh_tokenString刷新token
expires_inLongtoken过期时间

5.2.5 请求样例

https://spup-uat.icloudcity.com/api/smartpark-oauth/oauth/token?code=85bVWq&grant_type=authorization_code&redirect_uri=https://www.baidu.com

返回示例

json
{
    "code": 200,
    "success": true,
    "data": {
        "access_token":"xxx",
        "token_type": "bearer",
        "refresh_token":"xxx",
        "expires_in": 3599
    },
    "msg": "操作成功"
}

5.3 根据access_token获取用户信息

  • URL/api/smartpark-oauth/oauth/getCurrentUser
  • 请求方法:GET

5.3.2 请求头参数

参数key类型是否必填描述
AuthorizationStringBasic dGVzdDp0ZXN0

5.3.3 请求参数

参数key类型是否必填描述
tokenStringaccess_token

5.3.4 返回参数

参数key类型描述
idLong租户ID
codeString用户编码
accountString账号
nameString昵称
realNameString真实姓名
avatarString头像
emailString邮箱
phoneString手机
birthdayString生日
sexString1男/2女
roleIdString角色id,多角色逗号分隔
deptIdString组织id,多组织逗号分隔

5.3.5 请求样例

返回示例

json
{
    "code": 200,
    "success": true,
    "data": {
        "id": "1123598821738675201",
        "code": null,
        "account": "admin",
        "name": "管理员",
        "realName": "管理员",
        "avatar": "https://xxx/zos/rmsportal.png",
        "email": "admin@bladex.vip",
        "phone": "123333333333",
        "birthday": "2018-08-08 00:00:00",
        "sex": 1,
        "roleId": "1123598816738675201,1123598816738675203",
        "deptId": "1123598813738675201,1123598816738675211"
    },
    "msg": "操作成功"
}

5.4 refresh_token换取access_token接口

  • URL/api/smartpark-oauth/oauth/token
  • 请求方法:POST

5.4.2 请求头参数

参数key类型是否必填描述
AuthorizationStringBasic dGVzdDp0ZXN0
Content-TypeStringapplication/json

5.4.3 请求参数

参数key类型是否必填描述
refresh_tokenString认证接口返回的refresh_token
grant_typeString固定值:refresh_token

5.4.4 返回参数

参数key类型描述
access_tokenString用户token
token_typeStringToken类型
refresh_tokenString刷新token
expires_inLong过期时间
scopeString权限范围

5.4.5 请求样例

返回示例

json
{
    "code": 200,
    "success": true,
    "data": {
        "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
        "token_type": "bearer",
        "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
        "expires_in": 3599,
        "scope": "all"
    },
    "msg": "操作成功"
}

5.5 通过组织id列表,查询所有项目接口

  • URL/api/smartpark-system/dept/getDeptsAndChildrenProject
  • 请求方法:POST

5.5.2 请求头参数

参数key类型是否必填描述
AuthorizationStringBasic dGVzdDp0ZXN0
Content-TypeStringapplication/json

5.5.3 请求参数

参数key类型是否必填描述
deptIds[]组织ID集合

5.5.4 返回参数

参数key类型描述
idLongID
nameString名称
codeString编码
parentIdString父ID
typeStringdept=组织,project=项目

5.5.5 请求样例

请求参数

json
{
  "deptIds": [2003645334965362690]
}

返回示例

json
{
  "code": 200,
  "success": true,
  "data": [
    {
      "id": "2003645334965362690",
      "name": "北京四维图新科技股份股份有限公司",
      "code": "OO0000001",
      "parentId": "1123598813738675201",
      "type": "dept",
      "parentCode": null,
      "children": null
    },
    {
      "id": "2003700874080854017",
      "name": "四维图新集团",
      "code": "P0001",
      "parentId": "2003645334965362690",
      "type": "project",
      "parentCode": null,
      "children": null
    }
  ],
  "msg": "操作成功"
}

5.6 通过角色id,查当前用户是否有这个角色权限接口

  • URL/api/smartpark-system/role/whetherHasRolePermission
  • 请求方法:POST

5.6.2 请求头参数

参数key类型是否必填描述
AuthorizationStringBasic dGVzdDp0ZXN0
Content-TypeStringapplication/json

5.6.3 请求参数

参数key类型是否必填描述
roleIdLong角色ID
tokenStringaccess_token

5.6.4 返回参数

参数key类型描述
whetherHasRoleBooleantrue=有权限,false=无

5.6.5 请求样例

返回示例

json
{
    "code": 200,
    "success": true,
    "data": {
        "whetherHasRole": false
    },
    "msg": "操作成功"
}

6. Q&A

错误信息原因处理方式
error_description="Invalid redirect: https://xxx match one of the registered values."回调地址错误核对/修改注册的回调地址
系统未知异常[Httpstatus]:401未携带Authorization请求头补充必填Authorization请求头
Missing grant typegrant_type错误按文档传入固定grant_type值

7. 示例

java
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import com.alibaba.fastjson.JSON;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.entity.ContentType;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;

@Slf4j
public class OpenApiClientAuth {

   private OpenApiClientAuth() {
   }

   public static final String AUTHORIZATION = "Authorization";
   public static final String CONTENT_TYPE = "Content-Type";
   public static final String DATE = "Date";
   public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter
      .ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH);

   /**
    * @param httpMethod请求方式 GET,POST,PUT。。。
    * @param urlhttp://baidu.com/xxx/xx?aa=11
    * @param clientId123123
    * @param clientSecret sdfdsf
    * @param contentType请求类型
    * @return 返回授权说需要的Header参数
    */
   @SneakyThrows
   public static Map<String, String> getSignHeaders(final String httpMethod, final String url,
                                        final String clientId, final String clientSecret, final String contentType) {
      final String pathResource = OpenApiClientAuth.getPathResource(url);
      final String contentTypeValue = contentType != null ? contentType : "application/json";
      final String dateValue = DATE_TIME_FORMATTER.format(ZonedDateTime.now(ZoneId.of("GMT")));

      final String sign = OpenApiClientAuth
         .calculateSing(httpMethod, contentTypeValue, dateValue, pathResource, clientSecret);
      final String authorizationString = clientId + ":" + sign;

      final Map<String, String> authHeaders = new HashMap<>(4);
      authHeaders.put(OpenApiClientAuth.DATE, dateValue);
      authHeaders.put(OpenApiClientAuth.AUTHORIZATION, authorizationString);
      authHeaders.put(OpenApiClientAuth.CONTENT_TYPE, contentTypeValue);

      return authHeaders;
   }

   protected static String getPathResource(final String url) {
      final String substring = url.substring(url.indexOf("://") + 3);
      return substring.substring(substring.indexOf('/'));
   }

   protected static String calculateSing(final String httpMethodString,
                                final String contentTypeValue, final String dateValue, final String pathResource,
                                final String appSecret) throws InvalidKeyException, NoSuchAlgorithmException {
      final String signToString = OpenApiClientAuth
         .builderStringToSign(httpMethodString, contentTypeValue, dateValue,
            pathResource);

      final String base64HashString = HMAC.hmacSha1Encrypt(signToString, appSecret);

      return base64HashString.substring(5, 15);
   }

   protected static String builderStringToSign(final String method,
                                    final String contentTypeValue, final String dateValue, final String pathResource) {
      return method
         + "\n"
         + OpenApiClientAuth.handleNullString(contentTypeValue)
         + "\n"
         + OpenApiClientAuth.handleNullString(dateValue)
         + "\n"
         + pathResource;
   }

   protected static String handleNullString(final String str) {
      return !StringUtils.hasText(str) ? "" : str;
   }

   protected static class HMAC {

      private HMAC() {
      }

      private static final String KEY_MAC_SHA1 = "HmacSHA1";

      public static String hmacSha1Encrypt(final String encryptText, final String encryptKey)
         throws NoSuchAlgorithmException, InvalidKeyException {
         final byte[] text = encryptText.getBytes(StandardCharsets.UTF_8);
         final byte[] keyData = encryptKey.getBytes(StandardCharsets.UTF_8);
         final SecretKeySpec secretKey = new SecretKeySpec(keyData, HMAC.KEY_MAC_SHA1);
         final Mac mac = Mac.getInstance(secretKey.getAlgorithm());
         mac.init(secretKey);
         return new String(Base64.getEncoder().encode(mac.doFinal(text)),
            StandardCharsets.UTF_8);
      }
   }

   /**
    * @param url请求地址
    * @param body请求参数 json格式
    * @param httpMethod请求方法
    * @param clientIdclientId
    * @param clientSecret clientSecret
    * @return 返回值
    */
   public static String openApiClientExecute(String url, String body, HttpMethod httpMethod, String clientId, String clientSecret) {
      log.info("request url:{},param:{},httpMethod:{},appKey:{},appSecret:{}", url, body, httpMethod, clientId, clientSecret);
      RestTemplate restTemplate = new RestTemplate();

      Map<String, String> signHeaders = OpenApiClientAuth.getSignHeaders(httpMethod.toString(), url, clientId, clientSecret, ContentType.APPLICATION_JSON.toString());
      final HttpHeaders headers = new HttpHeaders();
      signHeaders.forEach(headers::set);
      if (!StringUtils.hasText(body)) {
         body = "";
      }
      final HttpEntity<String> httpEntity = new HttpEntity<>(body, headers);
      final ParameterizedTypeReference<String> parameterizedTypeReference = new ParameterizedTypeReference<String>() {};
      String r = null;
      try {
         ResponseEntity<String> exchange = restTemplate.exchange(url, httpMethod, httpEntity, parameterizedTypeReference);

         r = exchange.getBody();
      } catch (Exception e) {
         log.error("request error: ", e);
      }
      log.info("request url:{},param:{},httpMethod:{},appKey:{},appSecret:{} ,result:{} ", url, body, httpMethod, clientId, clientSecret, r);
      return r;
   }

   public static void main(String[] args) {
      String getUrl = "https://spup-uat.icloudcity.com/api/open/smartpark-notification/inform/getConfigPage";
      String clientId = "d9cmxqqg8bwepv38";
      String clientSecret = "3o1hhpfdy9yq6fpjqes6tnk6mxus5ngmthxdn1iprwelffio";
      // 无body示例
      String s1 = openApiClientExecute(String.format(getUrl, "i000000083"), null, HttpMethod.GET, clientId, clientSecret);
      System.out.println(s1);
      //有body示例
      String postUrl = "https://spup-uat.icloudcity.com/api/open/smartpark-notification/inform/getServiceList";
      List<String> body = new ArrayList<>();
      body.add("165801378272102400113ljqw0");
      body.add("wulingshili");
      body.add("IBMS_tunnel_001");

      String s2 = openApiClientExecute(postUrl, JSON.toJSONString(body), HttpMethod.POST, clientId, clientSecret);
      System.out.println(s2);
   }
}