Skip to content
Esta página se tradujo del original en chino, que ha cambiado desde entonces, por lo que puede estar desactualizada. La versión china es la autorizada. Ver el original en chino →

Autenticación de API (máquina a máquina) En producción ​

Orientada a la autenticación M2M: a diferencia del flujo OIDC del navegador, M2M usa la Service Account (cuenta de servicio) de Casdoor, obteniendo un access token sin intervención humana. Instancia de Casdoor https://identity.lurus.cn; los endpoints siguientes se basan todos en ella.

3métodos de autenticación
RS256firma de JWT Profile
≤5 minutosvalidez de la assertion
≈12 horasvalidez del access token

¿Buscas iniciar sesión con el navegador?

El inicio de sesión de usuarios humanos usa la integración OIDC / OAuth2; esta página trata solo de máquina a máquina: scripts, tareas en segundo plano, llamadas entre servicios.

Comparación de los tres métodos de autenticación ​

Del más simple (PAT) al más seguro (JWT Profile), equilibrando seguridad y coste operativo.

MétodoEscenario de usoModelo de permisosValidez del token
Personal Access Token (PAT)Scripts de usuarios individuales, depuración de desarrollo, llamadas temporales de CIHereda todos los permisos de la Service Account a la que perteneceA largo plazo (expiración personalizable, indefinida por defecto)
Service Account + Client CredentialsTareas programadas en segundo plano, llamadas REST entre servicios, cuentas de máquina simplesRequiere asignar el Project Role por separado mediante User Grantaccess token de aproximadamente 1 hora, requiere refresco automático
Service Account + JWT ProfileEscenarios de alta seguridad, servicios de producción, cargas de trabajo de K8sRequiere asignar el Project Role por separado mediante User Grantassertion JWT de 5 minutos como máximo, access token de aproximadamente 12 horas

Recomendaciones de elección

  • Depuración / scripts temporales: el PAT es lo más simple, basta con pegarlo en el header y usarlo.
  • Cuentas de máquina de producción: preferir JWT Profile, la rotación de la clave privada no depende de la distribución de secrets.
  • Servicios internos simples: Client Credentials es la alternativa simplificada a JWT Profile.

I. Personal Access Token (PAT) Listo para usar ​

El PAT es un token listo para usar, se coloca directamente como Bearer token en el header Authorization, sin necesidad de cambiarlo primero por un access token.

1.1 Crear un PAT ​

  1. Inicia sesión en la consola de identity.lurus.cn.
  2. Ve a Users → Service Accounts (si la cuenta pertenece a una Organization, opera dentro de ese contexto).
  3. New para crear una cuenta de servicio, rellena el nombre de usuario (las convenciones de nomenclatura están en la sección tres) y el nombre visible.
  4. Detalles de la cuenta → Personal Access Tokens → New, configura la fecha de expiración según necesites (en blanco significa que nunca expira).
  5. Copia el token: solo se muestra una vez, no se puede volver a ver después de cerrar el cuadro de diálogo.

1.2 Usar el PAT ​

Adjúntalo como Bearer token estándar en el header de cada petición; puede acceder a todas las API de Casdoor que esa Service Account tenga autorizadas, sin necesidad de un audience scope adicional.

bash
# 查询当前账号所在组织
curl -X GET https://identity.lurus.cn/management/v1/orgs/me \
  -H 'Authorization: Bearer <PAT>'
# 调用 v2 API
curl -X GET https://identity.lurus.cn/v2/users/me \
  -H 'Authorization: Bearer <PAT>'

1.3 Escenarios aplicables en Lurus ​

  • Scripts de operaciones: consultar o modificar usuarios en lote mediante la Management API
  • El MCP Server 2l-svc-casdoor-mcp accede a la Management API de Casdoor
  • Operaciones de gestión temporales en el Pipeline de CI/CD; pruebas rápidas de la API de Casdoor en desarrollo local

1.4 Consideraciones de seguridad ​

