前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >BlockStack身份授权流程

BlockStack身份授权流程

作者头像
rectinajh
发布2020-04-01 19:07:22
9870
发布2020-04-01 19:07:22
举报

为什么需要应用授权

去中心化身份的前提条件,是在同一个身份平台所能覆盖的范围内,用户的身份识别和检测标准统一,作为区块链应用开发基础设施的服务提供商,BlockStack 在数据权限上将应用权限和用户身份/数据分离,保障用户数据所有权。

这种设计虽然实现起来较为复杂,且需要多种类型的服务提供支持,但不论是对用户,开发者,还是整个 Blockstack 生态,都是非常优雅的方案。

mmexport1585486832221.jpg

  • 用户
    • gaia 通过 app 域名隔离数据权限,无需担心全量数据安全
    • 可以使用多身份来管理相同的应用数据
    • 使用应用之前明确的清楚应用的权限范围
    • 可以将数据在不同应用之间迁移
  • 开发者
    • 无需单独实现账户注册与用户管理等服务
    • 不需要处理复杂的加密解密等校验逻辑
  • Blockstack
    • 一套 DID 身份与用户数据管理标准
    • 提供更多的应用基础设施服务

应用授权的流程

如下所示:

  • 构建 Token 并跳转 通过 BlockStack.js 所提供的 redirectToSignIn 方法跳转到 BlockStackBrowser 完成授权
    • 构建请求体 authRequest
      • generateAndStoreTransitKey 生成一个临时并随机的公私钥 Key
      • 返回一个经过编码的 authRequest 字符串
    • launchCustomProtocol 封装一系列的逻辑并跳转至 BlockStackBrowser
      • 添加一些超时和请求序号等操作
  • Browser 接收参数并解析 BlockStack 浏览器端接收到 authRequest 参数触发验证流程
    • app/js/auth/index.js 中使用 verifyAuthRequestAndLoadManifest 校验 authRequest 合法性并获取 DApp 的 Manifest.json 文件
      • verifyAuthRequest 校验 Token 的合法性
      • fetchAppManifest 获取应用 Manifest.json 文件
    • getFreshIdentities 通过用户缓存在浏览器中的登录信息 addresses(地址)获取用户的信息
      • 请求 https://core.blockstack.org/addresses/bitcoin/${address} 获得用户比特币地址的信息
      • 加载用户域名信息
      • 从 Gaia 获取用户 profile 文件的位置,并拿到用户的 profile 文件
    • 用户根据 profile 中包含的身份信息让用户选择需要授权的用户名,触发login
      • 客户端 noCoreStorage 作为监听标志来构造 authRespon
      • 获取用户的 profileUrl
      • 获取 app 的 bucketUrl
      • 创建并更新用户的 Profile 中 apps 的数据
      • 构建 AuthResponse Token
        • 生成 appPrivateKey
        • 生成 gaiaAssociationToken
      • 通过 BlockStack.js 的 redirectUserToApp 返回应用
      • redirect URI
  • APP 接受并解析 通过 AuthRrsponse 参数解析获取用户信息并持久化
    • 调用 userSession.SignInPendinguserSession.handlePendingSignIn 能够触发对 AuthResponse 的解析
    • 通过verifyAuthResponse 进行一系列的验证,fetchPrivate 获得授权用户的 profile 数据
    • 持久化用户数据到浏览器 localstorage
代码语言:javascript
复制
sequenceDiagram
    participant A as App
    participant B as Broswer
    participant G as Gaia
    participant C as BlockStack Core
    participant Bi as Bitcoin network


    Note over A: Make authRequest
    A->>+B: authRequest Token
    Note over B: verifyAuthRequest
    B->>-A: fetch Manifest.json
    A->>B: Manifest.json
    opt getFreshIdentities
        B->>Bi: nameLookup names
        Bi->>B: get names
        alt is has no name
            B->>+G: fetchProfileLocations
            G->>-B: profile data
        else is well
            B->>+C: nameLookupUrl
            C->>-B: nameInfo with zoneFile
        end
    end 
    note over B: render account list
    opt user click login
         B->>+C: nameLookupUrl
         C->>-B: profile data
        note over B: verify APP Scope
        alt is name has no zonefile
            B->>G: fetchProfileLocations
            G->>B: profile url
        else is has zoneFile
            note over  B: Parse zonefile url
        end
        B->>G: getAppBucketUrl
        G->>B: appBucketUrl
        note over B: signProfileForUpload
        B-->> G: uploadProfile
        note over B: AuthResponse
        B->>A: AuthResponse Token
    end
    note over A: getAuthRes Token
    A->>G: get profile
    G->>A: profile data
    note over A: store userData

