前言
本文章是研究外置登录系统 Logto 插件的几项简单测试,包括:登录系统替换 ,鉴权系统的体验 说明 ,登录权限角色 体验,第三方登录支持 ,以及外置导入权限角色并添加相应人员的体验,使用我个人网站做为演示主题,并通过更改个人网站的登录系统的方式展示 Logto 对于网站登录系统的体验。对于本演示,使用的网站可以通过CSDN网址获得架构简单解释
针对网页前后端架构与部署监控的设计_mcp server实战:flask+react+node.js轻量级ai服务骨架-CSDN博客
本文章讲默认读者并没有了解过演示网站的架构设计,将在文章中顺带简要解释网页架构
1. Logto 个人体验定义解释
Logto是一个 Web 应用的身份认证与访问管理 (IAM, Identity and Access Management),使用的 OIDC/OAuth2.0 标准协议,可以对接多端、多应用 ,统一解决应用登录、注册、用户权限管控 这类通用需求。这个外部第三方身份认证平台不用和网页直接相连,自带云鉴权,云登录页面渲染 ,不用手写登录鉴权系统(演示网站改造前采用的是手写的登录系统),此外,Logto还自带我认为完善的用户监测系统,可以看到用户流量等基本信息。
Logto 提供2种部署方式,Logto Cloud(云托管登录) 和 Self-host(自托管登录),本文使用的是云托管登录,即账号密码信息管理存储于Logto云托管服务器。自托管登录则将数据库等数据存放于本地服务器,这里是体验,但是管理流程和云托管服务器大致不差。
自写登录系统一般来讲需要完成 架构分离设计:token设计,数据库设计,前后端网关鉴权设计等 ,而 Logto 提供了一系列 API 统一将功能分离,使用时调用比如 getIdTokenClaims 获得当前鉴权用户的所有信息(ID token声明)比如权限,账号token和账户名字等,同时自带密码哈希加密等通用功能,使用起来非常方便
个人体验而言,对于架构的解耦,无痛覆盖老的登录系统(这个登录系统得是使用接口或者信号分发机制一类的系统,耦合不能太严重,不然替换会麻烦)会非常有帮助
2. 网站替换 Logto 流程
这里是本人替换网页的流程和操作,如下:
1. 云端创建服务器租户
由于是云端存储,首先需要在Logto上申请单独的数据区域,就是申请租户(我的理解是申请了一块服务器数据存储区),用来存储当前这个Logto云端账户管理的登录数据,以及登录页面对应的回调等信息
2. 创建对应网站框架的 Logto 应用
本演示网页使用的是 React 前端架构,所以我是 单页应用 SPA 创建,创建 Logto 应用的主要用处就是给对应的网页框架提供云回调以及适配的接口服务,对于我演示的 React框架,需要填入云网页登录的时候登录完成后回去的网址,以及登出后回去的网址
我这里选择localhost:3000/callback 作为登入回调地址,localhost:3000/MainPage 作为登出回调地址

