HarmonyOS 应用开发进阶案例(五):实现应用免密登录

48 阅读5分钟

本案例使用ArkTS语言实现一个简单的免登录过程,展示基本的cookie管理操作。主要包含以下功能:

  1. 获取指定url对应的cookie的值。
  2. 设置cookie。
  3. 清除所有cookie。
  4. 免登录访问账户中心。

本案例使用的是在线网页,需添加网络权限:ohos.permission.INTERNET。

1. 案例效果截图

2. 原理说明

本应用旨在说明Web组件中cookie的管理操作。结合应用弹框和界面跳转两种方式进行讲解。

  • 应用弹框验证cookie读写

若用户已通过“设置cookie”完成cookie写操作,点击应用首页的“获取cookie”按钮,则应用弹窗中会带有“info=测试cookie写入”的信息。若用户未进行写操作,则弹窗信中无对应信息。

  • 界面跳转验证cookie存储

若用户在应用首页完成登录操作,则点击“验证cookies”按钮,界面会跳转至“关于”界面;若用户此前未完成登录操作,则会跳转至登录界面。这里借助真实的登录过程,体现了Web组件自动存储登录后的会话cookie,并在整个应用中生效的能力。

3. 代码结构

├──entry/src/main/ets               // 代码区
│  ├──common                         
│  │  ├──constants                   
│  │  │  └──CommonConstant.ets      // 常量类
│  │  └──utils                       
│  │     ├──DialogUtil.ets          // 弹框工具类 
│  │     └──Logger.ets              // 日志工具类
│  ├──entryability                    
│  │  └──EntryAbility.ets            // 程序入口类
│  ├──pages                          
│  │  ├──Verify.ets                 // 免登录验证界面
│  │  └──WebIndex.ets               // 应用首页
│  └──view                                
│     └──LinkButton.ets             // 链接按钮组件
└──entry/src/main/resources         // 应用资源目录

4. 公共文件与资源

4.1. 本案例涉及到的常量类和工具类

  1. 通用常量类
// entry/src/main/ets/common/utils/CommonConstants.ets
export class CommonConstants {
  static readonly USER_CENTER_URL = 'https://id1.cloud.huawei.com/AMW/portal/userCenter/index.html?' +
    'service=http://developer.huawei.com/consumer/cn/#/accountCenter/userCenter'
  static readonly USER_ABOUT_URL = 'https://id1.cloud.huawei.com/AMW/portal/userCenter/index.html?' +
    'service=http://developer.huawei.com/consumer/cn/#/accountCenter/about'
  static readonly NAVIGATOR_SIZE = '20fp'
  static readonly TITLE_SIZE = '18fp'
  static readonly BUTTON_SIZE = '14fp'
  static readonly DIVIDER_MARGIN = 8
  static readonly DIVIDER_HEIGHT = '2.2%'
  static readonly PAGE_VERIFY = 'pages/Verify'
  static readonly PAGE_INDEX = 'pages/WebIndex'
  static readonly FULL_WIDTH = '100%'
  static readonly FULL_HEIGHT = '100%'
  static readonly BACK_WIDTH = '6.7%'
  static readonly BACK_HEIGHT = '3.1%'
  static readonly WEB_HEIGHT = '70%'
  static readonly WEB_WIDTH = '90%'
  static readonly PAGE_TITLE_HEIGHT = '3.1%'
  static readonly PAGE_TITLE_WIDTH = '25.6%'
  static readonly NAVIGATOR_MARGIN_TOP = '1.7%'
  static readonly NAVIGATOR_MARGIN_LEFT = '10%'
  static readonly FONT_WEIGHT_DEEPER = 500
  static readonly APP_TITLE_MARGIN_TOP = '15vp'
  static readonly APP_TITLE_MARGIN_LEFT = '26vp'
  static readonly WEB_MARGIN_BOTTOM = '30vp'
  static readonly SUB_LENGTH: number = 300
}

export enum CookieOperation {
  GET_COOKIE = '读取cookie',
  SET_COOKIE = '设置cookie',
  DELETE_COOKIE = '删除cookie',
  VERIFY_COOKIE = '验证cookie'
}

2. 日志类

// entry/src/main/ets/common/utils/Logger.ets
import { hilog } from '@kit.PerformanceAnalysisKit'

class Logger {
  private domain: number
  private prefix: string
  private format: string = '%{public}s, %{public}s'

  constructor(prefix: string = 'MyApp', domain: number = 0xFF00) {
    this.prefix = prefix
    this.domain = domain
  }

  debug(...args: string[]): void {
    hilog.debug(this.domain, this.prefix, this.format, args)
  }

  info(...args: string[]): void {
    hilog.info(this.domain, this.prefix, this.format, args)
  }

  warn(...args: string[]): void {
    hilog.warn(this.domain, this.prefix, this.format, args)
  }

