Skip to content

接入流程

本文档详细介绍如何接入一单位一平台的 OAuth2 认证,包含完整的流程说明和代码示例。

业务场景

用户在一单位一平台上点击应用图标或消息链接,系统自动完成登录认证,无需重复输入用户名密码,实现单点登录体验。

整体流程

单点登录对接包含三个主要步骤:

步骤一:在一单位一平台创建应用

1.1 创建应用

登录一单位一平台管理后台,创建新的应用并填写应用信息。

1.2 配置应用信息

在创建应用时,需要配置以下信息:

配置项说明示例
应用名称应用的显示名称"办公系统"
应用首页地址应用的首页URLhttps://app.example.com
State生成接口应用提供的State生成接口地址https://app.example.com/api/sso/state
回调地址平台回调应用的固定地址https://app.example.com/api/sso/login

1.3 获取凭证信息

应用创建完成后,平台会分配以下凭证:

  • client_id: 应用唯一标识
  • client_secret: 应用密钥(需严格保密)

⚠️ 安全提示

  • client_secret 必须严格保密,不能泄露
  • 不要在前端代码中包含 client_secret

步骤二:配置State生成接口

2.1 接口说明

一单位一平台在发起授权前,会先调用应用提供的State生成接口。该接口不返回数据,而是直接重定向到一单位一平台的授权页面。

接口地址: 应用自行定义(如: https://app.example.com/api/sso/state)

请求方式: GET

请求参数:

参数名类型必填说明
redirect_uristring授权完成后的回调地址
client_idstring平台分配的应用标识

接口行为: 直接重定向到 /oauth/authorize 授权页面

2.2 接口实现示例

javascript
// Node.js (Express)
app.get('/api/sso/state', (req, res) => {
  const { redirect_uri, client_id } = req.query;

  // 验证client_id
  if (client_id !== process.env.CLIENT_ID) {
    return res.status(400).send('Invalid client_id');
  }

  // 生成随机state
  const state = crypto.randomBytes(16).toString('hex');

  // 存储state到缓存(如Redis),用于后续验证
  redis.set(`sso:state:${state}`, JSON.stringify({
    redirect_uri,
    timestamp: Date.now()
  }), 'EX', 600); // 10分钟过期

  // 直接重定向到授权页面,而不是返回JSON
  const authUrl = `https://platform.example.com/oauth/authorize?` +
    `response_type=code&` +
    `client_id=${client_id}&` +
    `redirect_uri=${encodeURIComponent(redirect_uri)}&` +
    `state=${state}`;

  res.redirect(authUrl);
});

步骤三:应用生成State并请求授权

3.1 授权流程时序图

3.2 关键说明

  1. State的生成: 应用在步骤二中生成State,用于防止CSRF攻击
  2. 重定向而非返回: State生成接口不返回JSON数据,而是直接重定向到授权页面
  3. State的验证: 应用在接收回调时必须验证State的有效性
  4. State的时效: State有效期建议为10分钟,过期自动失效

步骤四:接收授权码Code

4.1 授权回调

一单位一平台完成用户认证后,会重定向到应用提供的回调地址,并携带授权码。

回调地址: 在步骤一中配置的回调地址

回调参数:

参数名类型必填说明
codestring授权码,一次性使用,有效期10分钟
statestring步骤二中生成的State值

回调示例:

http
GET https://app.example.com/api/sso/callback?code=AUTH_CODE&state=a1b2c3d4e5f6g7h8i9j0

4.2 验证State参数

应用接收回调后,必须先验证State参数的有效性:

javascript
app.get('/api/sso/callback', async (req, res) => {
  const { code, state } = req.query;
  
  // 从缓存中获取state信息
  const stateData = await redis.get(`sso:state:${state}`);
  
  if (!stateData) {
    return res.status(400).json({
      code: 400,
      message: 'Invalid or expired state'
    });
  }
  
  const { redirect_uri } = JSON.parse(stateData);
  
  // 删除已使用的state
  await redis.del(`sso:state:${state}`);
  
  // 继续下一步...
});

步骤五:获取访问令牌

5.1 用授权码交换令牌

使用获取到的授权码,向一单位一平台请求访问令牌。

接口地址: POST /api/hztech-auth/oauth/token

请求头:

名称类型必填说明
AuthorizationstringBasic + base64(client_id:client_secret)
Hztech-Requested-Withstring固定值 HzTechHttpRequest
Content-Typestringapplication/x-www-form-urlencoded

请求参数:

参数名位置类型必填说明
grant_typequerystring固定值 authorization_code
codequerystring授权码
redirect_uriquerystring回调地址

请求示例:

javascript
const axios = require('axios');

async function getAccessToken(code) {
  const credentials = Buffer.from(
    `${process.env.CLIENT_ID}:${process.env.CLIENT_SECRET}`
  ).toString('base64');
  
  const response = await axios.post(
    'https://api.example.com/api/hztech-auth/oauth/token',
    null,
    {
      params: {
        grant_type: 'authorization_code',
        code: code,
        redirect_uri: process.env.APP_CALLBACK_URL
      },
      headers: {
        'Authorization': `Basic ${credentials}`,
        'Hztech-Requested-With': 'HzTechHttpRequest',
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    }
  );
  
  return response.data;
}

响应示例:

json
{
  "tenant_id": "000000",
  "user_id": "1123598821738675201",
  "dept_id": "1123598813738675201",
  "post_id": "1123598817738675201",
  "role_id": "1123598816738675201,1833900493036896258,1838843565126500354",
  "account": "admin",
  "user_name": "admin",
  "nick_name": "管理员",
  "real_name": "管理员",
  "role_name": "administrator,seatable,hzkb",
  "avatar": "",
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
  "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
  "token_type": "bearer",
  "expires_in": 3600,
  "detail": {
    "type": "web"
  }
}

5.2 响应字段说明

字段类型说明
tenant_idstring租户ID
user_idstring用户ID
accountstring用户账号
user_namestring用户名
nick_namestring昵称
real_namestring真实姓名
access_tokenstring访问令牌
refresh_tokenstring刷新令牌
expires_innumber过期时间(秒)

步骤六:完成登录

6.1 创建用户会话

获取到用户信息和令牌后,应用需要创建自己应用的登录信息

最近更新