3. 网站本体改造
Logto 网页完成云流程后,需要对即将接入Logto的网站进行改造,首先使用对应网页框架的Logto包的安装,我这里是
npm install @logto/react
1. 全局包裹 LogtoProvider
安装完后,需要对网站的顶层包裹住 LogtoProvider ,使全局子路由对应的脚本都能使用 useLogto 这些 Logto 专属组件,注意,LogtoProvider 要包裹住所有使用 Logto 对应接口的组件,我这里演示直接包裹住了index.js, 这样所有脚本都能使用Logto 的专属组件
javascript
// index.js
import React, { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';
import { LogtoProvider, UserScope } from '@logto/react';
/**
* Login configuration for Logto
* 注意:这里的endpoint和appId需要替换为你自己的Logto应用的配置
* 应用配置从你的云 Logto 控制台复制获取
*/
const loginConfig = {
endpoint: 'https://~~~~~~~~~~~/',
appId: '~~~~~~~~~~~~',
scopes: [
UserScope.OpenId,
UserScope.Profile,
UserScope.OfflineAccess,
UserScope.Roles
]
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<LogtoProvider config={loginConfig}>
<StrictMode>
<BrowserRouter future={{
v7_startTransition: true,
v7_relativeSplatPath: true
}}>
<App />
</BrowserRouter>
</StrictMode>
</LogtoProvider>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
LogtoProvider 的包裹需要传递控制参数用于身份鉴别,请从自己的云 Logto 控制台获取
2. 编写对应云 Logto 上注册的回调网址
整体包裹完成后,云端的登录界面需要返回到云端上定义的回调网址进行下一步操作,刚刚上文定义的网址为localhost:3000/callback,所以我们需要给 React 的路由添加 /callback,方便处理
我这里路由处理直接是回跳回主页面,如下
javascript
// LoginCallback.js
import { useHandleSignInCallback } from '@logto/react';
import { useNavigate } from 'react-router-dom';
/**
* 给 Logto 提供的 signIn callback 页面
* 这个页面会在用户登录成功后被 Logto 重定向到
* 你可以在这里处理登录回调逻辑,比如获取用户信息、存储 token 等
* 然后再重定向到你的应用的主页面或者其他页面
*/
export default function Callback() {
const navigate = useNavigate();
// 专门用来处理登录回调的hook
const { isLoading } = useHandleSignInCallback(() => {
navigate('/MainPage');
});
if (isLoading) {
return <div> 重定向到 Logto 登录组件 </div>;
}
return <div>跳转到账号中心</div>;
}
在这个脚本中 useHandleSignInCallback便是登录完成后,传回的回调,可以在这里写后续操作
3. 编写 Logto 登录页面跳转以及退出逻辑
上文已经有了回调处理,接下来是要解决如何跳转的问题,Logto提供了 signIn, signOut 两个方法,参数均为跳转后回调的地址,这里请填写云中你写入的地址 ,否则会被认为你定义的回调地址没有权限。方法跳转完成后,比如signIn跳转回来后,回调会传给你传入回调地址的 useHandleSignInCallback回调处理,如果回调不存在于你写的对应网址的脚本中,那就会报错,说找不到这个回调,这里我单开了一个 useAuth.js 脚本用来处理Logto自带登录页面的跳转
javascript
// useAuth.js
import { useLogto } from '@logto/react';
import { useState, useEffect } from 'react';
export function useAuth() {
const { isAuthenticated, getIdTokenClaims, signIn, signOut } = useLogto();
const [claims, setClaims] = useState(null);
const [loading, setLoading] = useState(true);
/** 获得用户声明,方便后续操作使用 */
useEffect(() => {
const loadClaims = async () => {
if (isAuthenticated) {
const data = await getIdTokenClaims();
setClaims(data);
} else {
setClaims(null);
}
setLoading(false);
};
loadClaims();
}, [isAuthenticated, getIdTokenClaims]);
return {
loginState: isAuthenticated,
accountId: claims?.sub,
user: claims,
isLoading: loading,
// 这里是登录和登出方法,传入回调地址,注意和你在 Logto 控制台配置的回调地址一致
doLogin: () => signIn('http://localhost:3000/callback'),
doLogout: () => signOut('http://localhost:3000/MainPage'),
};
}
4. 后续改造
完成上述内容后,需要往你框架的主页面中,或者是其他你用到登录的页面中,使用按钮或者组件调用其中的doLogin 和 doLogout 方法,然后不出意外的话,你应该会跳转到登录页面,如图

进行登录过后,你的网页会保存对应的 ID Token 声明 ,你可以在云控制台设置这个token声明能够保存多久,还有一些token等的分发操作。到这里为止,就完成了基本的网页登录系统转接,如果你的网站本身的框架是存token或者从某些登录系统获得用户信息或者权限的话,你可以花费一点时间把你网站的登录系统的接口全部对接Logto的,这样子你就完成了部署
4. 第三方登录支持
Logto 支持大量的第三方登录服务,包括邮箱短信还有微信QQ等第三方登录,如图

Logto 默认(至少我用的时候)使用的是邮箱验证器,我后续添加了GitHub验证器作为测试,我这里演示也同样使用GitHub验证器演示
1. Github
在使用验证器的时候,首先对云Logto控制台配置,找到Logto控制台中的连接器(Connectors),然后选择社交连接器,创建GitHub连接器。
创建GitHub连接器,首先会拿到一个回调地址,用于登录完成后回调到Logot页面,这里是连接器自动生成的,直接获取即可
随后去 GitHub 创建 OAuth Apps 应用后,拿回 Logto 控制台需要的比如 ClientID 和 ClientSecret 后填入,在Logto控制台登录与账户处开启对应的社交媒体连接器即可

5. 登录权限角色与外置导入权限角色
1. 权限脚本
Logto 内的权限角色可以通过控制台添加 以及脚本使用接口添加新角色类型(全局角色)
直接添加在控制台的 Roles 角色处使用控制台按钮直接添加即可
而使用脚本添加则需要多操作一些,外置脚本添加脚本不属于网页本体,属于 M2M(Client Credentials)流程,由独立脚本直接操作。
对应角色调用的API为: 你的租户地址/api/roles
javascript
`${logtoEndpoint}api/roles`
但是添加前需要 M2M 权限操作控制台,所以在Logto控制台首先手动添加 M2M 角色

当然,默认会自带一个 Logto Management API access Logto 管理 API 访问权限 角色,你可以直接使用这个角色分配给你的 M2M 应用,M2M应用也需要在 Logto 控制台手动创建,这样就可以提供 M2M 管理员 token给脚本使用,然后才能增加角色类型
创建 M2M 应用后(在控制台的Applications创建),会获得 client_id / client_secret , 这个需要提供给操作脚本
完成上述操作后,需要写操作脚本,操作脚本先要通过 client_id / client_secret 获得 **管理员 access_token,**然后在后续的请求操作中作为 HTTP 请求头,用来作为身份认证
以下的脚本是获得 管理员 access_token 后执行角色统一添加操作的示例脚本
javascript
/**
* createRoles.mjs
* 这里是用数组来批量创建角色的示例代码
* 你可以换成使用其他格式化文件来统一创建
* 然后后续可以给导入的名单中按照你的规则分配角色
* /
import fetch from 'node-fetch';
/**
* 这里保存你从Logto控制台获取的 M2M Client ID 和 Client Secret
*/
const logtoEndpoint = 'https://~~~~~~/';
const m2mClientId = '~~~~~~~~';
const m2mClientSecret = '~~~~~~~~';
// 你要导入的角色列表,可以根据需要修改成 CSV 或 JSON 文件读取
const roleList = [
{
name: 'admin',
description: '系统管理员',
type: 'User'
},
{
name: 'editor',
description: '内容编辑',
type: 'User'
},
{
name: 'guest',
description: '访客用户',
type: 'User'
}
];
// ========================================
/** 拿到 Logto M2M Token */
async function getAccessToken() {
// 通过 M2M Client 获取 Token
const res = await fetch(`${logtoEndpoint}oidc/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
/**
* grant_type: 'client_credentials' 表示使用客户端凭证模式获取Token
* client_id: m2mClientId 是你在Logto控制台创建的M2M应用的Client ID
* client_secret: m2mClientSecret 是你在Logto控制台创建的M2M应用的Client Secret
* resource: `${logtoEndpoint}api` 表示请求的资源是Logto的API
* scope: 'all' 表示请求的权限范围,这里请求所有权限
*/
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: m2mClientId,
client_secret: m2mClientSecret,
resource: `${logtoEndpoint}api`,
scope: 'all'
})
});
// 等待Token返回
const json = await res.json();
console.log(`获取到的 Token: ${JSON.stringify(json)}`);
if (!res.ok) {
throw new Error(`Token 获取失败: ${json.error_description || json.error}`);
}
return json.access_token;
}
/** 获得所有已经存在的角色 */
async function getExistingRoles(accessToken) {
const res = await fetch(`${logtoEndpoint}api/roles`, {
method: 'GET',
headers: {
// 附带上获取到的 M2M Token 作为 Bearer Token
'Authorization': `Bearer ${accessToken}`
}
});
const list = await res.json();
return list
}
/** 单角色创建 */
async function createRole(accessToken, roleData) {
const res = await fetch(`${logtoEndpoint}api/roles`, {
method: 'POST',
headers: {
// 附带上获取到的 M2M Token 作为 Bearer Token
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(roleData)
});
// 422 = 资源已存在兜底,不报错
if (res.status === 422) {
console.log(`角色[${roleData.name}]服务端提示已存在,忽略`);
return res.json();
}
if (!res.ok) {
const json = await res.json();
throw new Error(`Failed to create role: ${json.error_description || json.error}`);
}
console.log(`角色[${roleData.name}]创建成功`);
return res.json();
}
/** 批量创建角色 */
async function main() {
const token = await getAccessToken();
const existingRoles = await getExistingRoles(token);
console.log(`已经存在的角色: ${JSON.stringify(existingRoles)}`);
for (const role of roleList) {
let isExist = false;
// 原生for循环遍历 existingRoles 这个角色数组
for(let i = 0; i < existingRoles.length; i++){
const item = existingRoles[i];
if(item.name === role.name){
isExist = true;
break; // 跳出内层循环
}
}
if (isExist) {
console.log(`角色[${role.name}]已经存在,跳过创建。`);
continue;
}
await createRole(token, role);
console.log(`角色[${role.name}]创建成功。`);
}
}
main().catch(err => console.error('Error:', err));
查询角色使用 GET {endpoint}/api/roles
创建角色使用 POST {endpoint}/api/roles
创建角色格式示例
javascript
{
"name": "admin",
"description": "系统管理员",
"type": "User"
}
示例脚本实际操作结果如下

网页上可以看到自动创建了角色身份

2. 用户脚本
基本原理同上,调用 Logto 接口,上传对应 json 数据
创建单个用户接口: POST: /api/users
批量创建用户: POST /api/users/batch
创建用户后可以批量分配角色身份,不过请在身份存在后再分配,用户创建的格式示例如下
javascript
// 单个导入接口
const userList = [
{
primaryEmail: "admin@test.com",
password: "Abc123456!",
name: "系统管理员",
roleNames: ["admin"]
},
{
primaryEmail: "editor@test.com",
password: "Abc123456!",
name: "内容编辑",
roleNames: ["editor"]
}
];
// 批量导入接口
//`/batch` 接口**不支持传入明文密码**,适合无密码导入;带预置密码只能循环调用单用户 POST 接口
const payload = {
users: [
{ primaryEmail: "u1@test.com", name: "用户1" },
{ primaryEmail: "u2@test.com", name: "用户2" }
]
};
用户创建表可以通过格式化其他文件格式比如 csv格式 到这种形式来批量导入
6. 补充(本地部署想法)
Logto 官方原生支持 Docker 容器化部署(压缩包大小600 ~ 900MB左右),是自托管(Self-host)方案的一个部署选择,文档完整、镜像官方维护;它是无状态容器设计 (意味着大部分服务器都可以处理它的信息),数据和容器是解耦分开的,数据保存在 PostgreSQL(当然数据库也要自己装),对容器本身的操作一般不会影响数据,个人感觉还是对本地部署挺友好的