主题
接入流程
本文档详细介绍如何接入一单位一平台的 OAuth2 认证,包含完整的流程说明和代码示例。
业务场景
用户在一单位一平台上点击应用图标或消息链接,系统自动完成登录认证,无需重复输入用户名密码,实现单点登录体验。
整体流程
单点登录对接包含三个主要步骤:
步骤一:在一单位一平台创建应用
1.1 创建应用
登录一单位一平台管理后台,创建新的应用并填写应用信息。
1.2 配置应用信息
在创建应用时,需要配置以下信息:
| 配置项 | 说明 | 示例 |
|---|---|---|
| 应用名称 | 应用的显示名称 | "办公系统" |
| 应用首页地址 | 应用的首页URL | https://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_uri | string | 是 | 授权完成后的回调地址 |
| client_id | string | 是 | 平台分配的应用标识 |
接口行为: 直接重定向到 /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 关键说明
- State的生成: 应用在步骤二中生成State,用于防止CSRF攻击
- 重定向而非返回: State生成接口不返回JSON数据,而是直接重定向到授权页面
- State的验证: 应用在接收回调时必须验证State的有效性
- State的时效: State有效期建议为10分钟,过期自动失效
步骤四:接收授权码Code
4.1 授权回调
一单位一平台完成用户认证后,会重定向到应用提供的回调地址,并携带授权码。
回调地址: 在步骤一中配置的回调地址
回调参数:
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| code | string | 是 | 授权码,一次性使用,有效期10分钟 |
| state | string | 是 | 步骤二中生成的State值 |
回调示例:
http
GET https://app.example.com/api/sso/callback?code=AUTH_CODE&state=a1b2c3d4e5f6g7h8i9j04.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
请求头:
| 名称 | 类型 | 必填 | 说明 |
|---|---|---|---|
| Authorization | string | 是 | Basic + base64(client_id:client_secret) |
| Hztech-Requested-With | string | 是 | 固定值 HzTechHttpRequest |
| Content-Type | string | 是 | application/x-www-form-urlencoded |
请求参数:
| 参数名 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| grant_type | query | string | 是 | 固定值 authorization_code |
| code | query | string | 是 | 授权码 |
| redirect_uri | query | string | 是 | 回调地址 |
请求示例:
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_id | string | 租户ID |
| user_id | string | 用户ID |
| account | string | 用户账号 |
| user_name | string | 用户名 |
| nick_name | string | 昵称 |
| real_name | string | 真实姓名 |
| access_token | string | 访问令牌 |
| refresh_token | string | 刷新令牌 |
| expires_in | number | 过期时间(秒) |
步骤六:完成登录
6.1 创建用户会话
获取到用户信息和令牌后,应用需要创建自己应用的登录信息