从零实现 GitCode OAuth 登录 —— 鸿蒙 6 应用实战

第三篇:从零实现 GitCode OAuth 登录 ------ 鸿蒙 6 应用实战

前三篇我们完成了项目搭建和新闻功能,这次实现一个完整的 OAuth 授权码登录流程------对接 GitCode 平台,让用户通过 WebView 登录授权、自动获取令牌、展示个人主页。

复制代码
认证流程全景:
用户点击"GitCode 登录"
    ↓
WebView 打开 GitCode 授权页
    ↓
用户登录 → 授权
    ↓
GitCode 重定向回 app(携带授权码 code)
    ↓
WebView 拦截重定向 → 提取 code
    ↓
POST 请求交换 access_token
    ↓
保存令牌 → 跳转用户主页

一、OAuth 2.0 授权码模式

1.1 什么是 OAuth?

OAuth 2.0 是目前最主流的授权协议,让你可以用第三方账号登录自己的应用,而无需存储用户的密码。

1.2 授权码模式流程

步骤 方向 说明
1️⃣ App → GitCode 打开授权页面,用户登录并授权
2️⃣ GitCode → App 重定向回 App,携带 授权码 code
3️⃣ App → GitCode code + client_secret 交换 access_token
4️⃣ App → GitCode API access_token 获取用户信息
5️⃣ GitCode → App 返回用户资料(头像、用户名、仓库数等)

关键安全机制

  • client_secret 只在后端/设备端使用,永不暴露给浏览器
  • state 参数防止 CSRF 攻击,确保回调是来自你发起的请求
  • refresh_token 用于令牌过期后无感刷新

二、鸿蒙 6 中的技术选型

在移动端实现 OAuth 有几种方式:

方案 原理 优缺点
系统浏览器 + 自定义 Scheme openLink 打开浏览器,通过 URL Scheme 回调 ❌ 配置复杂,需注册 Scheme 到 module.json5
WebView 内嵌 在 App 内嵌浏览器加载授权页,拦截重定向 ✅ 完全可控,不需 Scheme 注册
WebView + JS Bridge 通过注入 JS 监听页面变化 ❌ 过度设计

我们选择 WebView 内嵌方案,原因:

  • 用户无需离开 App,体验更流畅
  • 拦截重定向提取 code 简单可靠
  • 不依赖外部 Scheme 注册,减少配置

三、项目结构

text 复制代码
entry/src/main/ets/
├── models/
│   └── OAuthModels.ets        # OAuthToken、GitCodeUser 接口
├── service/
│   └── OAuthService.ets       # 授权 URL 构建、令牌交换、用户信息获取
├── pages/
│   ├── Index.ets              # 主页(新增登录按钮,检查登录状态)
│   ├── OAuthLogin.ets         # WebView 授权页面
│   └── UserProfile.ets        # 用户主页(展示资料、退出登录)

四、数据模型:OAuthModels.ets

定义两个核心数据结构和状态枚举:

typescript 复制代码
// OAuth 令牌响应
export interface OAuthToken {
  access_token: string;
  expires_in: number;       // 过期时间(秒),GitCode 为 1296000(15天)
  refresh_token: string;
  scope: string;
  created_at: string;
}

// GitCode 用户信息
export interface GitCodeUser {
  id: number;
  login: string;          // 用户名
  name: string;           // 显示名称
  avatar_url: string;     // 头像 URL
  email: string;
  bio: string;            // 个人简介
  followers: number;
  following: number;
  public_repos: number;   // 公开仓库数
  created_at: string;
  company?: string;
  blog?: string;
}

五、OAuth 服务层:OAuthService.ets

这是核心业务逻辑,封装了完整流程。

5.1 配置信息