Normas de seguridad del PAT

  • Solo se muestra una vez; guárdalo de inmediato en una herramienta de gestión de secretos (K8s Secret, Vault) tras crearlo.
  • Nunca lo subas a un repositorio Git. Usa un PAT independiente para cada propósito, no lo compartas.
  • Audita y elimina periódicamente los PAT que ya no se usan; tras una fuga, elimínalo de inmediato en la consola (el atacante puede usarlo hasta que el PAT expire o se elimine).

II. Service Account (cuenta de servicio) ​

Tipo de cuenta que representa una entidad no humana, diseñada específicamente para M2M. Diferencias con un Human User:

AtributoHuman UserService Account
Forma de inicio de sesiónContraseña / Passkey / MFASin inicio de sesión interactivo; solo PAT o intercambio de token
Autenticación multifactorCompatibleNo aplica
Aparece en la sesión OIDCSíNo
Asignación de permisosGrant o Organization RoleVinculada a un Project Role mediante User Grant

2.1 Crear una Service Account ​

En la consola, ve a la Organization objetivo → Users → Service Accounts → New, rellena el nombre de usuario y el nombre visible → Create.

Convención de nomenclatura svc-<service>-<purpose>, ejemplos:

  • svc-lurus-api-platform-client — lurus-api llama a las interfaces internas de platform
  • svc-ci-deploy — dedicada al despliegue de CI/CD
  • svc-casdoor-mcp-admin — para la gestión del Casdoor MCP Server

2.2 Asignar permisos ​

Tras crearla, no tiene permisos por defecto. Ve al Project → User Grants → New correspondiente → selecciona la Service Account objetivo → asigna el Project Role (como admin, viewer).

Principio de privilegio mínimo

Otorga solo los permisos mínimos necesarios para completar la tarea; no des admin para tareas de solo lectura.


III. Client Credentials (la forma más simple de llamada para cuentas de máquina) OAuth2 estándar ​

Tipo de concesión estándar de OAuth 2.0, adecuado para escenarios de cuentas de máquina que no requieren alta seguridad.

3.1 Generar Client Credentials ​

Detalles de la Service Account → Actions → Generate Client Secret → anota el Client ID y el Client Secret (el Secret solo se muestra una vez).

3.2 Obtener un access token ​

Envía una petición de concesión client_credentials al endpoint de token:

bash
curl -X POST https://identity.lurus.cn/oauth/v2/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --user "$CLIENT_ID:$CLIENT_SECRET" \
  --data 'grant_type=client_credentials' \
  --data 'scope=openid profile'
