From 84f97712778e1d776a00d35d9d920dff890bea84 Mon Sep 17 00:00:00 2001 From: "Xingyu.Wang" Date: Thu, 12 Jul 2018 11:40:23 +0800 Subject: [PATCH] PRF:20180429 Passwordless Auth- Client.md @qhwdw --- .../20180429 Passwordless Auth- Client.md | 321 ++++++++++-------- 1 file changed, 171 insertions(+), 150 deletions(-) diff --git a/translated/tech/20180429 Passwordless Auth- Client.md b/translated/tech/20180429 Passwordless Auth- Client.md index e2faf2cb6e..0a14e7a871 100644 --- a/translated/tech/20180429 Passwordless Auth- Client.md +++ b/translated/tech/20180429 Passwordless Auth- Client.md @@ -1,27 +1,49 @@ 无密码验证:客户端 ====== + 我们继续 [无密码验证][1] 的文章。上一篇文章中,我们用 Go 写了一个 HTTP 服务,用这个服务来做无密码验证 API。今天,我们为它再写一个 JavaScript 客户端。 我们将使用 [这里的][2] 这个单页面应用程序(SPA)来展示使用的技术。如果你还没有读过它,请先读它。 -我们将根据验证的状态分别使用两个不同的根 URL(`/`):一个是访问状态的页面或者是欢迎已验证用户的页面。另一个页面是验证失败后重定向到验证页面。 +记住流程: -### Serving +- 用户输入其 email。 +- 用户收到一个带有魔法链接的邮件。 +- 用户点击该链接、 +- 用户验证成功。 + +对于根 URL(`/`),我们将根据验证的状态分别使用两个不同的页面:一个是带有访问表单的页面,或者是已验证通过的用户的欢迎页面。另一个页面是验证回调的重定向页面。 + +### 伺服 我们将使用相同的 Go 服务器来为客户端提供服务,因此,在我们前面的 `main.go` 中添加一些路由: -``` -router.Handle("GET", "/js/", http.FileServer(http.Dir("static"))) -router.HandleFunc("GET", "/...", serveFile("static/index.html")) +``` +router.Handle("GET", "/...", http.FileServer(SPAFileSystem{http.Dir("static")})) ``` -这个伺服文件在 `static/js` 下,而 `static/index.html` 文件是为所有的访问提供服务的。 +``` +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][3]。 ### HTML 我们来看一下那个 `static/index.html` 文件。 + ``` @@ -34,16 +56,16 @@ router.HandleFunc("GET", "/...", serveFile("static/index.html")) - ``` -单页面应用程序剩余的渲染由 JavaScript 来完成,因此,我们使用了一个空的 body 部分和一个 `main.js` 文件。 +单页面应用程序的所有渲染由 JavaScript 来完成,因此,我们使用了一个空的 body 部分和一个 `main.js` 文件。 我们将使用 [上篇文章][2] 中的 Router。 -### Rendering +### 渲染 现在,我们使用下面的内容来创建一个 `static/js/main.js` 文件: + ``` import Router from 'https://unpkg.com/@nicolasparada/router' import { isAuthenticated } from './auth.js' @@ -55,269 +77,268 @@ router.handle('/callback', view('callback')) router.handle(/^\//, view('not-found')) router.install(async resultPromise => { - document.body.innerHTML = '' - document.body.appendChild(await 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)) + 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) + return (...args) => isAuthenticated() + ? fn1(...args) + : fn2(...args) } - ``` -与上篇文章不同的是,我们实现了一个 `isAuthenticated()` 函数和一个 `guard()` 函数,使用它去渲染两种验证状态的页面。因此,当用户访问 `/` 时,它将根据用户是否通过了验证来展示 home 页面或者是欢迎页面。 +与上篇文章不同的是,我们实现了一个 `isAuthenticated()` 函数和一个 `guard()` 函数,使用它去渲染两种验证状态的页面。因此,当用户访问 `/` 时,它将根据用户是否通过了验证来展示主页或者是欢迎页面。 -### Auth +### 验证 现在,我们来编写 `isAuthenticated()` 函数。使用下面的内容来创建一个 `static/js/auth.js` 文件: + ``` export function getAuthUser() { - const authUserItem = localStorage.getItem('auth_user') - const expiresAtItem = localStorage.getItem('expires_at') + const authUserItem = localStorage.getItem('auth_user') + const expiresAtItem = localStorage.getItem('expires_at') - if (authUserItem !== null && expiresAtItem !== null) { - const expiresAt = new Date(expiresAtItem) + if (authUserItem !== null && expiresAtItem !== null) { + const expiresAt = new Date(expiresAtItem) - if (!isNaN(expiresAt.valueOf()) && expiresAt > new Date()) { - try { - return JSON.parse(authUserItem) - } catch (_) { } - } - } + if (!isNaN(expiresAt.valueOf()) && expiresAt > new Date()) { + try { + return JSON.parse(authUserItem) + } catch (_) { } + } + } - return null + return null } export function isAuthenticated() { - return localStorage.getItem('jwt') !== null && getAuthUser() !== null + return localStorage.getItem('jwt') !== null && getAuthUser() !== null } - ``` -当有人登入时,我们将保存 JSON 格式的 web 令牌、过期日期、以及在 `localStorage` 上的当前已验证用户。这个模块就是这个用处。 +当有人登入时,我们将保存 JSON 格式的 web 令牌、它的过期日期,以及在 `localStorage` 上的当前已验证用户。这个模块就是这个用处。 * `getAuthUser()` 用于从 `localStorage` 获取已认证的用户,以确认 JSON 格式的 Web 令牌没有过期。 - * `isAuthenticated()` 在前面的函数中用于去检查它是否返回了 `null`。 + * `isAuthenticated()` 在前面的函数中用于去检查它是否没有返回 `null`。 - - -### Fetch +### 获取 在继续这个页面之前,我将写一些与服务器 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) + 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) + 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')}` } - : {} + return isAuthenticated() + ? { authorization: `Bearer ${localStorage.getItem('jwt')}` } + : {} } export async function handleResponse(res) { - const body = await res.clone().json().catch(() => res.text()) - const response = { - url: res.url, - statusCode: res.status, - statusText: res.statusText, - headers: res.headers, - body, - } - if (!res.ok) throw Object.assign( - new Error(body.message || body || res.statusText), - response - ) - return response + 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, } - ``` 这个模块导出了 `get()` 和 `post()` 函数。它们是 `fetch` API 的封装。当用户是已验证的,这二个函数注入一个 `Authorization: Bearer ` 头到请求中;这样服务器就能对我们进行身份验证。 -### Welcome Page +### 欢迎页 我们现在来到欢迎页面。用如下的内容创建一个 `static/js/pages/welcome-page.js` 文件: + ``` const template = document.createElement('template') template.innerHTML = ` -

