添加北京大学统一认证接入支持skill
This commit is contained in:
@@ -12,8 +12,8 @@
|
||||
"Bash(tree:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(rg:*)", // ripgrep
|
||||
"Bash(fd:*)", // fd-find
|
||||
"Bash(rg:*)",
|
||||
"Bash(fd:*)",
|
||||
"Bash(file:*)",
|
||||
"Bash(stat:*)",
|
||||
"Bash(du:*)",
|
||||
@@ -25,7 +25,6 @@
|
||||
"Bash(date:*)",
|
||||
"Bash(uname:*)",
|
||||
|
||||
// ===== Git 只读操作 =====
|
||||
"Bash(git status:*)",
|
||||
"Bash(git diff:*)",
|
||||
"Bash(git log:*)",
|
||||
@@ -38,15 +37,12 @@
|
||||
"Bash(git shortlog:*)",
|
||||
"Bash(git rev-parse:*)",
|
||||
|
||||
// ===== 子代理 =====
|
||||
"Task",
|
||||
|
||||
// ===== 文件编辑 =====
|
||||
"Edit", // 编辑已有文件
|
||||
"Write", // 创建/覆写文件
|
||||
"MultiEdit", // 批量编辑
|
||||
"Edit",
|
||||
"Write",
|
||||
"MultiEdit",
|
||||
|
||||
// ===== 包管理 =====
|
||||
"Bash(pnpm install:*)",
|
||||
"Bash(pnpm add:*)",
|
||||
"Bash(pnpm run build:*)",
|
||||
@@ -55,7 +51,6 @@
|
||||
"Bash(pip install:*)",
|
||||
"Bash(pip3 install:*)",
|
||||
|
||||
// 相对安全的写操作
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(cp:*)",
|
||||
"Bash(mv:*)",
|
||||
@@ -77,7 +72,6 @@
|
||||
"Bash(tee:*)",
|
||||
"Bash(jq:*)",
|
||||
|
||||
// MCP
|
||||
"mcp__ide__getDiagnostics"
|
||||
],
|
||||
"deny": [
|
||||
|
||||
415
.claude/skills/pku-iaaa/SKILL.md
Normal file
415
.claude/skills/pku-iaaa/SKILL.md
Normal file
@@ -0,0 +1,415 @@
|
||||
---
|
||||
name: pku-iaaa
|
||||
description: |
|
||||
Integrate Peking University (PKU) IAAA unified authentication system into web applications.
|
||||
Use this skill whenever the user wants to implement PKU IAAA SSO login, PKU unified identity
|
||||
authentication, or connect their app to iaaa.pku.edu.cn. Also trigger when the user mentions
|
||||
"北大统一认证", "IAAA认证", "北大SSO", "PKU login", or wants to add campus authentication
|
||||
for a Peking University application. Covers login redirect, token validation, proxy SSO,
|
||||
and account verification — for any web framework (Django, Spring Boot, Express.js, Flask, etc).
|
||||
---
|
||||
|
||||
# 北京大学 IAAA 统一身份认证集成
|
||||
|
||||
本 skill 指导你将北京大学 IAAA(统一安全系统)身份认证集成到任意 Web 应用中。
|
||||
|
||||
## 前置条件
|
||||
|
||||
集成前需在 IAAA 注册应用系统,准备以下信息:
|
||||
- **应用系统 ID**(`appId`):英文字母开头,可含数字
|
||||
- **Key**:由 IAAA 提供,用于生成消息摘要
|
||||
- **服务器 IP 地址**:应用系统所在服务器的 IP
|
||||
- **回调地址 URL**(`redirectUrl`):用于接收 token 的回调端点
|
||||
|
||||
申请方式:管理部门用户登录校内门户 → "信息服务"或"办事大厅" → 搜索"统一身份认证应用备案申请"。
|
||||
注册后可在 `https://i3a.pku.edu.cn/iaaa/index.jsp` 自行管理服务器 IP 和回调 URL。
|
||||
|
||||
## 开始集成前,先确认需求
|
||||
|
||||
在动手写代码之前,先向用户确认:
|
||||
|
||||
1. **使用什么框架?** (Django / Spring Boot / Express.js / Flask / 其他)
|
||||
2. **需要哪种级别的集成?**
|
||||
- 基本认证(登录跳转 + Token 验证)— 绝大多数场景只需这个
|
||||
- 代理 SSO(中心应用 + 分支应用架构)— 参见 `references/proxy-sso.md`
|
||||
- 账号验证(仅校验某个学号/职工号是否存在)— 参见 `references/account-verify.md`
|
||||
3. **系统是否还有非校内账号?** 如果是,需要同时保留自有登录入口(影响前端跳转参数)
|
||||
|
||||
## 认证流程概览
|
||||
|
||||
```
|
||||
用户点击登录 ──POST──> iaaa.pku.edu.cn/iaaa/oauth.jsp
|
||||
│
|
||||
用户输入账号密码认证
|
||||
│
|
||||
认证成功,生成 token
|
||||
│
|
||||
302 重定向到 redirectUrl?token=xxx
|
||||
│
|
||||
应用服务端收到 token
|
||||
│
|
||||
服务端调用 validate.do 验证 token
|
||||
│
|
||||
验证成功,获取用户信息(identityId 等)
|
||||
│
|
||||
匹配本地用户,创建会话
|
||||
```
|
||||
|
||||
## 第一步:登录跳转(前端)
|
||||
|
||||
用户点击登录时,通过 POST 表单跳转到 IAAA 统一登录页面。
|
||||
|
||||
### 关键参数
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `appID` | 注册的应用系统 ID |
|
||||
| `redirectUrl` | 回调地址(验证 token 的端点),若含参数需 URLEncode |
|
||||
| `redirectLogonUrl` | (可选)应用自有登录地址,当系统还有非校内账号时使用 |
|
||||
|
||||
### 实现要点
|
||||
|
||||
- 必须使用 POST 方法提交表单到 `https://iaaa.pku.edu.cn/iaaa/oauth.jsp`
|
||||
- `redirectUrl` 的协议必须与实际部署一致(从当前请求动态获取,不要硬编码 `http://`)
|
||||
- 回调 URL 的尾部斜杠很重要——IAAA 可能因缺少尾部斜杠而拒绝回调
|
||||
|
||||
### 参考实现(JavaScript 前端通用)
|
||||
|
||||
```javascript
|
||||
function redirectToIAAALogin(appID, callbackPath, loginPath) {
|
||||
const form = document.createElement('form');
|
||||
form.action = 'https://iaaa.pku.edu.cn/iaaa/oauth.jsp';
|
||||
form.method = 'POST';
|
||||
form.style.display = 'none';
|
||||
|
||||
function addField(name, value) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = name;
|
||||
input.value = value;
|
||||
form.appendChild(input);
|
||||
}
|
||||
|
||||
// 使用当前页面协议,不要硬编码 http://
|
||||
const baseUrl = `${location.protocol}//${location.host}`;
|
||||
addField('appID', appID);
|
||||
addField('redirectUrl', `${baseUrl}${callbackPath}`);
|
||||
if (loginPath) {
|
||||
addField('redirectLogonUrl', `${baseUrl}${loginPath}`);
|
||||
}
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
```
|
||||
|
||||
## 第二步:Token 验证(服务端)
|
||||
|
||||
IAAA 认证成功后,会重定向浏览器到 `redirectUrl?token=<TOKEN>`。服务端需要验证这个 token。
|
||||
|
||||
### 验证端点
|
||||
|
||||
```
|
||||
GET https://iaaa.pku.edu.cn/iaaa/svc/token/validate.do
|
||||
?remoteAddr={REMOTE_ADDR}
|
||||
&appId={APP_ID}
|
||||
&token={TOKEN}
|
||||
&msgAbs={MSG_ABS}
|
||||
```
|
||||
|
||||
如果不需要返回用户详细信息(只需 identityId),可使用简单验证端点:
|
||||
```
|
||||
GET https://iaaa.pku.edu.cn/iaaa/svc/token/validateSimple.do?...(参数相同)
|
||||
```
|
||||
|
||||
### 参数说明
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `remoteAddr` | 访问应用系统的用户**真实 IP 地址** |
|
||||
| `appId` | 应用系统 ID |
|
||||
| `token` | IAAA 传回的票据 |
|
||||
| `msgAbs` | MD5 消息摘要(见下方计算方法) |
|
||||
|
||||
### msgAbs 计算方法(极其重要)
|
||||
|
||||
1. 取除 `msgAbs` 以外的所有请求参数
|
||||
2. 按**参数名升序**排列
|
||||
3. 拼接为查询字符串格式:`appId={APP_ID}&remoteAddr={REMOTE_ADDR}&token={TOKEN}`
|
||||
4. 将此字符串与 Key 直接拼接(不加分隔符):`PARA_STR + Key`
|
||||
5. 对拼接结果计算 MD5(UTF-8 编码,32 位十六进制,大小写不敏感)
|
||||
|
||||
**Python 示例:**
|
||||
```python
|
||||
import hashlib
|
||||
|
||||
para_str = f"appId={app_id}&remoteAddr={remote_addr}&token={token}"
|
||||
raw = para_str + key
|
||||
msg_abs = hashlib.md5(raw.encode('utf-8')).hexdigest()
|
||||
```
|
||||
|
||||
**Java 示例:**
|
||||
```java
|
||||
String paraStr = "appId=" + appId + "&remoteAddr=" + remoteAddr + "&token=" + token;
|
||||
String raw = paraStr + key;
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] digest = md.digest(raw.getBytes(StandardCharsets.UTF_8));
|
||||
String msgAbs = String.format("%032x", new BigInteger(1, digest));
|
||||
```
|
||||
|
||||
**Node.js 示例:**
|
||||
```javascript
|
||||
const crypto = require('crypto');
|
||||
const paraStr = `appId=${appId}&remoteAddr=${remoteAddr}&token=${token}`;
|
||||
const msgAbs = crypto.createHash('md5').update(paraStr + key).digest('hex');
|
||||
```
|
||||
|
||||
### 获取客户端真实 IP
|
||||
|
||||
反向代理(Nginx 等)环境下,客户端 IP 通常在 `X-Forwarded-For` 头中。取第一个值(最左侧)作为真实 IP:
|
||||
|
||||
```python
|
||||
# Python / Django
|
||||
remote_addr = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')[0].strip() \
|
||||
or request.META.get('REMOTE_ADDR', '')
|
||||
```
|
||||
|
||||
```java
|
||||
// Java / Spring Boot
|
||||
String remoteAddr = request.getHeader("X-Forwarded-For");
|
||||
if (remoteAddr != null && !remoteAddr.isEmpty()) {
|
||||
remoteAddr = remoteAddr.split(",")[0].trim();
|
||||
} else {
|
||||
remoteAddr = request.getRemoteAddr();
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// Node.js / Express
|
||||
const remoteAddr = (req.headers['x-forwarded-for'] || '').split(',')[0].trim()
|
||||
|| req.socket.remoteAddress;
|
||||
```
|
||||
|
||||
### 返回结果
|
||||
|
||||
**成功:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"errCode": "0",
|
||||
"errMsg": "认证成功",
|
||||
"userInfo": {
|
||||
"name": "用户姓名",
|
||||
"status": "账号状态(开通/禁止)",
|
||||
"identityId": "身份账号(学号/职工号)",
|
||||
"deptId": "所在院系码",
|
||||
"dept": "所在院系",
|
||||
"identityType": "身份类别(学生/职工)",
|
||||
"detailType": "身份细类",
|
||||
"identityStatus": "身份状态(在校/减离)",
|
||||
"campus": "校区"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**失败:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"errCode": "错误码",
|
||||
"errMsg": "错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
简单验证端点(`validateSimple.do`)成功时 `userInfo` 中只包含 `identityId`。
|
||||
|
||||
### 服务端验证的完整参考实现
|
||||
|
||||
#### Django
|
||||
|
||||
```python
|
||||
import hashlib
|
||||
import logging
|
||||
import requests
|
||||
from django.contrib.auth import login, get_user_model
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render, reverse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
User = get_user_model()
|
||||
|
||||
IAAA_APP_ID = "your_app_id"
|
||||
IAAA_KEY = "your_key"
|
||||
IAAA_USERID_FIELD = "username" # User 模型中与 identityId 对应的字段
|
||||
|
||||
def iaaa_callback(request):
|
||||
"""IAAA 认证回调视图"""
|
||||
token = request.GET.get('token')
|
||||
if not token:
|
||||
return render(request, 'login_failed.html', {'msg': '未收到认证票据'})
|
||||
|
||||
# 获取客户端真实 IP
|
||||
remote_addr = (
|
||||
request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')[0].strip()
|
||||
or request.META.get('REMOTE_ADDR', '')
|
||||
)
|
||||
|
||||
# 计算消息摘要(参数按字母序排列)
|
||||
para_str = f"appId={IAAA_APP_ID}&remoteAddr={remote_addr}&token={token}"
|
||||
msg_abs = hashlib.md5((para_str + IAAA_KEY).encode('utf-8')).hexdigest()
|
||||
|
||||
# 调用 IAAA 验证服务
|
||||
validate_url = (
|
||||
f"https://iaaa.pku.edu.cn/iaaa/svc/token/validate.do"
|
||||
f"?{para_str}&msgAbs={msg_abs}"
|
||||
)
|
||||
|
||||
try:
|
||||
resp = requests.get(validate_url, timeout=10)
|
||||
resp.raise_for_status()
|
||||
result = resp.json()
|
||||
except Exception as e:
|
||||
logger.error(f"IAAA token 验证请求失败: {e}")
|
||||
return render(request, 'login_failed.html', {'msg': '身份认证服务暂不可用,请稍后重试'})
|
||||
|
||||
if not result.get('success'):
|
||||
err_code = result.get('errCode', '未知')
|
||||
err_msg = result.get('errMsg', '未知错误')
|
||||
logger.warning(f"IAAA 认证失败: errCode={err_code}, errMsg={err_msg}")
|
||||
return render(request, 'login_failed.html', {'msg': f'认证失败:{err_msg}'})
|
||||
|
||||
# 认证成功,提取用户身份
|
||||
user_info = result.get('userInfo', {})
|
||||
identity_id = user_info.get('identityId', '')
|
||||
if not identity_id:
|
||||
logger.warning(f"IAAA 认证成功但未返回 identityId: {result}")
|
||||
return render(request, 'login_failed.html', {'msg': '无法获取您的身份账号'})
|
||||
|
||||
# 匹配本地用户
|
||||
user = User.objects.filter(**{IAAA_USERID_FIELD: identity_id}).first()
|
||||
if user is None:
|
||||
logger.info(f"IAAA 用户 {identity_id} 在系统中不存在")
|
||||
return render(request, 'login_failed.html', {'msg': '系统中不存在与您北大账号对应的用户'})
|
||||
|
||||
# 登录
|
||||
login(request, user)
|
||||
logger.info(f"IAAA 登录成功: {identity_id} ({user_info.get('name', '')})")
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
```
|
||||
|
||||
#### Spring Boot
|
||||
|
||||
```java
|
||||
@GetMapping("/iaaa/callback")
|
||||
public String iaaaCallback(
|
||||
@RequestParam String token,
|
||||
HttpServletRequest request,
|
||||
HttpSession session) throws Exception {
|
||||
|
||||
// 获取客户端真实 IP
|
||||
String remoteAddr = request.getHeader("X-Forwarded-For");
|
||||
if (remoteAddr != null && !remoteAddr.isEmpty()) {
|
||||
remoteAddr = remoteAddr.split(",")[0].trim();
|
||||
} else {
|
||||
remoteAddr = request.getRemoteAddr();
|
||||
}
|
||||
|
||||
// 计算消息摘要
|
||||
String paraStr = "appId=" + appId + "&remoteAddr=" + remoteAddr + "&token=" + token;
|
||||
String msgAbs = md5(paraStr + key);
|
||||
|
||||
// 调用验证服务
|
||||
String url = "https://iaaa.pku.edu.cn/iaaa/svc/token/validate.do?"
|
||||
+ paraStr + "&msgAbs=" + msgAbs;
|
||||
RestTemplate rest = new RestTemplate();
|
||||
Map<String, Object> result = rest.getForObject(url, Map.class);
|
||||
|
||||
if (!(Boolean) result.get("success")) {
|
||||
String errMsg = (String) result.getOrDefault("errMsg", "认证失败");
|
||||
// 处理失败...
|
||||
return "redirect:/login?error=" + URLEncoder.encode(errMsg, "UTF-8");
|
||||
}
|
||||
|
||||
Map<String, String> userInfo = (Map<String, String>) result.get("userInfo");
|
||||
String identityId = userInfo.get("identityId");
|
||||
// 查找本地用户,创建会话...
|
||||
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
private String md5(String input) throws Exception {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
return String.format("%032x", new java.math.BigInteger(1, digest));
|
||||
}
|
||||
```
|
||||
|
||||
#### Express.js
|
||||
|
||||
```javascript
|
||||
const crypto = require('crypto');
|
||||
const axios = require('axios');
|
||||
|
||||
app.get('/iaaa/callback', async (req, res) => {
|
||||
const { token } = req.query;
|
||||
if (!token) return res.redirect('/login?error=no_token');
|
||||
|
||||
const remoteAddr = (req.headers['x-forwarded-for'] || '').split(',')[0].trim()
|
||||
|| req.socket.remoteAddress;
|
||||
|
||||
const paraStr = `appId=${APP_ID}&remoteAddr=${remoteAddr}&token=${token}`;
|
||||
const msgAbs = crypto.createHash('md5').update(paraStr + KEY).digest('hex');
|
||||
|
||||
const url = `https://iaaa.pku.edu.cn/iaaa/svc/token/validate.do?${paraStr}&msgAbs=${msgAbs}`;
|
||||
|
||||
try {
|
||||
const { data } = await axios.get(url);
|
||||
|
||||
if (!data.success) {
|
||||
console.warn(`IAAA 认证失败: ${data.errCode} - ${data.errMsg}`);
|
||||
return res.redirect(`/login?error=${encodeURIComponent(data.errMsg || '认证失败')}`);
|
||||
}
|
||||
|
||||
const { identityId, name } = data.userInfo;
|
||||
if (!identityId) {
|
||||
return res.redirect('/login?error=no_identity');
|
||||
}
|
||||
|
||||
// 查找本地用户,创建 session...
|
||||
const user = await User.findOne({ where: { username: identityId } });
|
||||
if (!user) {
|
||||
return res.redirect('/login?error=user_not_found');
|
||||
}
|
||||
|
||||
req.session.userId = user.id;
|
||||
console.log(`IAAA 登录成功: ${identityId} (${name})`);
|
||||
res.redirect('/');
|
||||
} catch (err) {
|
||||
console.error('IAAA 验证请求失败:', err.message);
|
||||
res.redirect('/login?error=service_unavailable');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 常见易错点
|
||||
|
||||
实现时特别注意以下几点,这些是实际集成中最常见的 bug 来源:
|
||||
|
||||
1. **协议硬编码**:`redirectUrl` 中不要写死 `http://`,应从当前请求或配置动态获取。生产环境通常是 HTTPS,协议不匹配会导致回调失败。
|
||||
|
||||
2. **msgAbs 参数排序**:参数必须严格按参数名字母升序拼接。当前 `validate.do` 只有三个参数(`appId`, `remoteAddr`, `token`),恰好已是字母序。但如果使用代理认证等其他端点(参数更多),一定要排序。
|
||||
|
||||
3. **错误响应处理**:IAAA 返回 `success: false` 时,务必提取 `errCode` 和 `errMsg` 记录到日志并展示给用户,不要只返回一个笼统的"认证失败"。
|
||||
|
||||
4. **客户端 IP**:`remoteAddr` 必须是用户的真实 IP。如果应用在反向代理之后,使用 `X-Forwarded-For` 头的第一个值。IP 不匹配会导致 token 验证失败。
|
||||
|
||||
5. **回调 URL 尾部斜杠**:IAAA 在注册时绑定了精确的回调 URL,运行时传入的 `redirectUrl` 必须完全匹配(包括尾部斜杠)。
|
||||
|
||||
6. **服务端可达性**:应用服务器必须能访问 `iaaa.pku.edu.cn` 的 443 端口(HTTPS)。
|
||||
|
||||
## 高级功能
|
||||
|
||||
对于需要代理 SSO(中心应用 + 分支应用)或账号验证的场景,参阅:
|
||||
|
||||
- `references/proxy-sso.md` — 代理身份认证(单点登录多个应用)
|
||||
- `references/account-verify.md` — 验证北京大学账号是否存在
|
||||
78
.claude/skills/pku-iaaa/references/account-verify.md
Normal file
78
.claude/skills/pku-iaaa/references/account-verify.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# 北京大学账号验证
|
||||
|
||||
用于验证某个学号/职工号是否为有效的北京大学账号,不涉及登录流程。
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
GET https://iaaa.pku.edu.cn/iaaa/svc/pub/validate.do
|
||||
?userId={USERID}
|
||||
&userName={USERNAME}
|
||||
&appId={APPID}
|
||||
&msgAbs={MD5MSG}
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `userId` | 学号或职工号 |
|
||||
| `userName` | 账号姓名(传输时需 URLEncode,但计算 MD5 摘要时使用原始值,不做 URLEncode) |
|
||||
| `appId` | 应用 ID |
|
||||
| `msgAbs` | MD5 消息摘要 |
|
||||
|
||||
## msgAbs 计算
|
||||
|
||||
与其他端点规则一致:除 `msgAbs` 外所有参数按参数名升序排列拼接,再拼接 Key,取 MD5。
|
||||
|
||||
PARA_STR 排序后为:`appId={APPID}&userId={USERID}&userName={USERNAME}`
|
||||
|
||||
注意:`userName` 在 PARA_STR 中使用**原始值**(不做 URLEncode),只有在 URL 传输时才 URLEncode。
|
||||
|
||||
## 返回
|
||||
|
||||
**正常:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"valid": true,
|
||||
"userType": "用户身份类别"
|
||||
}
|
||||
```
|
||||
|
||||
`valid` 为 `true` 表示账号存在,`false` 表示不存在。
|
||||
|
||||
**异常:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"errCode": "错误代码",
|
||||
"errMsg": "错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
## Python 示例
|
||||
|
||||
```python
|
||||
import hashlib
|
||||
import urllib.parse
|
||||
import requests
|
||||
|
||||
def verify_pku_account(user_id: str, user_name: str, app_id: str, key: str) -> dict:
|
||||
"""验证北大账号是否存在"""
|
||||
# msgAbs 中 userName 使用原始值
|
||||
para_str = f"appId={app_id}&userId={user_id}&userName={user_name}"
|
||||
msg_abs = hashlib.md5((para_str + key).encode('utf-8')).hexdigest()
|
||||
|
||||
# URL 中 userName 需要 URLEncode
|
||||
url = (
|
||||
f"https://iaaa.pku.edu.cn/iaaa/svc/pub/validate.do"
|
||||
f"?appId={app_id}"
|
||||
f"&userId={user_id}"
|
||||
f"&userName={urllib.parse.quote(user_name)}"
|
||||
f"&msgAbs={msg_abs}"
|
||||
)
|
||||
resp = requests.get(url, timeout=10)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
```
|
||||
120
.claude/skills/pku-iaaa/references/proxy-sso.md
Normal file
120
.claude/skills/pku-iaaa/references/proxy-sso.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# 代理身份认证(Proxy SSO)
|
||||
|
||||
适用场景:应用 O(中心应用)登录后,用户点击进入应用 A、B、C(分支应用)时无需重新登录。
|
||||
|
||||
## 流程
|
||||
|
||||
```
|
||||
用户登录中心应用 O
|
||||
│
|
||||
├─ O 使用 validateWithProxy.do 验证 token → 获得 grantToken
|
||||
│
|
||||
├─ 用户点击分支应用 A
|
||||
│ ├─ O 调用 validateProxyGrantToken.do → 获得分支应用 A 的 token
|
||||
│ └─ 将 token 传给分支应用 A → A 用 validate.do 验证
|
||||
│
|
||||
└─ 用户退出中心应用 O
|
||||
└─ O 调用 expireProxy.do 注销 grantToken
|
||||
```
|
||||
|
||||
## API 详情
|
||||
|
||||
### 1. 中心应用验证(获取 grantToken)
|
||||
|
||||
```
|
||||
GET https://iaaa.pku.edu.cn/iaaa/svc/token/validateWithProxy.do
|
||||
?remoteAddr={REMOTE_ADDR}
|
||||
&appId={APP_ID}
|
||||
&token={TOKEN}
|
||||
&msgAbs={MSG_ABS}
|
||||
```
|
||||
|
||||
参数和 msgAbs 计算方式与标准 `validate.do` 相同。
|
||||
|
||||
**成功返回:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"errCode": "0",
|
||||
"errMsg": "认证成功",
|
||||
"userInfo": {
|
||||
"name": "用户姓名",
|
||||
"status": "账号状态",
|
||||
"identityId": "身份账号",
|
||||
"deptId": "所在院系码",
|
||||
"dept": "所在院系",
|
||||
"identityType": "身份类别",
|
||||
"detailType": "身份细类",
|
||||
"identityStatus": "身份状态"
|
||||
},
|
||||
"grantToken": "代理票据"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 获取分支应用 Token
|
||||
|
||||
```
|
||||
GET https://iaaa.pku.edu.cn/iaaa/svc/token/validateProxyGrantToken.do
|
||||
?remoteAddr={REMOTE_ADDR}
|
||||
&appId={APP_ID}
|
||||
&targetAppId={TARGET_APP_ID}
|
||||
&grantToken={GRANT_TOKEN}
|
||||
×tamp={TIMESTAMP}
|
||||
&msgAbs={MSG_ABS}
|
||||
```
|
||||
|
||||
**参数:**
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `remoteAddr` | 客户端 IP |
|
||||
| `appId` | 中心应用 ID |
|
||||
| `targetAppId` | 分支应用 ID |
|
||||
| `grantToken` | 上一步获得的代理票据 |
|
||||
| `timestamp` | 当前时间戳(long 型整数) |
|
||||
| `msgAbs` | MD5 摘要,PARA_STR 为除 msgAbs 外所有参数按参数名升序拼接 |
|
||||
|
||||
注意:此端点参数较多,PARA_STR 排序后为:
|
||||
`appId={}&grantToken={}&remoteAddr={}&targetAppId={}×tamp={}`
|
||||
|
||||
**成功返回:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"errCode": "0",
|
||||
"errMsg": "验证成功",
|
||||
"token": "分支应用访问票据"
|
||||
}
|
||||
```
|
||||
|
||||
**错误码 12(需要手机验证):** 重定向用户到:
|
||||
```
|
||||
https://iaaa.pku.edu.cn/iaaa/ma4proxy.jsp
|
||||
?proxyAppId={中心应用ID}
|
||||
&appId={分支应用ID}
|
||||
&userId={用户账号}
|
||||
&grantToken={GRANT_TOKEN}
|
||||
&redirectUrl={分支应用回调地址,需URLEncode}
|
||||
```
|
||||
|
||||
### 3. 分支应用验证 Token
|
||||
|
||||
分支应用收到 token 后,使用标准 `validate.do` 或 `validateSimple.do` 验证,与普通认证流程相同。
|
||||
|
||||
### 4. 注销代理 Token
|
||||
|
||||
中心应用退出时或必要时调用:
|
||||
|
||||
```
|
||||
GET https://iaaa.pku.edu.cn/iaaa/svc/token/expireProxy.do
|
||||
?appId={APP_ID}
|
||||
&grantToken={GRANT_TOKEN}
|
||||
&msgAbs={MSG_ABS}
|
||||
```
|
||||
|
||||
msgAbs 的 PARA_STR:`appId={}&grantToken={}`(按参数名升序)
|
||||
|
||||
**返回:**
|
||||
```json
|
||||
{"success": true, "errCode": "0", "errMsg": "注销成功"}
|
||||
```
|
||||
@@ -407,6 +407,7 @@ EOF
|
||||
echo ""
|
||||
git init
|
||||
git config init.defaultBranch main
|
||||
git config core.pager "less -F -X"
|
||||
print_info "设置 Git 用户配置..."
|
||||
git config user.email "$GIT_USER_EMAIL"
|
||||
git config user.name "$GIT_USER_NAME"
|
||||
|
||||
Reference in New Issue
Block a user