代码解析

构建授权请求

代码语言:javascript
复制
// 触发授权请求
 redirectToSignIn(
    redirectURI?: string,
    manifestURI?: string,
    scopes?: Array<AuthScope | string>
  ): void {
    const transitKey = this.generateAndStoreTransitKey() // 生成一个临时秘钥对
    const authRequest = this.makeAuthRequest(transitKey, redirectURI, manifestURI, scopes) // 构建 AuthRequest 
    const authenticatorURL = this.appConfig && this.appConfig.authenticatorURL // 获取授权跳转链接
    return authApp.redirectToSignInWithAuthRequest(authRequest, authenticatorURL) // 跳转
  }

// 构建 AuthRequest 数据
 makeAuthRequest(
    transitKey?: string,
    redirectURI?: string,
    manifestURI?: string,
    scopes?: Array<AuthScope | string>,
  ): string {
    const appConfig = this.appConfig 
    transitKey = transitKey || this.generateAndStoreTransitKey() 
    redirectURI = redirectURI || appConfig.redirectURI()
    manifestURI = manifestURI || appConfig.manifestURI()
    scopes = scopes || appConfig.scopes 
    return authMessages.makeAuthRequest(
      transitKey, redirectURI, manifestURI,
      scopes)
  }

// 构建请求授权 Token 详情
export function makeAuthRequest(
  transitPrivateKey?: string,
  redirectURI?: string, 
  manifestURI?: string, 
  scopes: Array<AuthScope | string> = DEFAULT_SCOPE.slice(),
  appDomain?: string,
  expiresAt: number = nextMonth().getTime(),
  extraParams: any = {}
): string {
 // ...
  const payload = Object.assign({}, extraParams, {
    jti: makeUUID4(),
    iat: Math.floor(new Date().getTime() / 1000), // JWT times are in seconds
    exp: Math.floor(expiresAt / 1000), // JWT times are in seconds
    iss: null,
    public_keys: [],
    domain_name: appDomain,
    manifest_uri: manifestURI,
    redirect_uri: redirectURI,
    version: VERSION,
    do_not_include_profile: true,
    supports_hub_url: true,
    scopes
  })
  /* Convert the private key to a public key to an issuer */
  const publicKey = SECP256K1Client.derivePublicKey(transitPrivateKey)
  payload.public_keys = [publicKey]
  const address = publicKeyToAddress(publicKey)
  payload.iss = makeDIDFromAddress(address)

  /* Sign and return the token */
  const tokenSigner = new TokenSigner('ES256k', transitPrivateKey)
  const token = tokenSigner.sign(payload) // jsontokens 用私钥签名
  return token
}

最终的 Token 会成为我们看到的形式

代码语言:javascript
复制
https://browser.blockstack.org/auth?authRequest=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJqdGkiOiIyYTA0Y2Q4YS1lZTBmLTQ1ZTYtYjE4MS1mNWE4YzdjMmY3NzUiLCJpYXQiOjE1ODQxNTk3MzgsImV4cCI6MTU4NDE2MzMzOCwiaXNzIjoiZGlkOmJ0Yy1hZGRyOjFCWlNXWGVUWTNoYkd3WFZBOEt2NjNoZFZGWDI5Z2JabmciLCJwdWJsaWNfa2V5cyI6WyIwMjNlMjM3MDk1NDBhNmVkOWEyNWQ0YWUzOGQ1MTcxYTVlNjljNGY4ZDhjODQzOWZjMzg2YTllYmQ3NGJmMDgyOTEiXSwiZG9tYWluX25hbWUiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJtYW5pZmVzdF91cmkiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvbWFuaWZlc3QuanNvbiIsInJlZGlyZWN0X3VyaSI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsInZlcnNpb24iOiIxLjMuMSIsImRvX25vdF9pbmNsdWRlX3Byb2ZpbGUiOnRydWUsInN1cHBvcnRzX2h1Yl91cmwiOnRydWUsInNjb3BlcyI6WyJzdG9yZV93cml0ZSIsInB1Ymxpc2hfZGF0YSJdfQ.HFaU9K2QV_y13h-ZMqEnzvsnC2RvphfdaA3r0qaCyfBZSA0mGghDvxU0z1Mq7_HfLgq9ClVNYHJvSR9_OHZc3g

Browser 端的参数解析与数据加载

BlockStack 浏览器端处理 query 中的 authRequest 参数