typescript
const params = new URLSearchParams({
  grant_type: 'client_credentials',
  scope: 'openid profile',
})
const credentials = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')
const response = await fetch('https://identity.lurus.cn/oauth/v2/token', {
  method: 'POST',
  headers: {
    'Authorization': `Basic ${credentials}`,
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: params.toString(),
})
const { access_token, expires_in } = await response.json()

La implementación en Go es isomorfa a la de TypeScript: http.NewRequest POST con el body anterior + req.SetBasicAuth(clientID, clientSecret), y parsea access_token.

3.3 Formato de respuesta ​

json
{ "access_token": "MtjHodGy4zxKylDOhg6kW90WeEQs2q...", "token_type": "Bearer", "expires_in": 43199 }
  • expires_in está en segundos, aproximadamente 12 horas. Implementa por tu cuenta el cacheo del token y el refresco automático (se recomienda refrescar 5 minutos antes de expirar).

3.4 Llamar a la API ​

bash
curl -X GET https://identity.lurus.cn/v2/users/me \
  -H "Authorization: Bearer $ACCESS_TOKEN"

IV. JWT Profile (el método de autenticación de máquina más seguro) Recomendado para producción ​

Usa claves asimétricas: la Service Account posee la clave privada y Casdoor guarda la clave pública correspondiente. El cliente firma un JWT de vida corta con la clave privada como client_assertion, y Casdoor emite un access token tras verificar la firma. La clave privada no se transmite por la red, siendo este el método M2M más seguro.

4.1 Generar el par de claves ​

Opción A (recomendada): Detalles de la Service Account → Keys → New (fecha de expiración opcional) → Download para guardar el archivo de clave JSON (solo se puede descargar una vez).

Opción B: genera la clave externamente y sube la clave pública:

bash
openssl genrsa -out privatekey.pem 2048
openssl rsa -in privatekey.pem -pubout -out publickey.pem

Luego sube publickey.pem mediante la User Service API de Casdoor.

4.2 Formato del archivo de clave JSON ​

json
{
  "type": "serviceaccount",
  "keyId": "100509901696068329",
  "key": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----\n",
  "userId": "100507859606888466"
}
CampoSignificado
keyIdCorresponde al kid (Key ID) en el header del JWT
keyClave privada RSA (PEM), usada para firmar el assertion JWT
userIdID de usuario de la Service Account, usado para iss y sub

4.3 Construir el JWT Assertion ​

Header: { "alg": "RS256", "kid": "<keyId>" }

Payload:

json
{ "iss": "<userId>", "sub": "<userId>", "aud": "https://identity.lurus.cn", "iat": 1714000000, "exp": 1714000300 }
ClaimDescripción
issEl userId del archivo JSON
subIgual que iss (representa al solicitante, es decir, la propia aplicación)
audDominio de la instancia de Casdoor: https://identity.lurus.cn
iatMarca de tiempo Unix UTC actual
expExpiración, se recomienda iat + 300 (5 minutos); no más de 1 hora como máximo

Sincronización del reloj

iat no debe ser más de 1 hora anterior a la hora del servidor de Casdoor, de lo contrario la petición de token será rechazada. Asegúrate de que la sincronización NTP de la máquina funcione correctamente.

4.4 Obtener un access token ​

Flujo con curl: primero firma el assertion con una biblioteca de código y luego haz un POST para cambiarlo por el token. Go / Node.js / Python, etc., usan sus respectivas bibliotecas JWT (golang-jwt, jsonwebtoken, PyJWT) para firmar el header/payload de la sección 4.3 con RS256 y enviar el assertion.

bash
# 1. 先用代码库签名 JWT(示意)
ASSERTION="<base64url-header>.<base64url-payload>.<base64url-signature>"
# 2. 请求 token
curl -X POST https://identity.lurus.cn/oauth/v2/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer' \
  --data 'scope=openid profile' \
  --data "assertion=$ASSERTION"
go
import (
    "crypto/rsa"; "crypto/x509"; "encoding/json"; "encoding/pem"
    "fmt"; "net/http"; "net/url"; "os"; "strings"; "time"
    "github.com/golang-jwt/jwt/v5"
)

type keyFile struct {
    KeyID  string `json:"keyId"`
    Key    string `json:"key"`
    UserID string `json:"userId"`
}

func loadPrivateKey(pemStr string) (*rsa.PrivateKey, error) {
    block, _ := pem.Decode([]byte(pemStr))
    if block == nil {
        return nil, fmt.Errorf("failed to decode PEM block")
    }
    return x509.ParsePKCS1PrivateKey(block.Bytes)
}

func GetToken(keyFilePath string) (string, error) {
    data, err := os.ReadFile(keyFilePath)
    if err != nil {
        return "", fmt.Errorf("read key file: %w", err)
    }
    var kf keyFile
    if err := json.Unmarshal(data, &kf); err != nil {
        return "", fmt.Errorf("parse key file: %w", err)
    }
    privateKey, err := loadPrivateKey(kf.Key)
    if err != nil {
        return "", fmt.Errorf("load private key: %w", err)
    }
    now := time.Now()
    claims := jwt.MapClaims{
        "iss": kf.UserID,
        "sub": kf.UserID,
        "aud": jwt.ClaimStrings{"https://identity.lurus.cn"},
        "iat": now.Unix(),
        "exp": now.Add(5 * time.Minute).Unix(),
    }
    token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
    token.Header["kid"] = kf.KeyID
    assertion, err := token.SignedString(privateKey)
    if err != nil {
        return "", fmt.Errorf("sign jwt: %w", err)
    }
    params := url.Values{
        "grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"},
        "scope":      {"openid profile"},
        "assertion":  {assertion},
    }
    req, _ := http.NewRequest("POST",
        "https://identity.lurus.cn/oauth/v2/token",
        strings.NewReader(params.Encode()),
    )
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", fmt.Errorf("token request: %w", err)
    }
    defer resp.Body.Close()
    var result struct {
        AccessToken string `json:"access_token"`
    }
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return "", fmt.Errorf("decode response: %w", err)
    }
    return result.AccessToken, nil
}
python
import json, time, jwt          # pip install PyJWT
import requests