typescript 复制代码
const GITCODE_CLIENT_ID = '73863f25ecaa42bdac201d376c6264e9';
const GITCODE_CLIENT_SECRET = '3ca4fc2140014916b4430f854bcf8ecf';
const GITCODE_AUTH_URL = 'https://gitcode.com/oauth/authorize';
const GITCODE_TOKEN_URL = 'https://gitcode.com/oauth/token';
const GITCODE_API_BASE = 'https://api.gitcode.com/api/v5';
const REDIRECT_URI = 'https://haronyos.app/oauth/callback';

注意client_secret 存储在设备端代码中。生产环境应通过后端代理请求,避免硬编码。

5.2 构建授权 URL

typescript 复制代码
export function buildAuthorizeUrl(): { url: string; state: string } {
  const state = generateState();  // 16 位随机字符串,防 CSRF
  const url = `${GITCODE_AUTH_URL}?client_id=${CLIENT_ID}` +
    `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
    `&response_type=code` +
    `&scope=${encodeURIComponent(SCOPE)}` +
    `&state=${state}`;
  return { url, state };
}

参数说明

参数 说明
client_id 73863f25... 应用注册时获取的客户端 ID
redirect_uri https://haronyos.app/oauth/callback 授权后回调地址,在 WebView 中拦截
response_type code 授权码模式固定值
scope all_user all_projects ... 请求的权限范围
state 随机 16 位字符串 CSRF 防御令牌

5.3 令牌交换(核心)

typescript 复制代码
export async function exchangeCodeForToken(code: string): Promise<OAuthToken | null> {
  const url = `${GITCODE_TOKEN_URL}?grant_type=authorization_code` +
    `&code=${encodeURIComponent(code)}` +
    `&client_id=${GITCODE_CLIENT_ID}`;

  const resp = await http.createHttp().request(url, {
    method: http.RequestMethod.POST,
    extraData: `client_secret=${encodeURIComponent(GITCODE_CLIENT_SECRET)}`,
    header: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'Accept': 'application/json',
    },
  });

  const result = resp.result as Record<string, Object>;
  // 提取 access_token, refresh_token, expires_in...
}

请求细节

  • grant_type=authorization_code --- 固定值,表明使用授权码模式
  • client_secret 放在请求体中(form-data),而非 URL 查询参数
  • 返回的 access_token 有效期 15 天 ,过期后可凭 refresh_token 续期

5.4 获取用户信息

typescript 复制代码
export async function fetchCurrentUser(accessToken: string): Promise<GitCodeUser | null> {
  const resp = await http.createHttp().request(`${API_BASE}/user`, {
    method: http.RequestMethod.GET,
    header: {
      'Authorization': `Bearer ${accessToken}`,  // 标准 Bearer Token 认证
      'Accept': 'application/json',
    },
  });
  // 解析返回的 JSON 为 GitCodeUser
}

5.5 刷新令牌

typescript 复制代码
export async function refreshAccessToken(refreshToken: string): Promise<OAuthToken | null> {
  const url = `${GITCODE_TOKEN_URL}?grant_type=refresh_token` +
    `&refresh_token=${encodeURIComponent(refreshToken)}`;
  // 返回包含新 access_token 的 OAuthToken
}

5.6 令牌持久化存储

使用 AppStorage 实现跨页面共享和持久化:

typescript 复制代码
export function saveTokens(token: string, refreshToken: string): void {
  AppStorage.set<string>('gitcode_access_token', token);
  AppStorage.set<string>('gitcode_refresh_token', refreshToken);
}

export function loadTokens(): { token: string; refreshToken: string } | null {
  const token = AppStorage.get<string>('gitcode_access_token');
  const refreshToken = AppStorage.get<string>('gitcode_refresh_token');
  if (token && refreshToken) return { token, refreshToken };
  return null;
}

六、OAuth 登录页面:OAuthLogin.ets

这是最核心的页面,使用 @kit.ArkWebWeb 组件。

6.1 页面初始化

typescript 复制代码
import { webview } from '@kit.ArkWeb';

@Entry
@Component
struct OAuthLogin {
  private controller: webview.WebviewController = new webview.WebviewController();
  private expectedState: string = '';
  private readonly REDIRECT_PREFIX = 'https://haronyos.app/oauth/callback';

  aboutToAppear(): void {
    const { url, state } = buildAuthorizeUrl();
    this.expectedState = state;
    setTimeout(() => {
      this.controller.loadUrl(url);
    }, 100);
  }
}

6.2 拦截重定向(关键逻辑)

typescript 复制代码
onLoadIntercept(event: { data: WebResourceRequest }): boolean {
  const reqUrl = event.data.getRequestUrl();
  if (reqUrl && reqUrl.startsWith(this.REDIRECT_PREFIX)) {
    this.handleRedirect(reqUrl);
    return true;  // 返回 true 阻止 WebView 继续加载
  }
  return false;
}

async handleRedirect(redirectUrl: string): Promise<void> {
  // 1. 解析 URL 中的 code 和 state
  const code = extractParam(redirectUrl, 'code');
  const state = extractParam(redirectUrl, 'state');

  // 2. 校验 state 防止 CSRF
  if (state !== this.expectedState) {
    this.errorMsg = '安全校验失败';
    return;
  }

  // 3. 用 code 交换令牌
  const token = await exchangeCodeForToken(code);

  // 4. 保存令牌并跳转
  saveTokens(token.access_token, token.refresh_token);
  router.replaceUrl({ url: 'pages/UserProfile' });
}

为什么用 onLoadIntercept 而不是 onPageBegin

因为 onLoadIntercept加载之前 触发,返回 true 可以阻止 WebView 加载重定向 URL(这个 URL 在浏览器中根本打不开)。而 onPageBegin 是加载之后才触发,页面已经白屏了。

6.3 WebView 组件声明

typescript 复制代码
build() {
  Column() {
    // 顶部导航栏
    Row() { /* 返回按钮 + 标题 */ }

    // 加载指示器
    if (this.isLoading) {
      LoadingProgress()
    }

    // 错误提示
    if (this.errorMsg) {
      Text(this.errorMsg)
      Button('重试')
    }

    // WebView
    Web({ src: 'about:blank', controller: this.controller })
      .onLoadIntercept((event) => this.onLoadIntercept(event))
      .javaScriptAccess(true)
      .domStorageAccess(true)
  }
}

七、用户主页:UserProfile.ets

登录成功后展示用户信息,支持退出登录。

7.1 加载用户信息

typescript 复制代码
async loadUserInfo(): Promise<void> {
  const tokens = loadTokens();
  if (!tokens) {
    this.errorMsg = '未登录';
    return;
  }
  this.user = await fetchCurrentUser(tokens.token);
}

7.2 个人信息卡片

复制代码
┌─────────────────────────┐
│        ● (头像)          │
│   坚果的博客 (name)      │
│    @NutPi (login)        │
│  AtomCode 作者... (bio)  │
├─────────────────────────┤
│  📦 42   👥 128  ➡️ 56 │
│  仓库    粉丝    关注    │
├─────────────────────────┤
│  📧 email@example.com    │
│  🏢 AtomGit              │
│  🌐 blog.nutpi.net       │
│  📅 2024-04-20           │
├─────────────────────────┤
│    [  退出登录  ] 🔴      │
└─────────────────────────┘

7.3 退出登录

typescript 复制代码
handleLogout(): void {
  clearTokens();               // 清空 AppStorage
  router.replaceUrl({ url: 'pages/Index' });
}

八、首页按钮与登录状态

Index.ets 中的登录按钮会根据 AppStorage 中的令牌自动切换状态:

状态 按钮文本 按钮颜色 点击行为
未登录 🔑 GitCode 登录 黑色 跳转 OAuthLogin 页
已登录 🔑 GitCode 登录 绿色 跳转 UserProfile 页
typescript 复制代码
aboutToAppear(): void {
  const token = AppStorage.get<string>('gitcode_access_token');
  this.hasLoggedIn = token !== undefined && token.length > 0;
}

九、路由与权限

9.1 路由注册

json 复制代码
{
  "src": [
    "pages/Index",
    "pages/News",
    "pages/OAuthLogin",       // ← 新增
    "pages/UserProfile"       // ← 新增
  ]
}

9.2 网络权限

OAuth 流程全部通过 HTTP API 完成,需要 INTERNET 权限(已在第一篇中添加):

json 复制代码
"requestPermissions": [
  { "name": "ohos.permission.INTERNET" }
]

十、完整数据流(时序图)

复制代码
Index.ets                         OAuthLogin.ets                    GitCode                        OAuthService
   │                                    │                              │                               │
   │── 点击"GitCode 登录"               │                              │                               │
   │──→ router.pushUrl() ─────────→     │                              │                               │
   │                                    │── buildAuthorizeUrl() ───→   │                               │
   │                                    │←── { url, state } ────────  │                               │
   │                                    │                              │                               │
   │                                    │── WebView.loadUrl(url) ──→   │                               │
   │                                    │                              │── 用户登录 + 授权              │
   │                                    │                              │── 重定向到 redirect_uri       │
   │                                    │←── { redirect_uri + code } ─│                               │
   │                                    │                              │                               │
   │                                    │── onLoadIntercept() 拦截     │                               │
   │                                    │── 校验 state                 │                               │
   │                                    │── exchangeCodeForToken() ──→  │──→ POST /oauth/token         │
   │                                    │←── OAuthToken ─────────────  │←── { access_token, ... }      │
   │                                    │                              │                               │
   │                                    │── saveTokens()               │                               │
   │                                    │── router.replaceUrl()        │                               │
   │                                    │                              │                               │
   │                              UserProfile.ets                     │                               │
   │                                    │── fetchCurrentUser() ──────  │──→ GET /api/v5/user           │
   │                                    │←── GitCodeUser ────────────  │←── { id, name, login, ... }   │
   │                                    │                              │                               │
   │                                    │── 渲染用户主页               │                               │

十一、总结

这次实现了一个完整的 OAuth 授权码流程,覆盖了:

功能 实现 难度
授权 URL 构建 拼接参数 + CSRF state
WebView 拦截回调 onLoadIntercept 拦截 redirect URI ⭐⭐
令牌交换 POST 请求 + client_secret ⭐⭐
用户信息获取 Bearer Token 认证
令牌持久化 AppStorage 存储
四态 UI(加载/成功/错误/未登录) @State + if/else ⭐⭐
退出登录 清空令牌 + 跳转首页

安全要点

  • state 参数防 CSRF 攻击
  • client_secret 仅在设备端 POST 请求中使用
  • ✅ 令牌不存储在明文文件中,使用 AppStorage(内存级安全)
  • onLoadIntercept 拦截,不暴露 redirect URI 到浏览器
bash 复制代码
# 一句话用 AtomCode 实现:
atomcode "给鸿蒙 6 应用实现 GitCode OAuth 登录,WebView 加载授权页,拦截回调交换令牌"
相关推荐
Niliuershangba3 天前
ChestnutCMS 栗子内容管理系统:从入门到模板开发实战
java·git·开源·gitlab·github·开源软件·gitcode
本地化文档8 天前
rust-style-guide-l10n
rust·github·gitcode
不做无法实现的梦~13 天前
Git 新手到团队协作与 GitHub/GitCode 指南
git·github·gitcode
laoli_coding15 天前
如何将GitCode仓库的提交同步到 GitHub
github·gitcode
. . . . .16 天前
Claude Code 插件市场开发及注意事项
人工智能·gitcode
残歌16 天前
【无标题】
gitcode
weixin_7042660522 天前
IDEA 整合 Git 并上传代码到 CSDN GitCode 超详细教程
git·intellij-idea·gitcode
云渊未归0623 天前
Python获取GitCode项目信息
python·数据分析·开源·网络爬虫·gitcode
本地化文档1 个月前
rust-nomicon-l10n
rust·github·gitcode