代码语言:javascript
复制
// app/js/auth/index.js 在组建内触发参数解析
  componentWillMount() {
    const queryDict = queryString.parse(location.search)
    const echoRequestId = queryDict.echo

    const authRequest = getAuthRequestFromURL() // 获取 query 中的参数
    const decodedToken = decodeToken(authRequest)
    const { scopes } = decodedToken.payload // gaia 授权范围

    this.setState({
      authRequest,
      echoRequestId,
      decodedToken,
      scopes: {
        ...this.state.scopes,
        email: scopes.includes('email'),
        publishData: scopes.includes('publish_data')
      }
    })

    this.props.verifyAuthRequestAndLoadManifest(authRequest) // 校验 authRequest 并获取 APP 的 Manifest.json 文件

    this.getFreshIdentities() // 加载用户账户信息
  }


  getFreshIdentities = async () => {
    await this.props.refreshIdentities(this.props.api, this.props.addresses)
    this.setState({ refreshingIdentities: false })
  }

// 加载用户信息
function refreshIdentities(
  api,
  ownerAddresses
) {
  return async (dispatch) => {
    logger.info('refreshIdentities')
    // account.identityAccount.addresses
    const promises = ownerAddresses.map((address, index) => { // 根据用户的储存在浏览器本地的地址数据循环拉取用户信息
      const promise = new Promise(resolve => {
        const url = api.bitcoinAddressLookupUrl.replace('{address}', address) // 比特币网络中的地址查询链接
        return fetch(url)
          .then(response => response.text())
          .then(responseText => JSON.parse(responseText))
          .then(responseJson => {
            if (responseJson.names.length === 0) { // 没有用户名
              const gaiaBucketAddress = ownerAddresses[0] // 默认第一个地址是用户的 gaia 地址
              return fetchProfileLocations(  // 寻找 profile 的储存位置
                api.gaiaHubConfig.url_prefix,
                address,
                gaiaBucketAddress,
                index
              ).then(returnObject => {
                if (returnObject && returnObject.profile) {
                  const profile = returnObject.profile
                  const zoneFile = ''
                  dispatch(updateProfile(index, profile, zoneFile)) // 更新现有的用户 profile 信息 
                  let verifications = []
                  let trustLevel = 0
                  // ...
                } else {
                  resolve()
                  return Promise.resolve()
                }
              })
            } else {
              const nameOwned = responseJson.names[0]
              dispatch(usernameOwned(index, nameOwned)) // 更新 redux
              const lookupUrl = api.nameLookupUrl.replace('{name}', nameOwned) // 通过用户名查询数据
              logger.debug(`refreshIdentities: fetching: ${lookupUrl}`)
              return fetch(lookupUrl)
                .then(response => response.text())
                .then(responseText => JSON.parse(responseText))
                .then(lookupResponseJson => {
                  const zoneFile = lookupResponseJson.zonefile // 获的用户的 zonefile 
                  const ownerAddress = lookupResponseJson.address 
                  const expireBlock = lookupResponseJson.expire_block || -1
                      resolve()
                      return Promise.resolve()
                    })
                    .catch(error => {
                      dispatch(updateProfile(index, DEFAULT_PROFILE, zoneFile))
                      resolve()
                      return Promise.resolve()
                    })
                })
                .catch(error => {
                  resolve()
                  return Promise.resolve()
                })
            }
          })
          .catch(error => {
            resolve()
            return Promise.resolve()
          })
      })
      return promise
    })
    return Promise.all(promises)
  }
}

localstorage 中保存了 Redux 的数据结构

Browser 端的解析和 Manifest 拉取

image

代码语言:javascript
复制
BlockStack.js
// 校验 authRequest
export async function verifyAuthRequestAndLoadManifest(token: string): Promise<any> {
  const valid = await verifyAuthRequest(token)
  if (!valid) {
    throw new Error('Token is an invalid auth request')
  }
  return fetchAppManifest(token)
}

// 校验 authRequest 的 token
export async function verifyAuthRequest(token: string): Promise<boolean> {
  if (decodeToken(token).header.alg === 'none') {
    throw new Error('Token must be signed in order to be verified')
  }
  const values = await Promise.all([
    isExpirationDateValid(token),
    isIssuanceDateValid(token),
    doSignaturesMatchPublicKeys(token),
    doPublicKeysMatchIssuer(token),
    isManifestUriValid(token),
    isRedirectUriValid(token)
  ])
  return values.every(val => val)
}