Passwordless Demo

-

Access

-
- - -
+

Passwordless Demo

+

Access

+
+ + +
` export default function welcomePage() { - const page = template.content.cloneNode(true) + const page = template.content.cloneNode(true) - page.getElementById('access-form') - .addEventListener('submit', onAccessFormSubmit) + page.getElementById('access-form') + .addEventListener('submit', onAccessFormSubmit) - return page + return page } - ``` -正如你所见,这个页面使用一个 `HTMLTemplateElement`。这是一个只输入用户 email 的简单表格。 +这个页面使用一个 `HTMLTemplateElement` 作为视图。这只是一个输入用户 email 的简单表单。 -为了不让代码太乏味,我将跳过错误处理部分,只是将它们输出到控制台上。 +为了避免干扰,我将跳过错误处理部分,只是将它们输出到控制台上。 现在,我们来写 `onAccessFormSubmit()` 函数。 + ``` import http from '../http.js' function onAccessFormSubmit(ev) { - ev.preventDefault() + ev.preventDefault() - const form = ev.currentTarget - const input = form.querySelector('input') - const email = input.value + 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) - } - }) + 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.') - }) + 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?') + return prompt('No user found. Do you want to create an account?') } - ``` -它使用 email 做了一个 `POST` 请求到 `/api/passwordless/start`,然后在 body 中做了 URI 转向。在本例中使用 `404 Not Found` 状态码返回,我们将创建一个用户。 +它对 `/api/passwordless/start` 发起了 POST 请求,请求体中包含 `email` 和 `redirectUri`。在本例中它返回 `404 Not Found` 状态码时,我们将创建一个用户。 + ``` function runCreateUserProgram(email) { - const username = prompt("Enter username") - if (username === null) return + 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) + http.post('/api/users', { email, username }) + .then(res => res.body) + .then(user => sendMagicLink(user.email)) + .catch(console.error) } - ``` -这个用户创建程序,首先询问用户名,然后使用 email 和用户名,在 body 中做一个 `POST` 请求到 `/api/users`。成功之后,给创建的用户发送一个魔法链接。 +这个用户创建程序,首先询问用户名,然后使用 email 和用户名做一个 `POST` 请求到 `/api/users`。成功之后,给创建的用户发送一个魔法链接。 -### Callback Page +### 回调页 + +这是访问表单的全部功能,现在我们来做回调页面。使用如下的内容来创建一个 `static/js/pages/callback-page.js` 文件: -这就是访问表格的所有功能,现在我们来做回调页面。使用如下的内容来创建一个 `static/js/pages/callback-page.js` 文件: ``` import http from '../http.js' const template = document.createElement('template') template.innerHTML = ` -

