HarmonyOS开发:用户认证Authentication
HarmonyOS开发:用户认证Authentication
📌 核心要点:Authentication Kit让你不用自己搞登录系统,华为账号、手机号、邮箱、第三方账号一站式集成,账号绑定与解绑开箱即用。
背景与动机
你做了一款App,需要用户登录。看起来简单——一个输入框加一个按钮嘛。然后呢?
手机号登录要发验证码,验证码有频率限制、有效期、防刷问题。邮箱登录要发确认邮件,邮件可能进垃圾箱。第三方登录要对接微信、QQ、微博的SDK,每个SDK的接入方式都不一样。账号合并要处理同一个手机号绑定了微信和邮箱的情况。Token管理要处理过期刷新。安全要防暴力破解、防重放攻击……
一个"登录"功能,背后是一个完整的认证系统。你自己搞,至少一个月,而且安全性还不一定过关。
Authentication Kit就是来解决这个问题的。它把华为账号登录、手机号认证、邮箱认证、第三方账号绑定全封装好了,你集成SDK就能用。
但Authentication Kit不是"一个按钮搞定登录"。你得理解认证流程、Token管理、账号绑定的逻辑,不然用户登录了却拿不到数据、换了手机登不上、第三方账号绑定冲突——每个都是线上事故。
核心原理
Authentication Kit的核心流程:用户选择登录方式 → 认证服务验证身份 → 返回Token → 客户端用Token访问资源。
flowchart TD
A[用户点击登录] --> B{选择登录方式}
B -->|华为账号| C[拉起华为账号授权页]
B -->|手机号| D[发送验证码]
B -->|邮箱| E[发送验证链接]
B -->|第三方| F[拉起第三方授权]
C --> G[用户确认授权]
D --> H[用户输入验证码]
E --> I[用户点击验证链接]
F --> J[第三方返回授权码]
G --> K[认证服务验证]
H --> K
I --> K
J --> K
K --> L{验证结果}
L -->|新用户| M[自动创建账号]
L -->|老用户| N[匹配已有账号]
M --> O[返回Token]
N --> O
O --> P[AccessToken: 访问资源]
O --> Q[RefreshToken: 刷新Token]
O --> R[IdToken: 用户身份信息]
P --> S[用Token访问Cloud DB/Storage/Functions]
classDef login fill:#1565C0,color:#fff,stroke:#0D47A1
classDef verify fill:#E65100,color:#fff,stroke:#BF360C
classDef token fill:#2E7D32,color:#fff,stroke:#1B5E20
classDef access fill:#6A1B9A,color:#fff,stroke:#4A148C
class A,B,C,D,E,F,login
class G,H,I,J,K,L,M,N,verify
class O,P,Q,R,token
class S,access
Token体系
Authentication Kit使用三Token体系:
- AccessToken:访问资源的凭证,有效期短(通常1小时),过期后需要刷新
- RefreshToken:刷新AccessToken的凭证,有效期长(通常30天),过期后需要重新登录
- IdToken:包含用户身份信息的JWT,可以解析出用户ID、昵称等
账号绑定
一个用户可以用华为账号登录,也可以用手机号登录。如果两种方式对应的是同一个用户,就需要"账号绑定"。
Authentication Kit支持一个主账号绑定多个认证方式:华为账号 + 手机号 + 邮箱 + 微信。用户用任何一种方式登录,都能访问同一份数据。
代码实战
基础用法:华为账号登录
最简单的登录方式——华为账号一键登录。
// AuthManager.ets
import { authentication } from '@kit.AuthenticationKit';
import { hmsAccount } from '@kit.HmsAccountKit';
export class AuthManager {
private static instance: AuthManager;
private currentUser: authentication.User | null = null;
private authServiceImpl: authentication.AuthService | null = null;
private constructor() {}
static getInstance(): AuthManager {
if (!AuthManager.instance) {
AuthManager.instance = new AuthManager();
}
return AuthManager.instance;
}
/**
* 初始化认证服务
*/
init(): void {
try {
this.authServiceImpl = authentication.getService();
console.info('[Auth] 认证服务初始化完成');
} catch (error) {
console.error(`[Auth] 初始化失败: ${JSON.stringify(error)}`);
}
}
/**
* 华为账号登录
* 一键登录,最简单的接入方式
*/
async signInWithHuaweiId(): Promise<authentication.SignInResult | null> {
if (!this.authServiceImpl) {
console.error('[Auth] 认证服务未初始化');
return null;
}
try {
// 构建登录请求
const signInRequest = new authentication.HuaweiIdAuthProvider()
.signIn();
// 执行登录
const result = await this.authServiceImpl.signIn(signInRequest);
// 保存当前用户信息
this.currentUser = result.getUser();
console.info(`[Auth] 登录成功: uid=${this.currentUser?.getUid()}, ` +
`nickname=${this.currentUser?.getDisplayName()}`);
return result;
} catch (error) {
const authError = error as authentication.AuthError;
switch (authError.code) {
case authentication.AuthErrorCode.SIGN_IN_CANCELLED:
console.info('[Auth] 用户取消登录');
break;
case authentication.AuthErrorCode.SIGN_IN_FAILED:
console.error('[Auth] 登录失败');
break;
default:
console.error(`[Auth] 登录异常: ${JSON.stringify(error)}`);
}
return null;
}
}
/**
* 获取当前登录用户
*/
getCurrentUser(): authentication.User | null {
return this.currentUser;
}
/**
* 判断是否已登录
*/
isSignedIn(): boolean {
return this.currentUser !== null;
}
/**
* 退出登录
*/
async signOut(): Promise<boolean> {
if (!this.authServiceImpl) return false;
try {
await this.authServiceImpl.signOut();
this.currentUser = null;
console.info('[Auth] 退出登录成功');
return true;
} catch (error) {
console.error(`[Auth] 退出登录失败: ${JSON.stringify(error)}`);
return false;
}
}
}
进阶用法:手机号/邮箱认证与账号绑定
华为账号登录最简单,但有些用户没有华为账号。手机号和邮箱认证覆盖面更广。
// PhoneAuth.ets
import { authentication } from '@kit.AuthenticationKit';
export class PhoneAuthService {
private authService: authentication.AuthService;
private verifyId: string = ''; // 验证ID,验证码校验时需要
constructor(authService: authentication.AuthService) {
this.authService = authService;
}
/**
* 发送手机验证码
*/
async sendVerificationCode(
phoneNumber: string,
countryCode: string = '+86'
): Promise<boolean> {
try {
// 请求发送验证码
const result = await this.authService.requestVerifyCode(
countryCode + phoneNumber,
authentication.VerifyCodeAction.SIGN_IN_OR_SIGN_UP
);
// 保存验证ID,后续校验时需要
this.verifyId = result.getVerifyId();
console.info(`[PhoneAuth] 验证码已发送到 ${countryCode}${phoneNumber}`);
return true;
} catch (error) {
const authError = error as authentication.AuthError;
switch (authError.code) {
case authentication.AuthErrorCode.VERIFY_CODE_TOO_FREQUENT:
console.error('[PhoneAuth] 验证码发送太频繁,请稍后再试');
break;
case authentication.AuthErrorCode.PHONE_NUMBER_INVALID:
console.error('[PhoneAuth] 手机号格式不正确');
break;
default:
console.error(`[PhoneAuth] 发送失败: ${JSON.stringify(error)}`);
}
return false;
}
}
/**
* 用手机验证码登录/注册
*/
async signInWithPhone(
phoneNumber: string,
verifyCode: string,
countryCode: string = '+86'
): Promise<authentication.SignInResult | null> {
try {
const provider = new authentication.PhoneAuthProvider();
const credential = provider.credential(
countryCode + phoneNumber,
verifyCode,
this.verifyId
);
const result = await this.authService.signIn(credential);
console.info(`[PhoneAuth] 手机号登录成功: ${phoneNumber}`);
return result;
} catch (error) {
console.error(`[PhoneAuth] 手机号登录失败: ${JSON.stringify(error)}`);
return null;
}
}
/**
* 绑定手机号到当前账号
* 已登录用户可以绑定手机号,之后可以用手机号登录
*/
async bindPhoneNumber(
phoneNumber: string,
verifyCode: string,
countryCode: string = '+86'
): Promise<boolean> {
try {
const provider = new authentication.PhoneAuthProvider();
const credential = provider.credential(
countryCode + phoneNumber,
verifyCode,
this.verifyId
);
await this.authService.link(credential);
console.info(`[PhoneAuth] 手机号绑定成功: ${phoneNumber}`);
return true;
} catch (error) {
const authError = error as authentication.AuthError;
if (authError.code === authentication.AuthErrorCode.LINK_ACCOUNT_CONFLICT) {
// 该手机号已被其他账号绑定
console.error('[PhoneAuth] 该手机号已被其他账号使用');
} else {
console.error(`[PhoneAuth] 绑定失败: ${JSON.stringify(error)}`);
}
return false;
}
}
/**
* 解绑手机号
*/
async unbindPhoneNumber(): Promise<boolean> {
try {
await this.authService.unlink(authentication.ProviderType.PHONE);
console.info('[PhoneAuth] 手机号解绑成功');
return true;
} catch (error) {
console.error(`[PhoneAuth] 解绑失败: ${JSON.stringify(error)}`);
return false;
}
}
}
// 邮箱认证类似,只是验证方式从短信变成邮件
export class EmailAuthService {
private authService: authentication.AuthService;
private verifyId: string = '';
constructor(authService: authentication.AuthService) {
this.authService = authService;
}
/**
* 发送邮箱验证码
*/
async sendEmailVerifyCode(email: string): Promise<boolean> {
try {
const result = await this.authService.requestVerifyCode(
email,
authentication.VerifyCodeAction.SIGN_IN_OR_SIGN_UP
);
this.verifyId = result.getVerifyId();
console.info(`[EmailAuth] 验证码已发送到 ${email}`);
return true;
} catch (error) {
console.error(`[EmailAuth] 发送失败: ${JSON.stringify(error)}`);
return false;
}
}
/**
* 邮箱验证码登录
*/
async signInWithEmail(email: string, verifyCode: string): Promise<authentication.SignInResult | null> {
try {
const provider = new authentication.EmailAuthProvider();
const credential = provider.credential(email, verifyCode, this.verifyId);
const result = await this.authService.signIn(credential);
console.info(`[EmailAuth] 邮箱登录成功: ${email}`);
return result;
} catch (error) {
console.error(`[EmailAuth] 登录失败: ${JSON.stringify(error)}`);
return null;
}
}
/**
* 绑定邮箱
*/
async bindEmail(email: string, verifyCode: string): Promise<boolean> {
try {
const provider = new authentication.EmailAuthProvider();
const credential = provider.credential(email, verifyCode, this.verifyId);
await this.authService.link(credential);
console.info(`[EmailAuth] 邮箱绑定成功: ${email}`);
return true;
} catch (error) {
console.error(`[EmailAuth] 绑定失败: ${JSON.stringify(error)}`);
return false;
}
}
}
完整示例:生产级认证系统
把华为账号、手机号、邮箱、第三方账号统一管理,处理Token刷新和账号冲突。
// ProductionAuthService.ets
import { authentication } from '@kit.AuthenticationKit';
// 认证状态
enum AuthState {
SIGNED_OUT = 'signed_out',
SIGNING_IN = 'signing_in',
SIGNED_IN = 'signed_in',
TOKEN_EXPIRED = 'token_expired'
}
// 用户信息模型
interface UserProfile {
uid: string;
displayName: string;
email: string | null;
phoneNumber: string | null;
photoUrl: string | null;
providerIds: string[]; // 已绑定的认证方式
isAnonymous: boolean;
}
export class ProductionAuthService {
private static instance: ProductionAuthService;
private authService: authentication.AuthService | null = null;
private authState: AuthState = AuthState.SIGNED_OUT;
private userProfile: UserProfile | null = null;
private stateListeners: Array<(state: AuthState) => void> = [];
private constructor() {}
static getInstance(): ProductionAuthService {
if (!ProductionAuthService.instance) {
ProductionAuthService.instance = new ProductionAuthService();
}
return ProductionAuthService.instance;
}
/**
* 初始化
*/
async init(): Promise<void> {
try {
this.authService = authentication.getService();
// 检查是否有已登录的用户(Token持久化)
const currentUser = this.authService.getCurrentUser();
if (currentUser) {
this.userProfile = this.convertUser(currentUser);
this.authState = AuthState.SIGNED_IN;
console.info(`[ProdAuth] 恢复登录状态: ${this.userProfile.uid}`);
}
// 监听Token过期事件
this.authService.addTokenListener((event: authentication.TokenEvent) => {
if (event.type === authentication.TokenEventType.TOKEN_EXPIRED) {
console.warn('[ProdAuth] Token已过期');
this.authState = AuthState.TOKEN_EXPIRED;
this.refreshToken();
}
});
this.notifyStateChange();
} catch (error) {
console.error(`[ProdAuth] 初始化失败: ${JSON.stringify(error)}`);
}
}
/**
* 华为账号登录
*/
async signInWithHuaweiId(): Promise<UserProfile | null> {
return this.executeSignIn(async () => {
const request = new authentication.HuaweiIdAuthProvider().signIn();
return await this.authService!.signIn(request);
});
}
/**
* 手机号登录
*/
async signInWithPhone(phone: string, code: string, verifyId: string): Promise<UserProfile | null> {
return this.executeSignIn(async () => {
const credential = new authentication.PhoneAuthProvider()
.credential(phone, code, verifyId);
return await this.authService!.signIn(credential);
});
}
/**
* 邮箱登录
*/
async signInWithEmail(email: string, code: string, verifyId: string): Promise<UserProfile | null> {
return this.executeSignIn(async () => {
const credential = new authentication.EmailAuthProvider()
.credential(email, code, verifyId);
return await this.authService!.signIn(credential);
});
}
/**
* 匿名登录
* 不需要任何信息,先进入App再引导绑定
*/
async signInAnonymously(): Promise<UserProfile | null> {
return this.executeSignIn(async () => {
return await this.authService!.signInAnonymously();
});
}
/**
* 退出登录
*/
async signOut(): Promise<void> {
if (!this.authService) return;
try {
await this.authService.signOut();
this.userProfile = null;
this.authState = AuthState.SIGNED_OUT;
this.notifyStateChange();
console.info('[ProdAuth] 退出登录成功');
} catch (error) {
console.error(`[ProdAuth] 退出失败: ${JSON.stringify(error)}`);
}
}
/**
* 删除账号
*/
async deleteAccount(): Promise<boolean> {
if (!this.authService) return false;
try {
await this.authService.deleteUser();
this.userProfile = null;
this.authState = AuthState.SIGNED_OUT;
this.notifyStateChange();
console.info('[ProdAuth] 账号已删除');
return true;
} catch (error) {
console.error(`[ProdAuth] 删除账号失败: ${JSON.stringify(error)}`);
return false;
}
}
/**
* 绑定第三方账号
*/
async linkProvider(providerType: authentication.ProviderType, credential: authentication.AuthCredential): Promise<boolean> {
if (!this.authService || !this.userProfile) return false;
try {
await this.authService.link(credential);
// 更新用户信息
const updatedUser = this.authService.getCurrentUser();
if (updatedUser) {
this.userProfile = this.convertUser(updatedUser);
}
console.info(`[ProdAuth] 绑定成功: ${providerType}`);
return true;
} catch (error) {
const authError = error as authentication.AuthError;
if (authError.code === authentication.AuthErrorCode.LINK_ACCOUNT_CONFLICT) {
console.error('[ProdAuth] 该认证方式已被其他账号使用');
// 可以提示用户是否要合并账号
}
return false;
}
}
/**
* 获取当前用户信息
*/
getUserProfile(): UserProfile | null {
return this.userProfile;
}
/**
* 获取认证状态
*/
getAuthState(): AuthState {
return this.authState;
}
/**
* 注册状态变化监听
*/
addStateListener(listener: (state: AuthState) => void): void {
this.stateListeners.push(listener);
}
/**
* 执行登录的通用方法
*/
private async executeSignIn(
signInAction: () => Promise<authentication.SignInResult>
): Promise<UserProfile | null> {
if (!this.authService) return null;
this.authState = AuthState.SIGNING_IN;
this.notifyStateChange();
try {
const result = await signInAction();
const user = result.getUser();
this.userProfile = this.convertUser(user);
this.authState = AuthState.SIGNED_IN;
this.notifyStateChange();
console.info(`[ProdAuth] 登录成功: uid=${this.userProfile.uid}`);
return this.userProfile;
} catch (error) {
this.authState = AuthState.SIGNED_OUT;
this.notifyStateChange();
console.error(`[ProdAuth] 登录失败: ${JSON.stringify(error)}`);
return null;
}
}
/**
* 刷新Token
*/
private async refreshToken(): Promise<void> {
if (!this.authService) return;
try {
await this.authService.refreshToken();
this.authState = AuthState.SIGNED_IN;
this.notifyStateChange();
console.info('[ProdAuth] Token刷新成功');
} catch (error) {
console.error(`[ProdAuth] Token刷新失败: ${JSON.stringify(error)}`);
// Token刷新失败,需要重新登录
this.authState = AuthState.SIGNED_OUT;
this.notifyStateChange();
}
}
/**
* 转换用户对象
*/
private convertUser(user: authentication.User): UserProfile {
return {
uid: user.getUid(),
displayName: user.getDisplayName() || '',
email: user.getEmail() || null,
phoneNumber: user.getPhoneNumber() || null,
photoUrl: user.getPhotoUrl() || null,
providerIds: user.getProviderIds() || [],
isAnonymous: user.isAnonymous()
};
}
/**
* 通知状态变化
*/
private notifyStateChange(): void {
this.stateListeners.forEach(listener => {
try {
listener(this.authState);
} catch (error) {
console.warn(`[ProdAuth] 监听器执行失败: ${error}`);
}
});
}
}
踩坑与注意事项
坑1:验证码的VerifyId必须保存
requestVerifyCode返回的verifyId,在后续credential校验时必须传入。如果你没保存这个ID,验证码就白发了。
更坑的是,verifyId有时效性,通常5分钟。用户磨蹭太久没输入验证码,verifyId过期了,得重新发。
坑2:账号绑定的冲突处理
用户A用华为账号登录,用户B用手机号登录。现在用户A想绑定同一个手机号——冲突了。
Authentication Kit不会自动合并账号,而是返回LINK_ACCOUNT_CONFLICT错误。你需要自己处理:
- 提示用户"该手机号已被其他账号使用"
- 提供账号合并选项(需要用户验证两个账号的身份)
坑3:Token过期后自动刷新可能失败
addTokenListener可以监听Token过期事件,但自动刷新不是100%成功的。比如用户长时间没打开App,RefreshToken也过期了,这时候只能重新登录。
建议:在关键操作前主动检查Token状态,不要等到操作失败了才发现Token过期。
坑4:匿名登录的数据迁移
用户先用匿名登录,产生了一些数据。后来绑定了华为账号,匿名账号升级为正式账号。但如果你在绑定之前没处理好,匿名账号的数据可能丢失。
解决方案:绑定操作会自动把匿名账号的数据迁移到正式账号,前提是你用的是同一个authService实例。
坑5:SHA256指纹必须匹配
华为账号登录要求App的签名指纹和AGC控制台配置的指纹一致。开发阶段用调试签名,上线用发布签名,两个指纹都要在控制台配置。
忘了配调试签名?登录时直接报SIGN_IN_FAILED,而且错误信息不会告诉你是指纹不匹配——你得自己想到。
坑6:手机号格式必须是国际格式
手机号必须带国际区号,如+8613800138000。如果你只传了13800138000,验证码发送失败。PhoneAuthProvider的credential方法也一样。
HarmonyOS 6适配说明
HarmonyOS 6对Authentication Kit做了以下更新:
- 生物认证集成:新增指纹和面部识别登录支持。用户首次用华为账号登录后,后续可以用生物认证快速登录,不需要每次都输密码。
// HarmonyOS 6生物认证登录
const biometricResult = await authService.signInWithBiometric(
authentication.BiometricType.FINGERPRINT
);
-
多设备登录管理:新增设备管理能力,用户可以查看当前账号在哪些设备上登录了,可以远程注销其他设备的登录状态。
-
账号安全等级:新增安全等级评估API,可以根据用户的认证方式(仅华为账号 vs 华为账号+手机号+邮箱)评估账号安全等级,引导用户绑定更多认证方式。
-
自定义认证UI:之前登录页面是系统提供的标准UI,HarmonyOS 6支持自定义登录页面样式,可以和App的品牌风格保持一致。
-
Token安全增强:AccessToken的有效期从1小时缩短到30分钟,RefreshToken增加了设备绑定,不能跨设备使用。
总结
Authentication Kit让你不用自己搞登录系统,但"不用自己搞"不等于"不用理解"。Token管理、账号绑定冲突、验证码有效期、签名指纹——这些坑你都得知道,不然线上出问题你都不知道怎么查。
核心记住三点:
- VerifyId必须保存,丢了验证码就白发了
- 账号绑定冲突要处理,两个不同账号绑同一个手机号,不处理就是事故
- SHA256指纹别忘了配,调试签名和发布签名都要在控制台配置
| 评估维度 | 说明 |
|---|---|
| 学习难度 | ⭐⭐⭐ 华为账号登录简单,手机号/邮箱认证和账号绑定需要理解 |
| 使用频率 | ⭐⭐⭐⭐⭐ 几乎所有应用都需要用户认证 |
| 重要程度 | ⭐⭐⭐⭐⭐ 没有认证,用户数据就没有归属 |
Authentication Kit不是可选项。你自己搞登录系统,安全性过不了关,体验也跟不上——何必呢?
- 点赞
- 收藏
- 关注作者
评论(0)