// 获取 APP 的 Manifest 文件
export async function fetchAppManifest(authRequest: string): Promise<any> {
  if (!authRequest) {
    throw new Error('Invalid auth request')
  }
  const payload = decodeToken(authRequest).payload
  if (typeof payload === 'string') {
    throw new Error('Unexpected token payload type of string')
  }
  const manifestURI = payload.manifest_uri as string
  try {
    Logger.debug(`Fetching manifest from ${manifestURI}`)
    const response = await fetchPrivate(manifestURI)
    const responseText = await response.text()
    const responseJSON = JSON.parse(responseText)
    return { ...responseJSON, manifestURI }
  } catch (error) {
    console.log(error)
    throw new Error('Could not fetch manifest.json')
  }
}

用户点击登录之后的授权流程

代码语言:javascript
复制
// login 函数 用户点击多个授权之后的回调
  login = (identityIndex = this.state.currentIdentityIndex) => {
    this.setState({
      processing: true,
      invalidScopes: false
    })
    // ...
    // if profile has no name, lookupUrl will be
    // http://localhost:6270/v1/names/ which returns 401
    const lookupUrl = this.props.api.nameLookupUrl.replace(
      '{name}',
      lookupValue
    )
    fetch(lookupUrl)
      .then(response => response.text())
      .then(responseText => JSON.parse(responseText))
      .then(responseJSON => {
        if (hasUsername) {
          if (responseJSON.hasOwnProperty('address')) {
            const nameOwningAddress = responseJSON.address
            if (nameOwningAddress === identity.ownerAddress) {
              logger.debug('login: name has propagated on the network.')
              this.setState({
                blockchainId: lookupValue
              })
            } else {
              logger.debug('login: name is not usable on the network.')
              hasUsername = false
            }
          } else {
            logger.debug('login: name is not visible on the network.')
            hasUsername = false
          }
        }

        const appDomain = this.state.decodedToken.payload.domain_name
        const scopes = this.state.decodedToken.payload.scopes
        const needsCoreStorage = !appRequestSupportsDirectHub(
          this.state.decodedToken.payload
        )
        const scopesJSONString = JSON.stringify(scopes)
       //...
       // APP 校验权限
        if (requestingStoreWrite && !needsCoreStorage) {
          this.setState({
            noCoreStorage: true // 更新跳转状态
          })
          this.props.noCoreSessionToken(appDomain)
        } else {
          this.setState({
            noCoreStorage: true // 更新跳转状态
          })
          this.props.noCoreSessionToken(appDomain)
        }
      })
  }