def get_token(key_file_path: str) -> str:
    with open(key_file_path) as f:
        key_data = json.load(f)
    now = int(time.time())
    assertion = jwt.encode(
        {
            "iss": key_data["userId"], "sub": key_data["userId"],
            "aud": "https://identity.lurus.cn", "iat": now, "exp": now + 300,
        },
        key_data["key"], algorithm="RS256",
        headers={"kid": key_data["keyId"]},
    )
    response = requests.post(
        "https://identity.lurus.cn/oauth/v2/token",
        data={
            "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
            "scope": "openid profile", "assertion": assertion,
        },
    )
    response.raise_for_status()
    return response.json()["access_token"]
typescript
import * as fs from 'fs';
import * as jwt from 'jsonwebtoken'; // npm install jsonwebtoken @types/jsonwebtoken

interface KeyFile {
  keyId: string;
  key: string;   // RSA private key PEM
  userId: string;
}

async function getToken(keyFilePath: string): Promise<string> {
  const keyData: KeyFile = JSON.parse(fs.readFileSync(keyFilePath, 'utf-8'));
  const now = Math.floor(Date.now() / 1000);

  const assertion = jwt.sign(
    { iss: keyData.userId, sub: keyData.userId, aud: 'https://identity.lurus.cn', iat: now, exp: now + 300 },
    keyData.key,
    { algorithm: 'RS256', keyid: keyData.keyId }
  );

  const params = new URLSearchParams({
    grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
    scope: 'openid profile',
    assertion,
  });
  const resp = await fetch('https://identity.lurus.cn/oauth/v2/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: params.toString(),
  });
  if (!resp.ok) throw new Error(`token request failed: ${resp.status}`);
  const { access_token } = await resp.json() as { access_token: string };
  return access_token;
}

4.5 Formato de respuesta ​

Igual que 3.3 (access token de aproximadamente 12 horas). Refréscalo de forma proactiva antes de que expire (vuelve a firmar el assertion → vuelve a solicitar el token).


V. Solicitar el Audience correcto ​

Casdoor valida el campo audience (aud) del access token. Para distintas API objetivo, declara el audience correspondiente en el scope.

Acceder a las propias API de Casdoor (Management / Admin / Auth): scope=openid profile urn:casdoor:iam:org:project:id:casdoor:aud. Este scope reservado añade el proyecto de Casdoor al audience del token; la Management API y otras rechazarán los tokens que no contengan este audience.

bash
curl -X POST https://identity.lurus.cn/oauth/v2/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --user "$CLIENT_ID:$CLIENT_SECRET" \
  --data 'grant_type=client_credentials' \
  --data 'scope=openid profile urn:casdoor:iam:org:project:id:casdoor:aud'

Acceder al servicio de recursos de un Project personalizado: scope=openid profile urn:casdoor:iam:org:project:id:<your_project_id>:aud (<your_project_id> se consulta en la página de detalles del Project en la consola).

Errores comunes ​

SíntomaCausaSolución
403 ForbiddenEl audience del token no contiene la API objetivoAñade el scope urn:casdoor:iam:... correspondiente al cambiar el token
401 UnauthorizedEl token ha expirado o la firma es inválidaVerifica la sincronización del reloj y vuelve a obtener el token
invalid_grantEl assertion ha expirado (>5 minutos) o el aud es incorrectoVerifica el exp y el aud del assertion