  error(...args: string[]): void {
    hilog.error(this.domain, this.prefix, this.format, args)
  }
}

export default new Logger('WebCookie', 0xFF00)

3. 弹框函数

// entry/src/main/ets/common/utils/DialogUtil.ets
import Logger from '../utils/Logger'
import { CommonConstants } from '../constants/CommonConstant'

export function showDialog(message: ResourceStr): void {
  let newMessage = message.toString()
  if (newMessage.length > CommonConstants.SUB_LENGTH) {
    message = newMessage.substring(0, CommonConstants.SUB_LENGTH)
  }
  AlertDialog.show(
    {
      title: $r('app.string.dialog_message'),
      message: message,
      confirm: {
        value: $r('app.string.button_confirm'),
        action: () => {
          Logger.info('Button-clicking callback')
        }
      },
      cancel: () => {
        Logger.info('Closed callbacks')
      }
    }
  )
}

4.2. 本案例涉及到的资源文件

  1. string.json
// entry/src/main/resources/base/element/string.json
{
  "string": [
    {
      "name": "module_desc",
      "value": "模块描述"
    },
    {
      "name": "EntryAbility_desc",
      "value": "description"
    },
    {
      "name": "EntryAbility_label",
      "value": "Web组件"
    },
    {
      "name": "button_confirm",
      "value": "确定"
    },
    {
      "name": "dialog_message",
      "value": "信息"
    },
    {
      "name": "navigator_name",
      "value": "Web组件"
    },
    {
      "name": "title_name",
      "value": "Web组件内"
    },
    {
      "name": "write_success",
      "value": "写入cookie成功"
    },
    {
      "name": "write_fail",
      "value": "写入cookie失败"
    },
    {
      "name": "delete_success",
      "value": "删除cookie成功"
    },
    {
      "name": "test_content",
      "value": "info=测试cookie写入"
    },
    {
      "name": "load_error",
      "value": "加载页面失败:请检查网络连接。"
    },
    {
      "name": "reason",
      "value": "Used to initiate network data requests."
    }
  ]
}

2. float.json

// entry/src/main/resources/base/element/float.json
{
  "float": [
    {
      "name": "page_title_margin_top",
      "value": "48vp"
    },
    {
      "name": "page_title_margin_bottom",
      "value": "28vp"
    }
  ]
}

3. color.json

// entry/src/main/resources/base/element/color.json
{
  "color": [
    {
      "name": "start_window_background",
      "value": "#FFFFFF"
    },
    {
      "name": "link_blue",
      "value": "#007DFF"
    },
    {
      "name": "navigator_black",
      "value": "#000000"
    },
    {
      "name": "title_black",
      "value": "#182431"
    },
    {
      "name": "page_background_grey",
      "value": "#F1F3F5"
    }
  ]
}

其他资源请到源码中获取。

5. Cookie读写操作

5.1. 首页

首次打开应用时,首页的Web组件内呈现的是登录界面。用户完成登录操作后,会跳转至账号中心界面。首页包含“读取cookie”、“设置cookie”和“删除cookie”等多个按钮,可对cookie进行读取、设置和删除等操作。

// entry/src/main/ets/pages/WebIndex.ets
import { webview } from '@kit.ArkWeb'
import { CommonConstants, CookieOperation } from '../common/constants/CommonConstant'
import { LinkButton } from '../view/LinkButton'
import { showDialog } from '../common/utils/DialogUtil'

@Entry
@ComponentV2
struct WebIndex {
  // 创建 WebView 控制器,用于管理 Web 视图
  controller: webview.WebviewController = new webview.WebviewController()

