無密碼驗證:客戶端

單頁面應用程序的所有渲染由 JavaScript 來完成,因此,我們使用了一個空的 body 部分和一個 main.js 文件。

我們將使用 上篇文章 中的 Router。

渲染

現在,我們使用下面的內容來創建一個 static/js/main.js 文件:

import Router from 'https://unpkg.com/@nicolasparada/router'

import { isAuthenticated } from './auth.js'

const router = new Router()

router.handle('/', guard(view('home')))

router.handle('/callback', view('callback'))

router.handle(/^\//, view('not-found'))

router.install(async resultPromise => {

document.body.innerHTML = ''

document.body.appendChild(await resultPromise)

})

function view(name) {

return (...args) => import(`/js/pages/${name}-page.js`)

.then(m => m.default(...args))

}

function guard(fn1, fn2 = view('welcome')) {

return (...args) => isAuthenticated()

? fn1(...args)

: fn2(...args)

}

與上篇文章不同的是,我們實現了一個 isAuthenticated() 函數和一個 guard() 函數,使用它去渲染兩種驗證狀態的頁面。因此,當用戶訪問 / 時,它將根據用戶是否通過了驗證來展示主頁或者是歡迎頁面。

驗證

現在,我們來編寫 isAuthenticated() 函數。使用下面的內容來創建一個 static/js/auth.js 文件:

export function getAuthUser() {

const authUserItem = localStorage.getItem('auth_user')

const expiresAtItem = localStorage.getItem('expires_at')

if (authUserItem !== null && expiresAtItem !== null) {

const expiresAt = new Date(expiresAtItem)

if (!isNaN(expiresAt.valueOf()) && expiresAt > new Date()) {

try {

return JSON.parse(authUserItem)

} catch (_) { }

}

}

return null

}

export function isAuthenticated() {

return localStorage.getItem('jwt') !== null && getAuthUser() !== null

}

當有人登入時,我們將保存 JSON 格式的 web 令牌、它的過期日期,以及在 localStorage 上的當前已驗證用戶。這個模塊就是這個用處。

  • getAuthUser() 用於從 localStorage 獲取已認證的用戶,以確認 JSON 格式的 Web 令牌沒有過期。
  • isAuthenticated() 在前面的函數中用於去檢查它是否沒有返回 null。

獲取

在繼續這個頁面之前,我將寫一些與服務器 API 一起使用的 HTTP 工具。

我們使用以下的內容去創建一個 static/js/http.js 文件:

import { isAuthenticated } from './auth.js'

function get(url, headers) {

return fetch(url, {

headers: Object.assign(getAuthHeader(), headers),

}).then(handleResponse)

}

function post(url, body, headers) {

return fetch(url, {

method: 'POST',

headers: Object.assign(getAuthHeader(), { 'content-type': 'application/json' }, headers),

body: JSON.stringify(body),

}).then(handleResponse)

}

function getAuthHeader() {

return isAuthenticated()

? { authorization: `Bearer ${localStorage.getItem('jwt')}` }

: {}

}

export async function handleResponse(res) {

const body = await res.clone().json().catch(() => res.text())

const response = {

statusCode: res.status,

statusText: res.statusText,

headers: res.headers,

body,

}

if (!res.ok) {

const message = typeof body === 'object' && body !== null && 'message' in body

? body.message

: typeof body === 'string' && body !== ''

? body

: res.statusText

const err = new Error(message)

throw Object.assign(err, response)

}

return response

}

export default {

get,

post,

}

這個模塊導出了 get() 和 post() 函數。它們是 fetch API 的封裝。當用戶是已驗證的,這二個函數注入一個 Authorization: Bearer 頭到請求中;這樣服務器就能對我們進行身份驗證。

歡迎頁

我們現在來到歡迎頁面。用如下的內容創建一個 static/js/pages/welcome-page.js 文件:

const template = document.createElement('template')

template.innerHTML = `

Passwordless Demo

Access

`

export default function welcomePage() {

const page = template.content.cloneNode(true)

page.getElementById('access-form')

.addEventListener('submit', onAccessFormSubmit)

return page

}

這個頁面使用一個 HTMLTemplateElement 作為視圖。這只是一個輸入用戶 email 的簡單表單。

為了避免干擾,我將跳過錯誤處理部分,只是將它們輸出到控制檯上。

現在,我們來寫 onAccessFormSubmit() 函數。

import http from '../http.js'

function onAccessFormSubmit(ev) {

ev.preventDefault()

const form = ev.currentTarget

const input = form.querySelector('input')

const email = input.value

sendMagicLink(email).catch(err => {

console.error(err)

if (err.statusCode === 404 && wantToCreateAccount()) {

runCreateUserProgram(email)

}

})

}

function sendMagicLink(email) {

return http.post('/api/passwordless/start', {

email,

redirectUri: location.origin + '/callback',

}).then(() => {

alert('Magic link sent. Go check your email inbox.')

})

}

function wantToCreateAccount() {

return prompt('No user found. Do you want to create an account?')

}

它對 /api/passwordless/start 發起了 POST 請求,請求體中包含 email 和 redirectUri。在本例中它返回 404 Not Found 狀態碼時,我們將創建一個用戶。

function runCreateUserProgram(email) {

const username = prompt("Enter username")

if (username === null) return

http.post('/api/users', { email, username })

.then(res => res.body)

.then(user => sendMagicLink(user.email))

.catch(console.error)

}

這個用戶創建程序,首先詢問用戶名,然後使用 email 和用戶名做一個 POST 請求到 /api/users。成功之後,給創建的用戶發送一個魔法鏈接。

回調頁

這是訪問表單的全部功能,現在我們來做回調頁面。使用如下的內容來創建一個 static/js/pages/callback-page.js 文件:

import http from '../http.js'

const template = document.createElement('template')

template.innerHTML = `

Authenticating you

`

export default function callbackPage() {

const page = template.content.cloneNode(true)

const hash = location.hash.substr(1)

const fragment = new URLSearchParams(hash)

for (const [k, v] of fragment.entries()) {

fragment.set(decodeURIComponent(k), decodeURIComponent(v))

}

const jwt = fragment.get('jwt')

const expiresAt = fragment.get('expires_at')

http.get('/api/auth_user', { authorization: `Bearer ${jwt}` })

.then(res => res.body)

.then(authUser => {

localStorage.setItem('jwt', jwt)

localStorage.setItem('auth_user', JSON.stringify(authUser))

localStorage.setItem('expires_at', expiresAt)

location.replace('/')

})

.catch(console.error)

return page

}

請記住……當點擊魔法鏈接時,我們會來到 /api/passwordless/verify_redirect,它將把我們重定向到重定向 URI,我們將放在哈希中的 JWT 和過期日期傳遞給 /callback。

回調頁面解碼 URL 中的哈希,提取這些參數去做一個 GET 請求到 /api/auth_user,用 JWT 保存所有數據到 localStorage 中。最後,重定向到主頁面。

主頁

創建如下內容的 static/pages/home-page.js 文件:

import { getAuthUser } from '../auth.js'

export default function homePage() {

const authUser = getAuthUser()

const template = document.createElement('template')

template.innerHTML = `

Passwordless Demo

Welcome back, ${authUser.username}


分享到:


相關文章: