HarmonyOS 保存登录信息Demo?

主要页面有登录页面,可以保存登录信息到本地及记住密码,

主页面底部导航栏有5个按钮,每个按钮在点击时图片切换为点击图片、title高亮。每个导航按钮对应一个新页面。

列表页面可以实现上、下拉,请求数据,刷新页面,并有刷新加载动画。

再帮忙封装一个http公共请求,所有页面的请求只需要传递url入口后缀及请求参数进行访问。

阅读 580
1 个回答

保存登录信息可以使用关键资产存储,参考:

https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-asset-V5?catalogVersion=V5

代码参考如下:

//权限——在module.json5中添加用户认证权限
"requestPermissions": [{
  "name": "ohos.permission.ACCESS_BIOMETRIC"
}]
//Index 
import asset from '@ohos.security.asset'
import buffer from '@ohos.buffer'
import { promptAction, router } from '@kit.ArkUI'
import userAuth from '@ohos.userIAM.userAuth'
const TAG = 'RememberPWD'
interface IAccountInfo {
  name: string
  password: string
}
@Entry
@Component
struct Index {
  @State message: string = '登录'
  @State accountName: string = ''
  @State accountPassword: string = ''
  @State rememberAccount: boolean = false
  accountAlias: string = 'account&pwdAlias'
  @State invalidInputAccount: boolean = false
  aboutToAppear(): void {
    this.preAccountInfo()
  }
  build() {
    Column() {
      Text(this.message)
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 30, bottom: 50 })
      Column() {
        Column() {
          if (this.invalidInputAccount) {
            Text('请输入账号信息').fontColor(Color.Red)
              .width('100%')
          }
        }.height(30)
        TextInput({ text: $$this.accountName, placeholder: '账号' })
          .height(45).borderRadius(10)
        TextInput({ text: $$this.accountPassword, placeholder: '密码' })
          .type(InputType.Password).borderRadius(10)
          .height(45)
        Row() {
          Checkbox({ name: 'rememberPassword' })
            .select($$this.rememberAccount)
            .shape(CheckBoxShape.ROUNDED_SQUARE)
          Text('记住密码').margin({ left: 5 })
        }
        .width('100%')
        Button('登录').width('100%').margin({ top: 30 })
          .onClick(() => {
            if (this.CheckInputAccount()) {
              return
            }
            this.saveAccount()
          })
        Button('重新加载该页面').width('100%').margin({ top: 5 })
          .onClick(() => {
            router.replaceUrl({
              url: 'pages/Index'
            })
          })
      }
      .width('80%')
      .height(350)
      .justifyContent(FlexAlign.SpaceAround)
    }
    .width('100%')
  }
  CheckInputAccount () {
    if (!this.accountName.length || !this.accountPassword.length) {
      this.invalidInputAccount = true
    } else {
      this.invalidInputAccount = false
    }
    return this.invalidInputAccount
  }
  async saveAccount () {
    if (!this.rememberAccount) {
      return
    }
    const attrInfo = this.getAccountLoginInfo()
    try {
      await asset.add(attrInfo)
    } catch (e) {
      console.error(TAG, `保存帐号失败 ${e.code} ${e.message}`)
      return
    }
    promptAction.showToast({
      message: '已记住密码,请重新进入该页面,会自动填充账号信息'
    })
    console.log(TAG, `保存密码成功`)
  }
  getAccountLoginInfo () {
    const accountInfo: IAccountInfo = {
      name: this.accountName,
      password: this.accountPassword
    }
    const attrInfo: asset.AssetMap = new Map()
    // 需要存储的关键资产数据
    attrInfo.set(asset.Tag.SECRET, this.stringToBuffer(JSON.stringify(accountInfo)))
    // 数据别名
    attrInfo.set(asset.Tag.ALIAS, this.stringToBuffer(this.accountAlias))
    // 解锁状态时可访问
    attrInfo.set(asset.Tag.ACCESSIBILITY, asset.Accessibility.DEVICE_UNLOCKED)
    // 仅在设置锁屏密码的情况下,可访问关键资产
    attrInfo.set(asset.Tag.REQUIRE_PASSWORD_SET, true)
    // 需要开启通过用户认证后,才访问关键资产
    attrInfo.set(asset.Tag.AUTH_TYPE, asset.AuthType.ANY)
    // 新增关键资产时已存在数据,则覆盖
    attrInfo.set(asset.Tag.CONFLICT_RESOLUTION, asset.ConflictResolution.OVERWRITE)
    return attrInfo
  }
  // step1 通过用户认证才能访问的账号信息,要先preQuery获取challenge
  // step2 拉起用户认证
  // step3 通过token查询数据
  // step4 调用postQuery结束查询过程
  async preAccountInfo () {
    const queryInfo: asset.AssetMap = new Map()
    queryInfo.set(asset.Tag.ALIAS, this.stringToBuffer(this.accountAlias))
    // 用户认证token有效期30s
    queryInfo.set(asset.Tag.AUTH_VALIDITY_PERIOD, 30)
    try {
      // step1
      const challenge = await asset.preQuery(queryInfo)
      // step2
      this.startUserAuth(challenge)
    } catch (e) {
      console.error(TAG, `查询账号登录信息失败 ${e.code} ${e.message}`)
    }
  }
  async queryAccountInfo (token: Uint8Array, challenge: Uint8Array) {
    const query: asset.AssetMap = new Map()
    query.set(asset.Tag.ALIAS, this.stringToBuffer(this.accountAlias))
    query.set(asset.Tag.AUTH_TOKEN, token)
    query.set(asset.Tag.AUTH_CHALLENGE, challenge)
    query.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ALL)
    try {
      const data: Array<asset.AssetMap> = await asset.query(query)
      if (data.length) {
        const map = data.shift()! as asset.AssetMap
        const secret = map.get(asset.Tag.SECRET) as Uint8Array
        const accountInfo: IAccountInfo = JSON.parse(this.bufferToString(secret))
        this.accountName = accountInfo.name
        this.accountPassword = accountInfo.password
      } else {
        console.error(TAG, `没有查询到数据`)
      }
    } catch (e) {
      console.error(TAG, `查询${this.accountAlias}数据失败,错误码:${e.code},${e.message}`)
    }
  }
  // 拉起用户认证
  startUserAuth (challenge: Uint8Array) {
    const authParam: userAuth.AuthParam = {
      challenge,
      authType: [userAuth.UserAuthType.PIN, userAuth.UserAuthType.FINGERPRINT],
      authTrustLevel: userAuth.AuthTrustLevel.ATL1
    }
    const widgeParam: userAuth.WidgetParam = {
      title: '请输入密码'
    }
    const userAuthInstance = userAuth.getUserAuthInstance(authParam, widgeParam)
    userAuthInstance.on('result', {
      onResult: async (data) => {
        // 认证成功
        if (data.result === userAuth.UserAuthResultCode.SUCCESS) {
          // step3
          await this.queryAccountInfo(data.token, challenge)
          // step4
          const handle: asset.AssetMap = new Map()
          handle.set(asset.Tag.AUTH_CHALLENGE, challenge)
          await asset.postQuery(handle)
          promptAction.showToast({
            message: '获取账户信息成功'
          })
        }
      }
    })
    userAuthInstance.start()
  }
  stringToBuffer (value: string) {
    return new Uint8Array(buffer.from(value, 'utf8').buffer)
  }
  bufferToString (bufferVal: Uint8Array) {
    return buffer.from(bufferVal).toString('utf8')
  }
}

