服务端点:

快速开始

步骤1: 注册应用

  1. 访问 https://conn.nodeloc.cc/apps
  2. 首次访问会重定向到NL进行身份验证
  3. 登录后点击”Create New Application”
  4. 填写应用信息并创建
重要: 保存显示的Client Secret,它只会显示一次

步骤2: 获取应用凭据

注册成功后,你将获得:
  • Client ID: abcd1234567890ef (用于标识应用)
  • Client Secret: secret_xyz123 (用于应用认证)
  • 重定向URI: 用户授权后的回调地址

步骤3: 实现OAuth流程

1

重定向用户到授权页面

引导用户访问授权URL进行身份验证
2

用户同意后获取授权码

用户同意授权后,系统会重定向到你的回调地址并携带授权码
3

使用授权码交换访问令牌

在后端使用授权码向令牌端点请求访问令牌
4

使用令牌获取用户信息

使用访问令牌调用用户信息端点获取用户详细信息

API 参考

授权端点

const authUrl = 'https://conn.nodeloc.cc/oauth2/auth?' + new URLSearchParams({
  response_type: 'code',
  client_id: 'YOUR_CLIENT_ID',
  redirect_uri: 'https://myapp.com/callback',
  scope: 'openid profile',
  state: 'random_state_string'
});

window.location.href = authUrl;
GET /oauth2/auth
参数类型必需说明
response_typestring固定值: code
client_idstring应用Client ID
redirect_uristring回调地址
scopestring建议: openid profile
statestring防CSRF随机字符串

令牌端点

const response = await fetch('https://conn.nodeloc.cc/oauth2/token', {
  method: 'POST',
  headers: {
    'Authorization': 'Basic ' + btoa('CLIENT_ID:CLIENT_SECRET'),
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: 'authorization_code_here',
    redirect_uri: 'https://myapp.com/callback'
  })
});

const tokens = await response.json();
POST /oauth2/token 请求头:
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
获取访问令牌参数:
  • grant_type: authorization_code
  • code: 授权码
  • redirect_uri: 回调地址
刷新令牌参数:
  • grant_type: refresh_token
  • refresh_token: 刷新令牌
响应:
{
  "access_token": "访问令牌",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "刷新令牌",
  "id_token": "ID令牌(JWT格式)"
}

用户信息端点

const userResponse = await fetch('https://conn.nodeloc.cc/oauth2/userinfo', {
  headers: {
    'Authorization': 'Bearer ' + access_token
  }
});

const userInfo = await userResponse.json();
GET /oauth2/userinfo 请求头:
Authorization: Bearer {access_token}
响应:
{
  "sub": "用户ID",
  "username": "用户名",
  "email": "邮箱地址",
  "groups": ["用户组1", "用户组2"]
}

代码集成

JavaScript 客户端

class NodeLocOAuth2Client {
  constructor(clientId, clientSecret, redirectUri) {
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.redirectUri = redirectUri;
    this.baseUrl = 'https://conn.nodeloc.cc';
  }

  // 生成授权URL
  getAuthUrl(state = null) {
    const params = new URLSearchParams({
      response_type: 'code',
      client_id: this.clientId,
      redirect_uri: this.redirectUri,
      scope: 'openid profile'
    });
    
    if (state) params.append('state', state);
    return `${this.baseUrl}/oauth2/auth?${params}`;
  }

  // 交换访问令牌
  async exchangeCodeForTokens(code) {
    const response = await fetch(`${this.baseUrl}/oauth2/token`, {
      method: 'POST',
      headers: {
        'Authorization': 'Basic ' + btoa(`${this.clientId}:${this.clientSecret}`),
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code: code,
        redirect_uri: this.redirectUri
      })
    });

    if (!response.ok) {
      throw new Error(`Token exchange failed: ${response.status}`);
    }
    return await response.json();
  }

  // 获取用户信息
  async getUserInfo(accessToken) {
    const response = await fetch(`${this.baseUrl}/oauth2/userinfo`, {
      headers: { 'Authorization': `Bearer ${accessToken}` }
    });

    if (!response.ok) {
      throw new Error(`Get user info failed: ${response.status}`);
    }
    return await response.json();
  }

  // 刷新令牌
  async refreshToken(refreshToken) {
    const response = await fetch(`${this.baseUrl}/oauth2/token`, {
      method: 'POST',
      headers: {
        'Authorization': 'Basic ' + btoa(`${this.clientId}:${this.clientSecret}`),
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      body: new URLSearchParams({
        grant_type: 'refresh_token',
        refresh_token: refreshToken
      })
    });

    if (!response.ok) {
      throw new Error(`Token refresh failed: ${response.status}`);
    }
    return await response.json();
  }

  // 解析JWT ID Token
  parseJwtToken(token) {
    const base64Url = token.split('.')[1];
    const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
    const jsonPayload = decodeURIComponent(
      atob(base64).split('').map(c => 
        '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
      ).join('')
    );
    return JSON.parse(jsonPayload);
  }
}

Node.js Express 集成

const express = require('express');
const session = require('express-session');

const app = express();
app.use(session({
  secret: 'your-session-secret',
  resave: false,
  saveUninitialized: true
}));

const oauth2Client = new NodeLocOAuth2Client(
  process.env.CLIENT_ID,
  process.env.CLIENT_SECRET,
  'https://yourapp.com/auth/callback'
);

// 登录路由
app.get('/login', (req, res) => {
  const state = Math.random().toString(36);
  req.session.oauthState = state;
  const authUrl = oauth2Client.getAuthUrl(state);
  res.redirect(authUrl);
});