// 跳转状态监听
  componentWillReceiveProps(nextProps) {
    if (!this.state.responseSent) {
      // ...
      const appDomain = this.state.decodedToken.payload.domain_name
      const localIdentities = nextProps.localIdentities
      const identityKeypairs = nextProps.identityKeypairs
      if (!appDomain || !nextProps.coreSessionTokens[appDomain]) {
        if (this.state.noCoreStorage) { // 跳转判断标志
          logger.debug(
            'componentWillReceiveProps: no core session token expected'
          )
        } else {
          logger.debug(
            'componentWillReceiveProps: no app domain or no core session token'
          )
          return
        }
      }


      // ...
      const identityIndex = this.state.currentIdentityIndex

      const hasUsername = this.state.hasUsername
      if (hasUsername) {
        logger.debug(`login(): id index ${identityIndex} has no username`)
      }

      // Get keypair corresponding to the current user identity 获得秘钥对
      const profileSigningKeypair = identityKeypairs[identityIndex]
      const identity = localIdentities[identityIndex]

      let blockchainId = null
      if (decodedCoreSessionToken) {
        blockchainId = decodedCoreSessionToken.payload.blockchain_id
      } else {
        blockchainId = this.state.blockchainId
      }
      // 获得用户的私钥和 appsNodeKey 
      const profile = identity.profile
      const privateKey = profileSigningKeypair.key
      const appsNodeKey = profileSigningKeypair.appsNodeKey
      const salt = profileSigningKeypair.salt

      let profileUrlPromise

      if (identity.zoneFile && identity.zoneFile.length > 0) {
        const zoneFileJson = parseZoneFile(identity.zoneFile)
        const profileUrlFromZonefile = getTokenFileUrlFromZoneFile(zoneFileJson) // 用 zonefile 获取用户信息
        if (
          profileUrlFromZonefile !== null &&
          profileUrlFromZonefile !== undefined
        ) {
          profileUrlPromise = Promise.resolve(profileUrlFromZonefile)
        }
      }

      const gaiaBucketAddress = nextProps.identityKeypairs[0].address
      const identityAddress = nextProps.identityKeypairs[identityIndex].address
      const gaiaUrlBase = nextProps.api.gaiaHubConfig.url_prefix
       
      // 没有 profile 就从 gaia 中查询
      if (!profileUrlPromise) {
        // use default Gaia hub if we can't tell from the profile where the profile Gaia hub is
        profileUrlPromise = fetchProfileLocations(
          gaiaUrlBase,
          identityAddress,
          gaiaBucketAddress,
          identityIndex
        ).then(fetchProfileResp => {
          if (fetchProfileResp && fetchProfileResp.profileUrl) {
            return fetchProfileResp.profileUrl
          } else {
            return getDefaultProfileUrl(gaiaUrlBase, identityAddress)
          }
        })
      }

      profileUrlPromise.then(async profileUrl => {
        const appPrivateKey = await BlockstackWallet.getLegacyAppPrivateKey(appsNodeKey, salt, appDomain) // 获得 APP 的 PrivateKey
        // Add app storage bucket URL to profile if publish_data scope is requested
        if (this.state.scopes.publishData) {
          let apps = {}
          if (profile.hasOwnProperty('apps')) {
            apps = profile.apps // 获得用户的 apps 配置
          }

          if (storageConnected) {
            const hubUrl = this.props.api.gaiaHubUrl
            await getAppBucketUrl(hubUrl, appPrivateKey) // 根据用户的授权历史在 apps 查找授权 APP 的 bucket 位置,没有则创建新的
              .then(appBucketUrl => {
                logger.debug(
                  `componentWillReceiveProps: appBucketUrl ${appBucketUrl}`
                )
                apps[appDomain] = appBucketUrl // bucketurl
                logger.debug(
                  `componentWillReceiveProps: new apps array ${JSON.stringify(
                    apps
                  )}`
                )
                profile.apps = apps
                const signedProfileTokenData = signProfileForUpload(  // 更新用户的 profile
                  profile,
                  nextProps.identityKeypairs[identityIndex],
                  this.props.api
                )
                logger.debug(
                  'componentWillReceiveProps: uploading updated profile with new apps array'
                )
                return uploadProfile(
                  this.props.api,
                  identity,
                  nextProps.identityKeypairs[identityIndex],
                  signedProfileTokenData
                )
              })
              .then(() => this.completeAuthResponse( // 构建 AuthResponse
                  privateKey,
                  blockchainId,
                  coreSessionToken,
                  appPrivateKey,
                  profile,
                  profileUrl
                )
              )

          } 
          // ...
        } else {
          await this.completeAuthResponse(
            privateKey,
            blockchainId,
            coreSessionToken,
            appPrivateKey,
            profile,
            profileUrl
          )
        }
      })
    } 
  }


// 构造授权之后的返回

 const authResponse = await makeAuthResponse(
      privateKey,
      profileResponseData,
      blockchainId,
      metadata,
      coreSessionToken,
      appPrivateKey,
      undefined,
      transitPublicKey,
      hubUrl,
      blockstackAPIUrl,
      associationToken
    )



 redirectUserToApp(this.state.authRequest, authResponse)

// 重定向回 APP 页面
  const payload = decodeToken(authRequest).payload
  if (typeof payload === 'string') {
    throw new Error('Unexpected token payload type of string')
  }
  let redirectURI = payload.redirect_uri as string
  Logger.debug(redirectURI)
  if (redirectURI) {
    redirectURI = updateQueryStringParameter(redirectURI, 'authResponse', authResponse)
  } else {
    throw new Error('Invalid redirect URI')
  }
  const location = getGlobalObject('location', { throwIfUnavailable: true, usageDesc: 'redirectUserToApp' })
  kk = redirectURI
}

最后我们得到

