無密碼驗證:客戶端
| 2018-07-12 11:41
我們繼續 無密碼驗證 的文章。上一篇文章中,我們用 Go 寫了一個 HTTP 服務,用這個服務來做無密碼驗證 API。今天,我們為它再寫一個 JavaScript 客戶端。
我們將使用 這裡的 這個單頁面應用程式(SPA)來展示使用的技術。如果你還沒有讀過它,請先讀它。
記住流程:
- 使用者輸入其 email。
- 使用者收到一個帶有魔法連結的郵件。
- 使用者點選該連結、
- 使用者驗證成功。
對於根 URL(/
),我們將根據驗證的狀態分別使用兩個不同的頁面:一個是帶有訪問表單的頁面,或者是已驗證透過的使用者的歡迎頁面。另一個頁面是驗證回撥的重定向頁面。
伺服
我們將使用相同的 Go 伺服器來為客戶端提供服務,因此,在我們前面的 main.go
中新增一些路由:
router.Handle("GET", "/...", http.FileServer(SPAFileSystem{http.Dir("static")}))
type SPAFileSystem struct {
fs http.FileSystem
}
func (spa SPAFileSystem) Open(name string) (http.File, error) {
f, err := spa.fs.Open(name)
if err != nil {
return spa.fs.Open("index.html")
}
return f, nil
}
這個伺服檔案放在 static
下,配合 static/index.html
作為回撥。
你可以使用你自己的伺服器,但是你得在伺服器上啟用 CORS。
HTML
我們來看一下那個 static/index.html
檔案。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Passwordless Demo</title>
<link rel="shortcut icon" href="data:,">
<script src="/js/main.js" type="module"></script>
</head>
<body></body>
</html>
單頁面應用程式的所有渲染由 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 <token_here>
頭到請求中;這樣伺服器就能對我們進行身份驗證。
歡迎頁
我們現在來到歡迎頁面。用如下的內容建立一個 static/js/pages/welcome-page.js
檔案:
const template = document.createElement('template')
template.innerHTML = `
<h1>Passwordless Demo</h1>
<h2>Access</h2>
<form id="access-form">
<input type="email" placeholder="Email" autofocus required>
<button type="submit">Send Magic Link</button>
</form>
`
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 = `
<h1>Authenticating you</h1>
`
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 = `
<h1>Passwordless Demo</h1>
<p>Welcome back, ${authUser.username}
相關文章
- 客戶端身份驗證客戶端
- 客戶端驗證和onclick事件控制程式碼客戶端事件
- 賬戶、密碼格式···正則驗證密碼
- validation客戶端驗證框架使用手冊客戶端框架
- 如果呼叫遠端遠端url介面為https,且存在客戶端證書驗證,如何在客戶端處理HTTP客戶端
- mvc 客戶端 驗證 失效 沒有 form 標籤MVC客戶端ORM
- MySQL的可插入驗證和客戶端明文驗證外掛介紹MySql客戶端
- iOS逆向-客戶端認證iOS客戶端
- 物理DataGuard客戶端無縫切換--客戶端TAF 配置客戶端
- 批量域更改客戶端本地administrator密碼客戶端密碼
- 【Azure 應用服務】應用程式碼需要客戶端證書進行驗證,部署到App Service後,如何配置讓客戶端攜帶證書呢?客戶端APP
- [WCF安全系列]談談WCF的客戶端認證[使用者名稱/密碼認證]客戶端密碼
- Identity Server 4 - Hybrid Flow - MVC客戶端身份驗證IDEServerMVC客戶端
- 客戶端如何刪除根證書客戶端
- 設定客戶端連線PostgreSQL不需要密碼客戶端SQL密碼
- Winform客戶端引用WCF客戶端後,部分類無法正常使用ORM客戶端
- plsql 客戶端亂碼SQL客戶端
- MVC驗證10-到底用哪種方式實現客戶端服務端雙重非同步驗證MVC客戶端服務端非同步
- 微軟賬戶將支援無密碼身份認證登入微軟密碼
- JAX-RSREST客戶端實現基本身份驗證機制REST客戶端
- 無密碼身份驗證:安全、簡單且部署快速密碼
- SSH無密碼驗證可能出現的問題密碼
- Oracle 密碼驗證方式Oracle密碼
- 【DATAGUARD】物理dg配置客戶端無縫切換 (八.3)--客戶端TAF 配置客戶端
- GitHub Windows客戶端無法登入GithubWindows客戶端
- Kubernetes客戶端認證(三)—— Kubernetes使用CertificateSigningRequest方式簽發客戶端證書客戶端
- ORACLE 密碼驗證函式Oracle密碼函式
- 基於CFSSL工具建立CA證書,服務端證書,客戶端證書服務端客戶端
- ACCESS 密碼驗證/文字驗證中的小坑密碼
- 使用WebService釋出soap介面,並實現客戶端的https驗證Web客戶端HTTP
- Java OAuth 2.0 客戶端程式設計(三):認證碼授權JavaOAuth客戶端程式設計
- .net客戶端呼叫activeMQ程式碼客戶端MQ
- 配置郵件客戶端(無SSL/TLS加密)客戶端TLS加密
- 完成ssh無密碼的驗證,採用公鑰、私鑰密碼
- 無密碼身份驗證如何保障使用者隱私安全?密碼
- 無密碼身份認證,跟密碼說再見!密碼
- 三分鐘讀懂客戶端證書客戶端
- Nebula Graph 原始碼解讀系列|客戶端的通訊祕密——fbthrift原始碼客戶端