微软发布 Azure Linux 正式版在内部使用 Azure Linux 两年，并从 2022 年 10 月开始公开预览后，微软本周终于正式提供其 Azure Linux。Azure Linux 是 Azure Kubernetes 服务（AKS）的一个开源容器主机操作系统，它针对 Azure 进行了优化，旨在使开发人员更容易使用微软的工具来部署和管理容器工作负载。Azure Linux 源于微软的 CBL-Mariner 项目，是“自我独立的 Linux 发行版”，而非衍生自 Ubuntu 或 Fedora。消息来源：The Register老王点评：这不是微软的第一个 Linux 发行版，也不是最后一个发行版。使用 XFS 的系统暂时不要升级到 Linux 6.3上周以来，多个用户已经报告了在升级到 Linux 6.3 稳定内核时，XFS 文件系统的元数据损坏问题。当他们在基于 XFS 的服务器上运行 Linux 6.3 时，服务器不断崩溃。据报道，Linux 6.4 内核对一些受影响的用户来说工作正常，这让人觉得一些补丁可能没有很好地回传到较新的 Linux 6.3 的小版本。消息来源：Phoronix老王点评：这比较罕见，无论是 Linux 稳定版，还是文件系统，出现这样的问题太不应该了。或许我们该担忧 Linux 内核的质量把控了？当然，谨慎的系统管理员从来在更新上都是“懒惰”的。Bitwarden 进军无密码安全领域可悲而简单的事实是，密码泄露正变得像人们闯红灯一样普遍。密码机制已经过时，大约一半的 IT 决策者认为他们使用的应用程序中缺乏无密码设计是主要原因。而无密码技术中的 密码钥匙 Passkey 可以通过设备上已有的密码、指纹、面部识别等，来替代用户的账户密码登录其它服务。它不仅消除了对密码、用户名和双因素认证（2FA）的需求，而且还通过减轻钓鱼攻击的风险来增强用户的安全性。流行的开源密码管理程序 Bitwarden 推出了 Passwordless.dev，这是一个开发者工具包，用于将无密码技术集成到网站和应用程序中。消息来源：The Newstack老王点评：作为著名的密码管理软件，面对无密码时代的来临，也不得不“与时俱进”。
我们继续 无密码验证 的文章。上一篇文章中，我们用 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 文件。 Passwordless Demo 单页面应用程序的所有渲染由 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 = ` Passwordless Demo Access Send Magic Link ` 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}