VI. Gestión de tokens: revocación / rotación / expiración ​

  • PAT: sin expiración automática (a menos que se establezca una fecha de expiración al crearlo). En la lista de Personal Access Tokens de la consola, eliminarlo manualmente equivale a revocarlo. Se recomienda auditar cada trimestre y eliminar los PAT no usados o de personas que han dejado la empresa.
  • Client Credentials: el Secret no expira, y el access token obtenido expira tras aproximadamente 12 horas. Rotar el Secret: vuelve a usar Generate Client Secret en la consola (el secret antiguo deja de ser válido de inmediato). El código de negocio registra expires_in y vuelve a solicitar de forma proactiva antes de que expire.
  • Clave privada de JWT Profile: si la clave JSON se filtra, el atacante puede firmar assertions para obtener tokens. Rotación: añade una Key nueva en la pestaña Keys → actualiza el keyfile que lee el servicio → confirma que la nueva key surte efecto y elimina la Key antigua. Se recomienda montar el keyfile como K8s Secret, sin incluirlo en la imagen:
yaml
apiVersion: v1
kind: Secret
metadata:
  name: idp-service-account-key
  namespace: lurus-system
type: Opaque
stringData:
  key.json: |
    {
      "type": "serviceaccount",
      "keyId": "...",
      "key": "-----BEGIN RSA PRIVATE KEY-----\n...",
      "userId": "..."
    }
yaml
# Deployment 中挂载
volumes:
  - name: idp-key
    secret:
      secretName: idp-service-account-key
containers:
  - name: app
    volumeMounts:
      - name: idp-key
        mountPath: /secrets/casdoor
        readOnly: true
    env:
      - name: OIDC_KEY_FILE
        value: /secrets/casdoor/key.json

VII. Escenarios de autenticación de los servicios internos de Lurus ​

ServicioEscenario de autenticaciónMétodo recomendado
2l-svc-casdoor-mcpLlamar a la Management API de Casdoor para gestionar usuarios/permisosPAT (operaciones) o Service Account + JWT Profile
2b-svc-api (Hub)El backend valida el token OIDC del frontend; llama a las interfaces internas de platformToken OIDC de usuario en el frontend; las llamadas de plataforma usan INTERNAL_API_KEY (modo bearer_internal_key)
2l-svc-platformProporciona API internas (/internal/v1/...), consumidas por Hub y otrosbearer_internal_key (no es de Casdoor, es una convención interna de la plataforma)
Pipeline de CI/CDConsultar/actualizar recursos del clúster durante el despliegueService Account independiente + PAT, una cuenta independiente por cada flujo
Tareas programadas en segundo planoJob independiente, sin contexto de usuarioService Account + Client Credentials o JWT Profile

Acerca de bearer_internal_key

Las API internas de lurus-platform (service: platform-core.lurus-platform.svc:18104) usan una INTERNAL_API_KEY independiente en lugar de un token de Casdoor; esta key se distribuye a los consumidores mediante un K8s Secret. Consulta la sección capabilities de lurus.yaml para más detalles.


VIII. Buenas prácticas de seguridad ​

Requisitos obligatorios en entorno de producción

  1. Nunca subas a Git los PAT, Client Secrets ni archivos de clave privada (incluso en repositorios privados).
  2. Prioriza JWT Profile en producción: la rotación de la clave privada no requiere coordinar la actualización del secret en todos los consumidores, y el límite de seguridad es más claro.
  3. Usa una Service Account independiente por cada tarea de CI/servicio, para facilitar una auditoría precisa y una revocación rápida.
  4. Audita periódicamente: revisa cada trimestre todas las Service Accounts y PAT, y elimina los que ya no se usan.
  5. Privilegio mínimo: otorga solo el Project Role mínimo, no des Owner a nivel de organización (a menos que sea estrictamente necesario).
  6. Validez del assertion JWT: se recomiendan 5 minutos, sin superar 1 hora como máximo.
  7. No registres los tokens en logs: asegúrate de que los logs de la aplicación no impriman el access token en texto plano.

Próximos pasos

Enlaces de referencia ​

Powered by Lurus Technologies