LMU AI 계정으로 로그인
웹사이트나 앱에 "LMU AI 계정으로 로그인"을 추가하세요. 사용자를 승인 페이지로 리디렉션하고, 콜백에서 state를 검증한 뒤, 서버에서 client_secret으로 ticket을 사용자 신원 정보로 교환합니다. 흐름, 엔드포인트, 사용자 필드, 보안 요구 사항, 샘플 코드, 그리고 AI 코딩 어시스턴트에 바로 전달할 수 있는 프롬프트를 다룹니다.
"LMU AI 계정으로 로그인"을 사용하면 다른 웹사이트와 앱(커뮤니티, 크리에이티브 도구, 내부 프로젝트 등)에서 사용자가 LMU AI 계정으로 로그인할 수 있습니다. 사용자는 LMU AI에서 "승인 및 로그인"을 한 번 클릭하기만 하면 됩니다. 계정이 없는 사용자는 그 자리에서 LMU AI에 가입할 수 있으며, 가입 후 자동으로 사이트로 돌아옵니다.
사이트는 사용자의 신원 정보만 받습니다. 사용자의 API 키나 잔액은 절대 받지 않습니다.
사용 대상
웹사이트나 앱에 서버가 필요합니다. 마지막 단계에서 client_secret을 사용해 ticket을 교환하는데, client_secret은 브라우저, 미니 프로그램, 모바일 프론트엔드 코드에 절대 나타나서는 안 됩니다. 프론트엔드만 있는 프로젝트는 자체 백엔드를 통해 교환해야 합니다.
1. 동작 방식
이 흐름은 간소화된 OAuth 인가 코드 흐름입니다. 브라우저 주소창에는 60초 동안만 유효한 일회용 ticket만 전달되며, 서버가 client_secret으로 이를 사용자 신원 정보와 교환합니다.
2. 시작하기 전에: 접근 신청
다음 항목을 LMU AI에 등록하세요.
| 항목 | 설명 |
|---|---|
| 프로젝트 이름 | 승인 페이지에 표시됩니다. 예: "立刻图 liketu.ai" |
| 콜백 URL | HTTPS를 사용해야 하며 전체 URL이 정확히 일치해야 합니다. 예: https://your-app.example.com/sso/callback. 테스트 환경용으로 두 번째 URL을 등록할 수 있으며, 로컬 개발용으로 http://localhost 또는 http://127.0.0.1로 시작하는 URL도 등록할 수 있습니다 |
| 로고(선택) | 승인 페이지에 표시됩니다 |
| 이메일 필요 여부 | 기본적으로 이메일은 반환되지 않습니다. 필요한 경우 이유를 설명해 주세요. 승인 후에만 반환됩니다 |
등록이 완료되면 다음을 받습니다.
client_id: 프로젝트를 식별하며 공개되어도 됩니다.client_secret: 프로젝트 시크릿입니다. 한 번만 표시되므로 서버 측 환경 변수나 시크릿 관리자에 보관하세요.
신청 방법
LMU AI 내부 프로젝트는 관리자가 관리 콘솔에서 생성합니다. 외부 파트너는 LMU AI 고객 지원에 문의하여 신청하세요.
3. Base URL
https://api.lmuai.com계정은 LMU AI 중국 본토 사이트와 국제 사이트 모두에서 사용할 수 있으며, 동일한 사용자는 두 사이트에서 같은 사용자 ID를 가집니다. 이 페이지의 예시는 현재 사이트 주소를 사용합니다.
승인과 교환은 같은 사이트에서
ticket은 이를 발급한 사이트에 저장됩니다. 사용자를 승인하도록 보낸 사이트와 같은 사이트에서 ticket을 교환해야 하며, 그렇지 않으면 교환이 실패합니다(401).
4. 1단계: 승인 페이지로 리디렉션
GET /sso/authorize
사용자가 "LMU AI 계정으로 로그인"을 클릭하면, 서버는 무작위 state를 생성해 쿠키에 저장하고 브라우저를 승인 페이지로 302 리디렉션합니다.
https://api.lmuai.com/sso/authorize?client_id=YOUR_CLIENT_ID&state=RANDOM_STATE&redirect_uri=https%3A%2F%2Fyour-app.example.com%2Fsso%2Fcallback| 매개변수 | 필수 | 설명 |
|---|---|---|
client_id | 예 | 신청 시 받은 프로젝트 식별자 |
state | 예 | 로그인할 때마다 새로 생성하는 무작위 문자열로, A-Z a-z 0-9 _ -만 포함하며 길이는 16–128자입니다. 32바이트 난수를 base64url로 인코딩한 값(43자)을 권장합니다 |
redirect_uri | 아니요 | 콜백 URL로, 등록된 URL 중 하나와 정확히 일치해야 합니다. 생략하면 첫 번째로 등록된 URL이 사용됩니다 |
LMU AI 측에서 사용자는 다음을 진행합니다.
- 로그인되어 있지 않으면 먼저 로그인하거나, 계정이 없으면 그 자리에서 가입한 후 자동으로 승인 페이지로 돌아옵니다.
- 승인 페이지에서 프로젝트 이름과 "로그인에만 사용됩니다. API 키와 잔액은 공유되지 않습니다."라는 안내를 확인합니다.
- "승인 및 로그인" 또는 "취소"를 클릭합니다.
승인 페이지를 iframe에 포함하지 마세요
승인 페이지는 포함할 수 없습니다(X-Frame-Options: DENY). 전체 페이지를 리디렉션하거나 새 창에서 여세요.
5. 2단계: 콜백 처리
사용자가 승인하면 브라우저가 콜백 URL로 리디렉션됩니다.
https://your-app.example.com/sso/callback?ticket=TICKET&state=RANDOM_STATE사용자가 취소한 경우:
https://your-app.example.com/sso/callback?error=access_denied&state=RANDOM_STATE콜백 핸들러는 다음을 순서대로 수행해야 합니다.
- state 확인: URL의
state가 쿠키에 저장된 값과 정확히 일치해야 하며, 확인 직후 쿠키를 삭제합니다. 일치하지 않으면 로그인을 거부합니다. - 취소 처리:
error=access_denied인 경우 로그인 페이지로 돌아가 "승인을 취소했습니다."라고 안내합니다. - 신원 정보 교환: 서버에서
ticket으로 3단계의 교환 엔드포인트를 호출합니다. - 계정 생성 또는 로그인: 사용자의
id로 로컬 계정을 찾거나 생성하고, 사이트의 세션을 설정합니다. - 원래 페이지로 복귀: 오픈 리디렉션으로 악용되지 않도록 자체 사이트의 상대 경로만 허용합니다.
6. 3단계: 서버에서 ticket을 사용자 신원 정보로 교환
POST /api/v1/sso/exchange
이 단계는 반드시 서버에서만 호출해야 합니다.
curl https://api.lmuai.com/api/v1/sso/exchange \
-H "Content-Type: application/json" \
-d '{
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"ticket": "TICKET"
}'| 매개변수 | 필수 | 설명 |
|---|---|---|
client_id | 예 | 프로젝트 식별자 |
client_secret | 예 | 프로젝트 시크릿 |
ticket | 예 | 콜백 URL에서 받은 ticket |
성공 응답(HTTP 200):
{
"code": 0,
"message": "success",
"data": {
"user": {
"id": 10086,
"username": "alice",
"email": "alice@example.com",
"avatar": "https://example.com/avatar.png"
}
}
}실패 시:
| HTTP | 원인 | 조치 |
|---|---|---|
401 | client_id 또는 client_secret이 잘못됨, ticket이 만료되었거나 이미 사용되었거나 이 프로젝트에 발급되지 않음 | 같은 ticket으로 재시도하지 말고, 사용자가 로그인을 다시 시작하도록 합니다 |
404 | 이 사이트에서 LMU AI 계정 로그인이 활성화되지 않음 | Base URL이 신청한 사이트인지 확인합니다 |
429 | 요청이 너무 많음 | 잠시 후 재시도합니다 |
5xx | 서비스를 일시적으로 사용할 수 없음 | 잠시 후 사용자가 로그인을 다시 시작하도록 합니다 |
탐색 공격을 방지하기 위해 자격 증명이나 ticket 관련 오류는 어떤 항목이 잘못되었는지 알려 주지 않고 모두 동일한 401을 반환합니다.
ticket 규칙: 60초 동안 유효하고, 한 번만 교환할 수 있으며, 발급된 프로젝트만, 그리고 발급한 사이트에서만 교환할 수 있습니다.
7. 사용자 필드
| 필드 | 유형 | 설명 |
|---|---|---|
id | integer | 고유 사용자 ID입니다. 절대 변경되지 않으며 중국 본토 사이트와 국제 사이트에서 동일합니다. 자체 서비스에서 계정을 연결하는 키로 사용하세요 |
username | string | 사용자 이름입니다. 사용자가 변경할 수 있으며 비어 있을 수 있습니다. 비어 있으면 "LMU AI 사용자 {id}"와 같은 기본값을 표시하세요 |
email | string | 이메일 수신이 승인된 프로젝트에만 반환되며, 그렇지 않으면 비어 있습니다. 사용자가 변경할 수 있으며 인증된 이메일임이 보장되지 않습니다 |
avatar | string | 아바타 URL이며 비어 있을 수 있습니다 |
이메일로 계정을 자동 병합하지 마세요
사이트에 이미 이메일로 가입한 계정이 있는 경우, LMU AI가 반환한 이메일이 일치한다는 이유만으로 사용자를 기존 계정에 바로 로그인시키지 마세요. 먼저 해당 이메일로 인증 코드를 보내고, 인증된 후에만 계정을 연결하세요. 해당 이메일을 사용하는 계정이 없다면 새 계정을 생성하면 됩니다.
8. 보안 요구 사항
출시 전에 각 항목을 확인하세요.
-
client_secret은 서버에만 존재해야 합니다. 프론트엔드 코드, 로그, URL, 코드 저장소에 절대 포함하지 마세요. - 로그인할 때마다 새
state를 생성하고, 5분 후 만료되는 httpOnly, Secure, SameSite=Lax 쿠키에 저장하세요. 콜백에서 일치하면 즉시 삭제하세요. - 콜백 URL은 HTTPS를 사용하고 등록된 URL과 정확히 일치해야 합니다.
-
ticket을 받으면 즉시 교환하세요. 재시도, 캐시, 로그 기록을 하지 마세요. - 계정은
id로 연결하고, 이메일로 기존 계정을 자동 병합하지 마세요. - 로그인 후에는 자체 사이트의 상대 경로로만 리디렉션하세요.
- 교환 호출에 타임아웃을 설정하세요(10초 권장). 실패 시 빈 화면을 남기지 말고 이유와 함께 로그인 페이지로 돌아가세요.
- 사이트에서 로그아웃하면 자체 세션만 삭제되며, 사용자의 LMU AI 로그인 상태에는 영향을 주지 않습니다.
9. 샘플 코드
두 샘플 모두 /sso/login(승인 페이지로 리디렉션)과 /sso/callback(state 확인 및 신원 정보 교환)을 구현합니다. 사용하는 환경 변수:
| 변수 | 예시 |
|---|---|
LMU_SSO_BASE_URL | https://api.lmuai.com |
LMU_SSO_CLIENT_ID | 발급받은 client_id |
LMU_SSO_CLIENT_SECRET | 발급받은 client_secret |
LMU_SSO_REDIRECT_URI | https://your-app.example.com/sso/callback |
import express from "express";
import cookieParser from "cookie-parser";
import crypto from "node:crypto";
const BASE = process.env.LMU_SSO_BASE_URL;
const CLIENT_ID = process.env.LMU_SSO_CLIENT_ID;
const CLIENT_SECRET = process.env.LMU_SSO_CLIENT_SECRET;
const REDIRECT_URI = process.env.LMU_SSO_REDIRECT_URI;
const STATE_COOKIE = "lmu_sso_state";
const app = express();
app.use(cookieParser());
// Step 1: generate state and redirect to the LMU AI authorization page
app.get("/sso/login", (req, res) => {
const state = crypto.randomBytes(32).toString("base64url");
res.cookie(STATE_COOKIE, state, {
httpOnly: true, secure: true, sameSite: "lax", maxAge: 5 * 60 * 1000, path: "/",
});
const url = new URL("/sso/authorize", BASE);
url.searchParams.set("client_id", CLIENT_ID);
url.searchParams.set("state", state);
url.searchParams.set("redirect_uri", REDIRECT_URI);
res.redirect(url.toString());
});
// Steps 2 and 3: check state and exchange for identity on the server
app.get("/sso/callback", async (req, res) => {
const { ticket, state, error } = req.query;
const expected = req.cookies[STATE_COOKIE];
res.clearCookie(STATE_COOKIE, { path: "/" });
const ok = typeof state === "string" && typeof expected === "string" &&
Buffer.byteLength(state) === Buffer.byteLength(expected) &&
crypto.timingSafeEqual(Buffer.from(state), Buffer.from(expected));
if (!ok) return res.redirect("/login?error=invalid_state");
if (error === "access_denied") return res.redirect("/login?error=cancelled");
if (typeof ticket !== "string" || !ticket) return res.redirect("/login?error=missing_ticket");
let user;
try {
const resp = await fetch(new URL("/api/v1/sso/exchange", BASE), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ client_id: CLIENT_ID, client_secret: CLIENT_SECRET, ticket }),
signal: AbortSignal.timeout(10_000),
});
if (!resp.ok) return res.redirect("/login?error=exchange_failed");
user = (await resp.json()).data.user; // { id, username, email, avatar }
} catch {
return res.redirect("/login?error=exchange_failed");
}
// Replace with your own logic: find or create the local account by user.id, then set your session
// const localUser = await upsertUserByLmuId(user);
// await createSession(res, localUser);
res.redirect("/");
});
app.listen(3000);import hmac
import os
import secrets
from urllib.parse import urlencode
import httpx
from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse
BASE = os.environ["LMU_SSO_BASE_URL"]
CLIENT_ID = os.environ["LMU_SSO_CLIENT_ID"]
CLIENT_SECRET = os.environ["LMU_SSO_CLIENT_SECRET"]
REDIRECT_URI = os.environ["LMU_SSO_REDIRECT_URI"]
STATE_COOKIE = "lmu_sso_state"
app = FastAPI()
def back_to_login(reason: str) -> RedirectResponse:
resp = RedirectResponse(f"/login?error={reason}", status_code=302)
resp.delete_cookie(STATE_COOKIE)
return resp
# Step 1: generate state and redirect to the LMU AI authorization page
@app.get("/sso/login")
def sso_login():
state = secrets.token_urlsafe(32)
query = urlencode({"client_id": CLIENT_ID, "state": state, "redirect_uri": REDIRECT_URI})
resp = RedirectResponse(f"{BASE}/sso/authorize?{query}", status_code=302)
resp.set_cookie(STATE_COOKIE, state, max_age=300, httponly=True, secure=True, samesite="lax")
return resp
# Steps 2 and 3: check state and exchange for identity on the server
@app.get("/sso/callback")
async def sso_callback(request: Request, ticket: str | None = None,
state: str | None = None, error: str | None = None):
expected = request.cookies.get(STATE_COOKIE)
if not state or not expected or not hmac.compare_digest(state.encode(), expected.encode()):
return back_to_login("invalid_state")
if error == "access_denied":
return back_to_login("cancelled")
if not ticket:
return back_to_login("missing_ticket")
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.post(f"{BASE}/api/v1/sso/exchange", json={
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"ticket": ticket,
})
resp.raise_for_status()
user = resp.json()["data"]["user"] # {"id", "username", "email", "avatar"}
except (httpx.HTTPError, ValueError, KeyError, TypeError):
return back_to_login("exchange_failed")
# Replace with your own logic: find or create the local account by user["id"], then set your session
# local_user = upsert_user_by_lmu_id(user)
done = RedirectResponse("/", status_code=302)
done.delete_cookie(STATE_COOKIE)
# create_session(done, local_user)
return done10. AI 코딩 어시스턴트에게 연동 맡기기
아래 프롬프트를 프로젝트와 함께 Claude Code, Codex, Cursor 같은 AI 코딩 어시스턴트에 전달하고, 꺾쇠괄호 안의 부분을 프로젝트에 맞게 바꾸세요.
Integrate "Sign in with LMU AI" into this project. Implement it exactly as specified below; do not change the endpoints, parameters, or security requirements.
[Configuration] Add 4 server-side environment variables. They must never appear in front-end code, logs, or the code repository:
- LMU_SSO_BASE_URL=https://api.lmuai.com
- LMU_SSO_CLIENT_ID=<your client_id>
- LMU_SSO_CLIENT_SECRET=<your client_secret>
- LMU_SSO_REDIRECT_URI=https://<this project's domain>/sso/callback (must exactly match the callback URL registered with LMU AI)
[Step 1] Add GET /sso/login:
- Generate state as the base64url encoding of 32 random bytes;
- Store it in the cookie lmu_sso_state: httpOnly, Secure, SameSite=Lax, expires in 5 minutes;
- 302-redirect to {LMU_SSO_BASE_URL}/sso/authorize?client_id=…&state=…&redirect_uri=… (URL-encode the parameters).
[Step 2] Add GET /sso/callback:
- The state URL parameter must exactly match the cookie lmu_sso_state (use a constant-time comparison); delete the cookie right after comparing. If they do not match, return to the login page with "The sign-in link has expired. Please sign in again.";
- If error=access_denied, return to the login page with "You cancelled authorization.";
- If ticket is missing, return to the login page with the reason.
[Step 3] Exchange the ticket for the identity on the server:
- POST {LMU_SSO_BASE_URL}/api/v1/sso/exchange with Content-Type: application/json and body {"client_id","client_secret","ticket"}, timeout 10 seconds;
- On success (HTTP 200) the user is in data.user of the response JSON: {id, username, email, avatar};
- On failure (401/404/429/5xx or timeout), do not retry the same ticket; return to the login page with "Sign-in failed. Please try again.";
- A ticket can be used only once and expires in 60 seconds; do not cache or log it.
[Accounts]
- Use data.user.id as the unique key for linking LMU AI accounts (add a field such as lmu_user_id with a unique index): update the username and avatar if it exists, otherwise create a new account;
- Do not sign the user into an existing local account just because the email matches: send a verification code to that email first and link only after it is verified;
- username may be empty — show "LMU AI user {id}" in that case; email may be empty.
- After creating or signing in the account, set this project's session and redirect to the page the user came from (relative paths on this site only).
[UI] Add a "Sign in with LMU AI" button to the login page that goes to /sso/login.
[Tests] Add automated tests covering at least:
1. Normal sign-in: account created on first sign-in, profile updated on the next sign-in;
2. Missing, mismatched, or tampered state is rejected;
3. error=access_denied returns to the login page with a cancellation notice;
4. A 401 from exchange returns to the login page without retrying;
5. Reusing the same ticket fails;
6. An existing account with the same email can only be linked after email verification.
When done, list the changed files, the new environment variables, and the test results.11. 출시 전 점검
- 이미 LMU AI에 로그인한 사용자: "LMU AI 계정으로 로그인" 클릭 → 승인 페이지에서 "승인 및 로그인" 클릭 → 사이트로 돌아와 로그인된 상태.
- LMU AI 계정이 없는 신규 사용자: 로그인 클릭 → LMU AI에 가입 → 승인 → 사이트로 돌아와 로그인된 상태.
- 승인 페이지에서 "취소" 클릭: 로그인 페이지로 돌아오며 승인이 취소되었다는 안내가 표시됨.
- 콜백 페이지 새로고침, 같은 ticket 재사용, 변조되었거나 누락된 state: 모두 실패하고 이유와 함께 로그인 페이지로 돌아감.
- 같은 LMU AI 사용자가 두 번째로 로그인하면 같은 로컬 계정으로 들어가고 프로필이 업데이트됨.
-
client_secret이 프론트엔드 코드, 브라우저 네트워크 요청, 로그에 절대 나타나지 않음.
마지막 업데이트:
LMU AI API 키를 발급받고 Claude와 Codex를 바로 사용하세요
무료 가입, 유연한 요금제. 하나의 API 키로 Claude Code, Codex CLI, Cursor, VS Code 확장, OpenCode, Cherry Studio 등 AI 도구에 연결됩니다.
가입하기