// 回调路由
app.get('/auth/callback', async (req, res) => {
  const { code, state } = req.query;
  
  // 验证state参数
  if (state !== req.session.oauthState) {
    return res.status(400).send('Invalid state parameter');
  }
  
  try {
    const tokens = await oauth2Client.exchangeCodeForTokens(code);
    const userInfo = await oauth2Client.getUserInfo(tokens.access_token);
    
    req.session.user = userInfo;
    req.session.tokens = tokens;
    
    res.redirect('/dashboard');
  } catch (error) {
    console.error('OAuth error:', error);
    res.status(500).send('Authentication failed');
  }
});

// 受保护路由
app.get('/dashboard', (req, res) => {
  if (!req.session.user) {
    return res.redirect('/login');
  }
  res.json(req.session.user);
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Python Flask 集成

import requests
import base64
import json
from urllib.parse import urlencode

class NodeLocOAuth2Client:
    def __init__(self, client_id, client_secret, redirect_uri):
        self.client_id = client_id
        self.client_secret = client_secret
        self.redirect_uri = redirect_uri
        self.base_url = 'https://conn.nodeloc.cc'
    
    def get_auth_url(self, state=None):
        params = {
            'response_type': 'code',
            'client_id': self.client_id,
            'redirect_uri': self.redirect_uri,
            'scope': 'openid profile'
        }
        if state:
            params['state'] = state
        return f"{self.base_url}/oauth2/auth?{urlencode(params)}"
    
    def exchange_code_for_tokens(self, code):
        auth_header = base64.b64encode(
            f"{self.client_id}:{self.client_secret}".encode()
        ).decode()
        
        response = requests.post(
            f"{self.base_url}/oauth2/token",
            data={
                'grant_type': 'authorization_code',
                'code': code,
                'redirect_uri': self.redirect_uri
            },
            headers={
                'Authorization': f'Basic {auth_header}',
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        )
        response.raise_for_status()
        return response.json()
    
    def get_user_info(self, access_token):
        response = requests.get(
            f"{self.base_url}/oauth2/userinfo",
            headers={'Authorization': f'Bearer {access_token}'}
        )
        response.raise_for_status()
        return response.json()

应用管理 API

获取应用列表

GET /api/apps 响应:
[
  {
    "client_id": "应用ID",
    "name": "应用名称",
    "description": "应用描述",
    "owner_username": "创建者",
    "created_at": "2024-01-01T00:00:00Z"
  }
]

创建应用

POST /api/apps 请求体:
{
  "name": "应用名称",
  "description": "应用描述",
  "redirect_uris": ["https://app.com/callback"],
  "allow_groups": ["developers"],
  "deny_groups": ["banned"]
}
响应:
{
  "client_id": "生成的应用ID",
  "client_secret": "生成的密钥",
  "name": "应用名称",
  "description": "应用描述"
}

安全最佳实践

客户端密钥保护

永远不要在前端代码中暴露Client Secret
  • 使用环境变量存储敏感信息
  • 定期轮换客户端密钥
  • 在服务器端处理令牌交换

CSRF 防护

始终使用随机的state参数防止CSRF攻击
  • 在回调中验证state参数
  • 使用HTTPS传输所有敏感数据
  • 实现会话超时机制

令牌管理

  • 安全存储访问令牌和刷新令牌
  • 实现令牌自动刷新机制
  • 处理令牌过期情况

重定向URI 验证

  • 使用精确匹配的重定向URI
  • 避免使用通配符
  • 生产环境必须使用HTTPS

错误处理

常见错误代码

错误HTTP状态说明解决方案
invalid_client401Client ID/Secret错误检查应用凭据
invalid_grant400授权码无效/过期重新获取授权码
invalid_redirect_uri400回调URI不匹配检查注册的URI
access_denied403用户拒绝授权引导用户重新授权
unsupported_grant_type400不支持的授权类型使用正确的grant_type

错误响应格式

{
  "error": "invalid_client",
  "error_description": "Client authentication failed"
}

错误处理示例

async function handleOAuthError(error) {
  if (error.message.includes('invalid_client')) {
    console.error('客户端认证失败,请检查Client ID和Secret');
    // 重定向到配置页面
  } else if (error.message.includes('invalid_grant')) {
    console.error('授权码无效或过期,重新获取授权');
    // 重新发起授权流程
    window.location.href = oauth2Client.getAuthUrl();
  } else {
    console.error('未知OAuth错误:', error);
    // 显示通用错误信息
  }
}

测试和调试

使用 cURL 测试

1

获取授权码

在浏览器中访问授权URL:
https://conn.nodeloc.cc/oauth2/auth?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https://yourapp.com/callback&scope=openid
2

交换访问令牌

curl -X POST https://conn.nodeloc.cc/oauth2/token \
  -H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code&code=AUTH_CODE&redirect_uri=https://yourapp.com/callback"
3

获取用户信息

curl -H "Authorization: Bearer ACCESS_TOKEN" \
  https://conn.nodeloc.cc/oauth2/userinfo

在线调试工具

使用 OpenID Connect Debugger
配置信息:
  • Authorize URI: https://conn.nodeloc.cc/oauth2/auth
  • Client ID: 你的应用ID
  • Scope: openid profile
  • Response Type: code

开发环境配置

// 环境配置
const config = {
  development: {
    clientId: 'dev_client_id',
    clientSecret: 'dev_client_secret',
    redirectUri: 'http://localhost:3000/auth/callback'
  },
  production: {
    clientId: process.env.OAUTH_CLIENT_ID,
    clientSecret: process.env.OAUTH_CLIENT_SECRET,
    redirectUri: 'https://yourapp.com/auth/callback'
  }
};

const currentConfig = config[process.env.NODE_ENV || 'development'];
const oauth2Client = new NodeLocOAuth2Client(
  currentConfig.clientId,
  currentConfig.clientSecret,
  currentConfig.redirectUri
);