  build() {
    Column() {
      // 应用导航标题
      Text($r('app.string.navigator_name'))
        .fontSize(CommonConstants.NAVIGATOR_SIZE)
        .fontWeight(CommonConstants.FONT_WEIGHT_DEEPER)
        .fontColor($r('app.color.navigator_black'))
        .width(CommonConstants.FULL_WIDTH)
        .margin({
          top: CommonConstants.APP_TITLE_MARGIN_TOP,
          left: CommonConstants.APP_TITLE_MARGIN_LEFT 
        })

      // 页面主标题
      Text($r('app.string.title_name'))
        .fontSize(CommonConstants.TITLE_SIZE)
        .fontWeight(CommonConstants.FONT_WEIGHT_DEEPER)
        .fontColor($r('app.color.title_black'))
        .textAlign(TextAlign.Center)
        .width(CommonConstants.WEB_WIDTH)
        .height(CommonConstants.PAGE_TITLE_HEIGHT)
        .margin({
          top: $r('app.float.page_title_margin_top'),
          bottom: $r('app.float.page_title_margin_bottom')
        })

      // 加载 WebView,嵌入用户中心页面
      Web({
        src: CommonConstants.USER_CENTER_URL, // WebView 加载的 URL
        controller: this.controller // 绑定 WebView 控制器
      })
        .domStorageAccess(true) // 允许访问 DOM 存储
        .javaScriptAccess(true) // 允许执行 JavaScript
        .height(CommonConstants.WEB_HEIGHT)
        .width(CommonConstants.WEB_WIDTH)
        .margin({ bottom: CommonConstants.WEB_MARGIN_BOTTOM }) 
        .onErrorReceive((event) => { // 监听 WebView 加载错误事件
          if (event?.request.isMainFrame()) { // 检查是否为主框架加载失败
            let message = $r('app.string.load_error') // 加载失败提示信息
            showDialog(message) // 显示错误对话框
          }
        })

      // 操作按钮区域(获取、设置、删除和验证 Cookie)
      Row() {
        LinkButton({ buttonType: CookieOperation.GET_COOKIE, isNeedDivider: true }) // 获取 Cookie 按钮
        LinkButton({ buttonType: CookieOperation.SET_COOKIE, isNeedDivider: true }) // 设置 Cookie 按钮
        LinkButton({ buttonType: CookieOperation.DELETE_COOKIE, isNeedDivider: true }) // 删除 Cookie 按钮
      }
      .justifyContent(FlexAlign.SpaceBetween)
      .width(CommonConstants.WEB_WIDTH)
    }
    .backgroundColor($r('app.color.page_background_grey'))
    .width(CommonConstants.FULL_WIDTH)
    .height(CommonConstants.FULL_HEIGHT)
  }
}

关键代码说明:

  • LinkButton({...})自定义组件,用于实现获取、设置、删除和验证 Cookie。

5.2. LinkButton组件

// entry/src/main/ets/view/LinkButton.ets
import { webview } from '@kit.ArkWeb'
import { router } from '@kit.ArkUI'
import { showDialog } from '../common/utils/DialogUtil'
import { CommonConstants, CookieOperation } from '../common/constants/CommonConstant'

/**
 * LinkButton 组件
 * 用于执行 Cookie 操作(获取、设置、删除)或导航到验证页面
 */
@ComponentV2
export struct LinkButton {
  @Param buttonType?: string = '' // 按钮类型(表示不同的 Cookie 操作)
  @Param isNeedDivider?: boolean = true // 是否需要分隔线(用于 UI 视觉分割)

  /**
   * 组件构建方法
   */
  build() {
    Row() { // 水平排列按钮和分隔线
      // 按钮文本
      Text(this.buttonType)
        .fontColor($r('app.color.link_blue')) // 设置字体颜色
        .fontSize(CommonConstants.BUTTON_SIZE) // 设置字体大小
        .textAlign(TextAlign.Center) // 文本居中
        .fontWeight(FontWeight.Normal) // 设置字体粗细
        .onClick(() => {
          this.operationMethod() // 绑定点击事件,执行相应操作
        })

      // 是否需要分隔线
      if (this.isNeedDivider) {
        Divider()
          .vertical(true) // 竖直分隔线
          .margin(CommonConstants.DIVIDER_MARGIN) // 设置分隔线边距
          .height(CommonConstants.DIVIDER_HEIGHT) // 设置分隔线高度
      }
    }
  }

  /**
   * 执行不同类型的 Cookie 操作或页面跳转
   */
  operationMethod(): void {
    try {
      if (this.buttonType === CookieOperation.GET_COOKIE) {
        // 获取指定 URL 的 Cookie
        let originCookie = webview.WebCookieManager.fetchCookieSync(CommonConstants.USER_CENTER_URL)
        showDialog(originCookie) // 显示获取到的 Cookie
      } else if (this.buttonType === CookieOperation.SET_COOKIE) {
        // 设置 Cookie
        webview.WebCookieManager.configCookieSync(CommonConstants.USER_ABOUT_URL, 'info=测试cookie写入')
        showDialog($r('app.string.write_success')) // 显示成功写入提示
      } else if (this.buttonType === CookieOperation.DELETE_COOKIE) {
        // 清除所有 Cookie
        webview.WebCookieManager.clearAllCookiesSync()
        let deleteMessage = $r('app.string.delete_success')
        showDialog(deleteMessage) // 显示删除成功提示
      } else {
        // 其他情况,执行页面跳转到 Cookie 验证页面
        // TODO
      }
    } catch (error) {
      // 捕获异常并显示错误信息
      showDialog('Operation failed: ' + JSON.stringify(error))
    }
  }
}

关键代码说明:

  • WebCookieManager.fetchCookieSync:获取指定 URL 的 Cookie。
  • WebCookieManager.configCookieSync:设置 Cookie
  • WebCookieManager.clearAllCookiesSync:清除所有 Cookie

6. Cookie存储验证