底部导航栏可以参考:

https://gitee.com/harmonyos\_samples/multi-tab-navigation

上拉加载、下拉刷新可以参考以下代码:

@Entry
@Component
struct Index {
  @State arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  @State refreshing:boolean = false;
  @State refreshOffset: number = 0
  @State refreshState: RefreshStatus = RefreshStatus.Inactive

  @Builder refreshBuilder() {
    Stack({ alignContent: Alignment.Bottom }) {
      if (this.refreshState != RefreshStatus.Inactive && this.refreshState != RefreshStatus.Done) {
        Progress({ value: this.refreshOffset, total: 64, type: ProgressType.Ring })
          .width(32).height(32)
          .style({ status: this.refreshing ? ProgressStatus.LOADING : ProgressStatus.PROGRESSING })
          .margin(10)
      }
    }.height("100%").width("100%")
  }

  build() {
    Refresh({refreshing: $$this.refreshing, builder:this.refreshBuilder()}) {
      List() {
        ForEach(this.arr, (item: number) => {
          ListItem() {
            Text('' + item)
              .width('100%')
              .height(100)
              .fontSize(16)
              .textAlign(TextAlign.Center)
              .borderRadius(10)
              .backgroundColor(0xFFFFFF)
          }
        }, (item: string) => item)
        ListItem() {
          Row() {
            LoadingProgress().height(32).width(48)
            Text("加载中")
          }
        }.width('100%').height(64)
      }
      .onScrollIndex((start:number, end:number)=>{
        if (end >= this.arr.length) {
          setTimeout(() => {
            for (let i = 0; i < 5; i++) {
              this.arr.push(this.arr.length)
            }
          }, 1000)
        }
      })
      .onAreaChange((oldValue: Area, newValue: Area)=>{
        this.refreshOffset = newValue.position.y as number
      })
      .scrollBar(BarState.Off)
      .divider({strokeWidth:"1px"})
    }.width('100%').height('100%').backgroundColor(0xDCDCDC)
    .onStateChange((state:RefreshStatus)=>{
      this.refreshState = state;
    })
    .onRefreshing(()=>{
      setTimeout(()=>{
        this.refreshing = false
      }, 2000)
    })
  }
}

加密的Http公共请求需自行实现,加密可以参考加解密算法框架服务:

https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-cryptoframework-V5