代码语言:javascript
复制
http://localhost:3000/?authResponse=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJqdGkiOiIyMzA3NmE1NC0zYmVkLTRiYjEtOGZlOC0yY2I1MDgyNjBiOGIiLCJpYXQiOjE1ODQyNTU1MzksImV4cCI6MTU4NjkzMzg2NSwiaXNzIjoiZGlkOmJ0Yy1hZGRyOjE4d0xaanozRWFUdzd5d2o0a3hlZjR1eWlMWDFmeW9GWDMiLCJwcml2YXRlX2tleSI6IjdiMjI2OTc2MjIzYTIyNjQzNDY0NjQ2NjY0Mzg2NjM4MzAzMTM0NjQzMjYxMzY2NjM4MzAzOTM1NjEzNTY1MzIzMjMxMzY2NDYxNjM2MzIyMmMyMjY1NzA2ODY1NmQ2NTcyNjE2YzUwNGIyMjNhMjIzMDMyNjI2NDYzMzUzNjMwNjIzODY0MzY2NTM2NjEzNjY1MzEzNjM2NjEzNzM0NjQzMzM5MzYzNjM2NjUzNzYyMzMzODYxMzkzMjY1Mzg2MzMxNjIzMDM2MzY2MzY1MzAzOTY1MzUzMTM2NjIzMjYyNjE2NDYzMzIzMjM1NjMzNjMxMzMyMjJjMjI2MzY5NzA2ODY1NzI1NDY1Nzg3NDIyM2EyMjM5NjIzMzYyMzg2NDM5MzYzMjM4NjQzNjM0MzU2MTMxMzYzMzM0MzQzNTY1NjM2NTY0NjEzMzM4NjUzOTYxMzY2NjY1MzQzMTMyMzYzMjM5NjQ2MjY0NjE2NjY2NjMzNDMzMzAzNTM5NjIzNjYzNjUzNjM5NjMzMTYyMzczOTYyMzkzMzYyNjEzNTM4MzkzNjY2NjUzNzM5NjY2MTMyMzYzNjM5NjQ2NjM3MzkzMzM5MzUzNTMyNjQzNDYxMzYzNjM0Mzc2MzM5MzkzNDYyMzEzODM5MzE2NDMxMzEzNTYzMzE2NTM3MzkzNzY1MzMzMDY1MzI2NDM1MzM2NDYxMzEzODM5MzA2MTM1NjUzODMzMzc2NDM0MzUzNzY0MzIzNzM5MzI2MTMyMzMzMDY2MzgzNjYzMzkzOTMyNjQ2MjYxMjIyYzIyNmQ2MTYzMjIzYTIyNjI2NTY0MzMzMDY2MzQ2MzMyMzI2MTY1MzA2MzY0MzUzNTM1Mzc2NTYxMzQ2MzMxMzk2MjYxNjQzMzM3MzU2MjMxMzQzODM4NjEzMDM4NjQzMzY0NjIzOTMyMzQ2NTMwNjYzOTMzMzgzNjM0MzMzMDM0MzEzNTMxMzUzODM1NjQyMjJjMjI3NzYxNzM1Mzc0NzI2OTZlNjcyMjNhNzQ3Mjc1NjU3ZCIsInB1YmxpY19rZXlzIjpbIjAzOWM4OTg3OWZmNTZhMzVkOWU0MjYzYTI5ZjI5YmQyZTZjMzE1Y2UyMTZiMzYyMDZkZDA3NDg5OWMxMWJhNmRjYSJdLCJwcm9maWxlIjpudWxsLCJ1c2VybmFtZSI6Il9fY2Fvc19fLmlkLmJsb2Nrc3RhY2siLCJjb3JlX3Rva2VuIjpudWxsLCJlbWFpbCI6bnVsbCwicHJvZmlsZV91cmwiOiJodHRwczovL2dhaWEuYmxvY2tzdGFjay5vcmcvaHViLzE4d0xaanozRWFUdzd5d2o0a3hlZjR1eWlMWDFmeW9GWDMvcHJvZmlsZS5qc29uIiwiaHViVXJsIjoiaHR0cHM6Ly9odWIuYmxvY2tzdGFjay5vcmciLCJibG9ja3N0YWNrQVBJVXJsIjoiaHR0cHM6Ly9jb3JlLmJsb2Nrc3RhY2sub3JnIiwiYXNzb2NpYXRpb25Ub2tlbiI6ImV5SjBlWEFpT2lKS1YxUWlMQ0poYkdjaU9pSkZVekkxTmtzaWZRLmV5SmphR2xzWkZSdlFYTnpiMk5wWVhSbElqb2lNREprWkRKbFlXTTFaamcxTURFMllqRXlNV0kwWlRBME16SmxOREkyTkRabFlXTXhOVFkyWTJZeFpqVTVZemN4T1dZeE5UWmxPR0UyTm1ZNE1XSTRZek5pSWl3aWFYTnpJam9pTURNNVl6ZzVPRGM1Wm1ZMU5tRXpOV1E1WlRReU5qTmhNamxtTWpsaVpESmxObU16TVRWalpUSXhObUl6TmpJd05tUmtNRGMwT0RrNVl6RXhZbUUyWkdOaElpd2laWGh3SWpveE5qRTFOemt4TkRRMkxqRTROeXdpYVdGMElqb3hOVGcwTWpVMU5EUTJMakU0Tnl3aWMyRnNkQ0k2SWpjMk1UYzNZV1U1T0RWa01EVmtOell5TmpVMFltTmtZVEJsTkRReE16Y3hJbjAuMThxOTZJeW5DWTJFclRMdGtsOG05dUpQR2tickRLaXgxY3Y2NDFhOC1RRnZHT1BIODM1S0FrZm1rd19nNVRZM2lSamQxcGt3VG44Y2F1ZFhLQWY2TVEiLCJ2ZXJzaW9uIjoiMS4zLjEifQ.KGRfgjzPkQ3Y66ek2EjS2XT8EFeRc9FoElnxrsPJOCN3_YBibRpvaYPVbUkMXAqqVM6jIzlJBfdvFI3jN4O5Cg