Authenticating you 👀

+

Authenticating you

` export default function callbackPage() { - const page = template.content.cloneNode(true) + 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') + 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) + 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) + location.replace('/') + }) + .catch(console.error) - return page + return page } - ``` -请记住 … 当点击魔法链接时,我们来到 `/api/passwordless/verify_redirect`,我们通过 (`/callback`)在 URL 的哈希中传递 JWT 和过期日期,将我们转向到重定向 URI。 +请记住……当点击魔法链接时,我们会来到 `/api/passwordless/verify_redirect`,它将把我们重定向到重定向 URI,我们将放在哈希中的 JWT 和过期日期传递给 `/callback`。 回调页面解码 URL 中的哈希,提取这些参数去做一个 `GET` 请求到 `/api/auth_user`,用 JWT 保存所有数据到 `localStorage` 中。最后,重定向到主页面。 -### Home Page +### 主页 创建如下内容的 `static/pages/home-page.js` 文件: + ``` import { getAuthUser } from '../auth.js' export default function homePage() { - const authUser = getAuthUser() + const authUser = getAuthUser() - const template = document.createElement('template') - template.innerHTML = ` -

Passwordless Demo

-

Welcome back, ${authUser.username} 👋

- - ` + const template = document.createElement('template') + template.innerHTML = ` +

Passwordless Demo

+

Welcome back, ${authUser.username} 👋

+ + ` - const page = template.content + const page = template.content - page.getElementById('logout-button') - .addEventListener('click', logout) + page.getElementById('logout-button') + .addEventListener('click', logout) - return page + return page } function logout() { - localStorage.clear() - location.reload() + localStorage.clear() + location.reload() } - ``` -这个页面欢迎已验证用户,同时也有一个登出按钮。`logout()` 函数的功能只是清理掉 `localStorage` 并重载这个页面。 +这个页面用于欢迎已验证用户,同时也有一个登出按钮。`logout()` 函数的功能只是清理掉 `localStorage` 并重载这个页面。 -这就是全部内容了。我敢说你在此之前已经看过这个 [demo][4] 了。当然,这些源代码也在同一个 [仓库][5] 中。 - -👋👋👋 +这就是全部内容了。我猜你在此之前已经看过这个 [demo][4] 了。当然,这些源代码也在同一个 [仓库][5] 中。 -------------------------------------------------------------------------------- @@ -326,13 +347,13 @@ via: https://nicolasparada.netlify.com/posts/passwordless-auth-client/ 作者:[Nicolás Parada][a] 选题:[lujun9972](https://github.com/lujun9972) 译者:[qhwdw](https://github.com/qhwdw) -校对:[校对者ID](https://github.com/校对者ID) +校对:[wxy](https://github.com/wxy) 本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出 [a]:https://nicolasparada.netlify.com/ -[1]:https://nicolasparada.netlify.com/posts/passwordless-auth-server/ -[2]:https://nicolasparada.netlify.com/posts/javascript-client-router/ +[1]:https://linux.cn/article-9748-1.html +[2]:https://linux.cn/article-9815-1.html [3]:https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS [4]:https://go-passwordless-demo.herokuapp.com/ [5]:https://github.com/nicolasparada/go-passwordless-demo