一个应用中的所有Web组件共享一个WebCookie,因此一个应用中Web组件存储的cookie信息,也是可以共享的。当用户在应用内完成登录操作时,Web组件会自动存储登录的会话cookie。应用内其他页面可共享当前会话cookie信息,免去多余的登录操作。

6.1. 首页入口

// entry/src/main/ets/pages/WebIndex.ets
// ...

@Entry
@ComponentV2
struct WebIndex {
  // ...
  
  build() {
    Column() {
      // ...

      // 操作按钮区域
      Row() {
        // ...
        
        // 验证 Cookie 按钮
        LinkButton({ buttonType: CookieOperation.VERIFY_COOKIE, isNeedDivider: false })
      }
      // ...
    }
    // ...
  }
}

6.2. LinkButton组件路由跳转

// entry/src/main/ets/view/LinkButton.ets
// ...

@ComponentV2
export struct LinkButton {
  // ...
  
  /**
   * 执行不同类型的 Cookie 操作或页面跳转
   */
  operationMethod(): void {
    try {
      if (this.buttonType === CookieOperation.GET_COOKIE) {
        // ...
      } else {
        // 其他情况,执行页面跳转到 Cookie 验证页面
        router.pushUrl({
          url: CommonConstants.PAGE_VERIFY
        })
      }
    }
    // ...
  }
}

6.3. cookie验证

// entry/src/main/ets/pages/Verify.ets
import { webview } from '@kit.ArkWeb'
import { showDialog } from '../common/utils/DialogUtil'
import { CommonConstants } from '../common/constants/CommonConstant'

/**
 * Verify 组件
 * 用于加载 `USER_ABOUT_URL` 页面,并在页面加载完成时尝试获取 Cookie。
 */
@Entry
@ComponentV2
struct Verify {
  @Local navPathStack: NavPathStack = new NavPathStack() // 导航路径管理
  fileAccess: boolean = true // 是否允许文件访问
  controller: webview.WebviewController = new webview.WebviewController() // WebView 控制器
  isRedirect: boolean = false // 标记是否已经进行跳转,防止重复操作

  /**
   * 页面显示时触发
   * 重新设置 `isRedirect` 为 false,确保每次进入页面时可以正确获取 Cookie
   */
  onPageShow(): void {
    this.isRedirect = false
  }

  /**
   * 组件构建方法
   */
  build() {
    Navigation(this.navPathStack) {
      Column() {
        Text($r('app.string.title_name'))
          .fontSize(CommonConstants.TITLE_SIZE)
          .fontWeight(CommonConstants.FONT_WEIGHT_DEEPER)
          .fontColor($r('app.color.title_black'))
          .width(CommonConstants.PAGE_TITLE_WIDTH) 
          .height(CommonConstants.PAGE_TITLE_HEIGHT)
          .margin({
            top: $r('app.float.page_title_margin_top'),
            bottom: $r('app.float.page_title_margin_bottom')
          })

        // WebView 组件
        Web({
          src: CommonConstants.USER_ABOUT_URL, // 目标网页 URL
          controller: this.controller // 绑定 WebView 控制器
        })
          .height(CommonConstants.WEB_HEIGHT)
          .width(CommonConstants.WEB_WIDTH)
          .fileAccess(this.fileAccess) // 允许文件访问
          .javaScriptAccess(true) // 允许 JavaScript 交互
          .onPageEnd(() => {
            try {
              // 在页面加载完成后尝试获取 Cookie
              let originCookie = webview.WebCookieManager.fetchCookieSync(CommonConstants.USER_ABOUT_URL)
              if (originCookie === '' || this.isRedirect) {
                return // 如果 Cookie 为空或者已经重定向过,则不执行操作
              }
              this.isRedirect = true // 设置跳转标志,避免重复执行
              showDialog(originCookie) // 显示 Cookie 信息
            } catch (error) {
              // 捕获错误并显示弹窗提示
              showDialog('Failed to load the web page. ' + JSON.stringify(error))
            }
          })
      }
      .backgroundColor($r('app.color.page_background_grey'))
      .width(CommonConstants.FULL_WIDTH) 
      .height(CommonConstants.FULL_HEIGHT)
    }
    .titleMode(NavigationTitleMode.Mini)
    .title($r('app.string.navigator_name'))
  }
}

关键代码解读:

  • 在WebView页面加载完成后,同步获取指定URL(USER_ABOUT_URL) 的 Cookie。
  • 如果Cookie为空或已经执行过跳转 (isRedirect为true),则不进行任何操作,防止重复执行。否则,将isRedirect置为true。
  • 通过showDialog显示获取到的Cookie信息。

7. 结语

专栏持续更新,感谢关注、点赞与收藏。

✋ 需要参加鸿蒙认证的请点击 鸿蒙认证链接