App 端的解析与处理

app 端的数据通过 userSession.SignInPendinguserSession.handlePendingSignIn 解析 authResponse 参数

代码语言:javascript
复制
export async function handlePendingSignIn(
  nameLookupURL: string = '', 
  authResponseToken: string = getAuthResponseToken(), 
  transitKey?: string,
  caller?: UserSession
): Promise<UserData> {

  if (!caller) {
    caller = new UserSession()
  }
  const sessionData = caller.store.getSessionData()

  if (!transitKey) {
    transitKey = caller.store.getSessionData().transitKey
  }
  if (!nameLookupURL) {
    let coreNode = caller.appConfig && caller.appConfig.coreNode
    if (!coreNode) {
      coreNode = config.network.blockstackAPIUrl
    }

    const tokenPayload = decodeToken(authResponseToken).payload
    if (typeof tokenPayload === 'string') {
      throw new Error('Unexpected token payload type of string')
    }
    if (isLaterVersion(tokenPayload.version as string, '1.3.0')
       && tokenPayload.blockstackAPIUrl !== null && tokenPayload.blockstackAPIUrl !== undefined) {

      config.network.blockstackAPIUrl = tokenPayload.blockstackAPIUrl as string
      coreNode = tokenPayload.blockstackAPIUrl as string
    }

    nameLookupURL = `${coreNode}${NAME_LOOKUP_PATH}`
  }

  const isValid = await verifyAuthResponse(authResponseToken, nameLookupURL) // 校验 authResponse Token
  if (!isValid) {
    throw new LoginFailedError('Invalid authentication response.')
  }
  const tokenPayload = decodeToken(authResponseToken).payload

  // TODO: real version handling
  let appPrivateKey = tokenPayload.private_key as string
  let coreSessionToken = tokenPayload.core_token as string
  // ...
  let hubUrl = BLOCKSTACK_DEFAULT_GAIA_HUB_URL
  let gaiaAssociationToken: string

  const userData: UserData = {
    username: tokenPayload.username as string,
    profile: tokenPayload.profile,
    email: tokenPayload.email as string,
    decentralizedID: tokenPayload.iss,
    identityAddress: getAddressFromDID(tokenPayload.iss),
    appPrivateKey,
    coreSessionToken,
    authResponseToken,
    hubUrl,
    coreNode: tokenPayload.blockstackAPIUrl as string,
    gaiaAssociationToken
  }
  const profileURL = tokenPayload.profile_url as string
  if (!userData.profile && profileURL) {
    const response = await fetchPrivate(profileURL) // 拉取用户 profile 信息
    if (!response.ok) { // return blank profile if we fail to fetch
      userData.profile = Object.assign({}, DEFAULT_PROFILE)
    } else {
      const responseText = await response.text()
      const wrappedProfile = JSON.parse(responseText)
      const profile = extractProfile(wrappedProfile[0].token)
      userData.profile = profile
    }
  } else {
    userData.profile = tokenPayload.profile
  }

  sessionData.userData = userData
  caller.store.setSessionData(sessionData) // 缓存用户数据到 

  return userData // 返回结果
}

userData 最后的样子(Redux)

image

  • name - 用户的域名
  • profile - 域名下的身份信息
  • email - 用户的邮箱信息
  • decentralizedID - DID
  • identityAddress - 用户身份的 BTC 地址
  • appPrivateKey - app 应用的私钥
  • coreSessionToken - V2 预留
  • authResponseToken - browser 返回的授权信息 Token
  • hubUrl - gaia hub 的地址
  • gaiaAssociationToken - app 与 gaia 交互所需要的 token
  • gaiaHubConfig - gaia 服务器的配置信息
KeyPairs && appPrivateKey
代码语言:javascript
复制
  const appPrivateKey = await BlockstackWallet.getLegacyAppPrivateKey(appsNodeKey, salt, appDomain)

// src/wallet.ts@blockstack.js// 获取身份的密钥对

 getIdentityKeyPair(addressIndex: number, 
                     alwaysUncompressed: boolean = false): IdentityKeyPair {
    const identityNode = this.getIdentityAddressNode(addressIndex)

    const address = BlockstackWallet.getAddressFromBIP32Node(identityNode)
    let identityKey = getNodePrivateKey(identityNode)
    if (alwaysUncompressed && identityKey.length === 66) {
      identityKey = identityKey.slice(0, 64)
    }

    const identityKeyID = getNodePublicKey(identityNode)
    const appsNodeKey = BlockstackWallet.getAppsNode(identityNode).toBase58() // 获取 appsNodeKey
    const salt = this.getIdentitySalt()
    const keyPair = {
      key: identityKey,
      keyID: identityKeyID,
      address,
      appsNodeKey,
      salt
    }
    return keyPair
  }
}

  // 获取 appPrivateKey
  static getLegacyAppPrivateKey(appsNodeKey: string, 
                                salt: string, appDomain: string): string {
    const appNode = getLegacyAppNode(appsNodeKey, salt, appDomain)
    return getNodePrivateKey(appNode).slice(0, 64)
  }

function getNodePrivateKey(node: BIP32Interface): string {
  return ecPairToHexString(ECPair.fromPrivateKey(node.privateKey))
}


// src/storage/index.ts@blockstack.js
// 使用 appPrivateKey 加密内容
export async function encryptContent(
  content: string | Buffer,
  options?: EncryptContentOptions,
  caller?: UserSession
): Promise<string> {
  const opts = Object.assign({}, options)
  let privateKey: string
  if (!opts.publicKey) {
    privateKey = (caller || new UserSession()).loadUserData().appPrivateKey
    opts.publicKey = getPublicKeyFromPrivate(privateKey)
  }
 // ...
  const contentBuffer = typeof content === 'string' ? Buffer.from(content) : content
  const cipherObject = await encryptECIES(opts.publicKey, 
                                          contentBuffer, 
                                          wasString, 
                                          opts.cipherTextEncoding)
  let cipherPayload = JSON.stringify(cipherObject)
  // ...
  return cipherPayload
}

// 使用 appPrivateKey 解密内容
export function decryptContent(
  content: string,
  options?: {
    privateKey?: string
  },
  caller?: UserSession
): Promise<string | Buffer> {
  const opts = Object.assign({}, options)
  if (!opts.privateKey) {
    opts.privateKey = (caller || new UserSession()).loadUserData().appPrivateKey
  }

  try {
    const cipherObject = JSON.parse(content)
    return decryptECIES(opts.privateKey, cipherObject)
  } catch (err) {
}
持久化 Redux 数据
代码语言:javascript
复制
// app/js/store/configure.js@browser 

import persistState from 'redux-localstorage' // 持久化 Redux 中间件

export default function configureStore(initialState) {
  return finalCreateStore(RootReducer, initialState)
}

const finalCreateStore = composeEnhancers(
  applyMiddleware(thunk),
  persistState(null, { // persistState 持久化
    // eslint-disable-next-line
    slicer: paths => state => ({
      ...state,
      auth: AuthInitialState,
      notifications: []
    })
  })
)(createStore)

userData 也会保存在 localstorage 中

image

localstorage 的保存
代码语言:javascript
复制
export class UserSession {
    
   // ...
   constructor(options?: {
    appConfig?: AppConfig,
    sessionStore?: SessionDataStore,
    sessionOptions?: SessionOptions }) {
    // ...
    if (options && options.sessionStore) {
      this.store = options.sessionStore
    } else if (runningInBrowser) {
      if (options) {
        this.store = new LocalStorageStore(options.sessionOptions)
      } else {
        this.store = new LocalStorageStore()
      }
    } else if (options) {
      this.store = new InstanceDataStore(options.sessionOptions)
    } else {
      this.store = new InstanceDataStore()
    }
  }

}


// 继承并覆盖 setSessionData ,持久化数据到 LocalStorage
export class LocalStorageStore extends SessionDataStore {
  key: string

  constructor(sessionOptions?: SessionOptions) {
    super(sessionOptions)
   //...

  setSessionData(session: SessionData): boolean {
    localStorage.setItem(this.key, session.toString())
    return true
  }
}

2020-03-15 由助教曹帅整理

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么需要应用授权
  • 应用授权的流程
  • 代码解析
    • 构建授权请求
      • Browser 端的参数解析与数据加载
        • Browser 端的解析和 Manifest 拉取
          • 用户点击登录之后的授权流程
            • App 端的解析与处理
              • KeyPairs && appPrivateKey
              • 持久化 Redux 数据
              • localstorage 的保存
          相关产品与服务
          消息队列 TDMQ
          消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档