标签 登录 下的文章

微软将取消 Windows 中的第三方打印机驱动程序

微软计划于 2027 年取消 Windows 中的第三方打印机驱动程序。这样做的部分原因是,Mopria 联盟为打印机提供了通用标准。该联盟由多家打印机厂商于 2013 年成立。从 Windows 10 21H2 开始,微软就在其操作系统中加入了对 Mopria 的支持,借助微软 IPP 类驱动程序,可支持通过网络或 USB 连接的设备。微软表示:“这样一来,打印设备制造商就无需提供自己的安装程序、驱动程序、实用程序等”。

消息来源:The Register
老王点评:这种通用外设标准化是一件好事。除了一些非常老旧的打印机,应该没什么影响。

获得艺术大奖的著名 AI 绘画未能获得版权

还记得那副获得 2022 年科罗拉多州博览会年度美术竞赛的《 空间歌剧院 Théâtre d'Opéra Spatial 》的 AI 绘画吗?它是艺术家使用 MidJourney AI 生成的,以其复古的笔触细腻的色彩,意外地赢得了艺术创作比赛的第一名。这件事引发了公众对 AI 绘画和 MidJourney 的极大兴趣。不出意外,美国版权局也拒绝了对该作品的版权授予,“因为它不是人类创作的产物……其中包含的人工智能创作素材超过了最低限度”。使用 MidJourney 创作这幅画的 Jason Allen 告诉版权局,他使用 Midjourney “输入了大量修改和文本提示,至少 624 次,才得到了图片的初始版本”,并用 Photoshop 对其进行了修改。

消息来源:路透社
老王点评:不授予版权有不授予的道理,我认为 AI 制品应该有不同于现有版权范围的新的版权认定。

微软决定由它来选择你使用的安全登录方式

微软本周将推出多因素身份验证(MFA)系统首选验证,它将为登录的个人选择最安全的方法,如果该方法不可用,则提供替代方法。Azure 活动目录会查看用户账户注册的身份验证方法,并选择最安全的途径。首选方法列表从临时访问通行证开始,然后依次是基于证书的身份验证、FIDO2 安全密钥、微软认证器的推送通知和基于时间的一次性密码(OTP),最后是手机。微软的总体目标是最终放弃使用用户名和密码作为身份验证方法,转而使用生物识别等其他方法。

消息来源:The Register
老王点评:总之,就是大家都习惯的密码是最不可靠的安全方式。

回音

  • 果然,Bcachefs 没有进入 Linux 6.6,并按照 Linus Torvalds 的意见,先行 并入 了 linux-next 树,它有望进入下一个版本的 Linux 内核。

随着在设备上使用各种程序的用户的需求变得越来越复杂,程序本身也需要跟上用户的现实需求和未来期望。

我发现我每天需要的东西是一个在网页浏览器保持登录多个账号的简单方法。我 可以 根据需要对我的每个账号进行登录和注销操作,但在短时间内切换多个账号时,这变得非常乏味。

最初,我使用谷歌浏览器,它拥有管理多个帐户的能力。这很有效,但管理起来略显繁琐,而且明明只需 一个 谷歌账号就能搞定的事却要创建一个新的谷歌账号来完成,这显得有点儿笨拙。

这是我转而使用 Firefox 多账户容器 Multi-Account Containers 功能的原因。它不仅比我在谷歌 Chrome 浏览器上的设置灵活得多,而且我还使用了由我的浏览器开发者自己创建的工具,从而在整体上获得了更流畅和更简单的体验。

Firefox 中的容器图示

Firefox 中的多帐户容器是什么?

如果你想将数字生活的各个部分彼此分开,多账户容器也非常有效。通过使用容器,你在一个容器中的浏览活动不会与其他容器共享。这种隔离意味着你可以在不同容器中登录同一网站上的两个不同帐户。你的登录会话、网站偏好和跟踪数据将被限制在你使用某个网站的容器中。

它还有什么其他优势?想象一下,你在亚马逊或其他电子商务网站上购物。你浏览了一些商品,但没有购买任何东西。现在,如果你浏览网络,你会看到与你浏览的产品相关的广告。尽管有广告拦截器,一些网站仍会显示广告。使用容器,你可以将你的购物网站与其他网站分开。(LCTT 校注:甚至根据你的浏览历史,你再次访问同一网站时看到的价格可能会被“宰熟”——反复浏览代表了你的购买倾向。)

再给大家分享一个例子。Firefox 默认提供一个 Facebook 容器。默认情况下,此容器包括 Facebook、Messenger 和 Instagram 网站。这意味着当你打开这三个网站中的任何一个时,它们都只会在“Facebook 容器”中打开。因此,Facebook 将无法跟踪你在其他网站上的活动。

这是 很少有人知道或使用的 Firefox 功能 之一。

使用多账户容器

安装 Firefox 多账户容器是一个非常简单的过程,只需点击几下。

首先,前往 Firefox 附加组件网站上的 扩展程序页面。之后你唯一需要做的就是单击 “添加到 Firefox” 按钮。

安装完成!现在我们可以实际使用一下这个新的扩展。

可能你还没有注意到,你的搜索栏右侧应该会出现一个新图标:

这是你将用于与 Firefox 多帐户容器交互的图标。如果你单击该图标,你将看到一个小菜单:

让我们使用这个扩展尝试一些例子,看看多账户容器是如何工作的。

设置容器

首先,我们需要生成一个容器。点击多账户容器菜单中的 ` 管理容器 Manage Containers ,然后点击 新建容器 New Container

接着输入新容器的名称,选择颜色和图标。然后,点击 “OK” 保存新容器。

大功告成!我们现在可以返回主菜单在新容器中打开一个新选项卡:

你还会注意到新选项卡有一些样式,表示它正在容器内运行:

观察容器工作

现在让我们看看容器在使用时实际做了什么。

我们将在一个普通的浏览器选项卡中访问 Linode 管理网站,我已经在其中登录:

现在让我们尝试在 Firefox 容器中打开相同的页面,此时我被重定向到 Linode 登录页面:

为什么我被重定向了?因为现在我没有登录。这就是 Firefox 容器的乐趣之一:在一个浏览器会话中登录后,再进入一个容器,就好像你以前从未访问过该站点一样。

如果你在容器内完成对某个网站的登录,你从容器中访问该网站时将会保持登录状态。你还可以使用此功能从容器内登录网站,从而使该网站的所有数据与你的正常浏览器数据相隔开。

注意:你的浏览器历史记录本身之类的内容仍会暴露给你的正常浏览器会话。容器功能只是提供了一种方法来分离本文中提到的登录帐户等内容。

总结

对于那些在乎自己的隐私,或者只是想真正尝试对其系统的安全性进行严格控制的人来说,多账户容器被证明是一个很棒的功能。

例如,你可以在容器内登录你的谷歌帐户,谷歌永远不会知悉你在容器外的信息。 对拥有多个帐户的人来说,此扩展程序是一个不错的选择。有了它无需为你要使用的每样东西创建单独的浏览器帐户。

好了,这就是 Firefox 的多帐户容器的基本知识。

需要任何帮助,或者只是有点问题?请随时在评论区指出。


via: https://itsfoss.com/firefox-containers/

作者:Hunter Wittenborn 选题:lujun9972 译者:hanszhao80 校对:校对者ID

本文由 LCTT 原创编译,Linux中国 荣誉推出

现在很多高端笔记本都配备了指纹识别器。Windows 和 macOS 支持指纹登录已经有一段时间了。在桌面 Linux 中,对指纹登录的支持更多需要极客的调整,但 GNOMEKDE 已经开始通过系统设置来支持它。

这意味着在新的 Linux 发行版上,你可以轻松使用指纹识别。在这里我将在 Ubuntu 中启用指纹登录,但你也可以在其他运行 GNOME 3.38 的发行版上使用这些步骤。

前提条件

当然,这是显而易见的。你的电脑必须有一个指纹识别器。

这个方法适用于任何运行 GNOME 3.38 或更高版本的 Linux 发行版。如果你不确定,你可以检查你使用的桌面环境版本

KDE 5.21 也有一个指纹管理器。当然,截图看起来会有所不同。

在 Ubuntu 和其他 Linux 发行版中添加指纹登录功能

进入 “设置”,然后点击左边栏的 “用户”。你应该可以看到系统中所有的用户账号。你会看到几个选项,包括 “指纹登录”。

点击启用这里的指纹登录选项。

Enable fingerprint login in Ubuntu

它将立即要求你扫描一个新的指纹。当你点击 “+” 号来添加指纹时,它会提供一些预定义的选项,这样你就可以很容易地识别出它是哪根手指或拇指。

当然,你可以点击右手食指但扫描左手拇指,不过我看不出你有什么好的理由要这么做。

Adding fingerprint

在添加指纹时,请按照指示旋转你的手指或拇指。

Rotate your finger

系统登记了整个手指后,就会给你一个绿色的信号,表示已经添加了指纹。

Fingerprint successfully added

如果你想马上测试一下,在 Ubuntu 中按 Super+L 快捷键锁定屏幕,然后使用指纹进行登录。

Login With Fingerprint in Ubuntu

在 Ubuntu 上使用指纹登录的经验

指纹登录顾名思义就是使用你的指纹来登录系统。就是这样。当要求对需要 sudo 访问的程序进行认证时,你不能使用手指。它不能代替你的密码。

还有一件事。指纹登录可以让你登录,但当系统要求输入 sudo 密码时,你不能用手指。Ubuntu 中的 钥匙环 也仍然是锁定的。

另一件烦人的事情是因为 GNOME 的 GDM 登录界面。当你登录时,你必须先点击你的账户才能进入密码界面。你在这可以使用手指。如果能省去先点击用户帐户 ID 的麻烦就更好了。

我还注意到,指纹识别没有 Windows 中那么流畅和快速。不过,它可以使用。

如果你对 Linux 上的指纹登录有些失望,你可以禁用它。让我在下一节告诉你步骤。

禁用指纹登录

禁用指纹登录和最初启用指纹登录差不多。

进入 “设置→用户”,然后点击指纹登录选项。它会显示一个有添加更多指纹或删除现有指纹的页面。你需要删除现有的指纹。

Disable Fingerprint Login

指纹登录确实有一些好处,特别是对于我这种懒人来说。我不用每次锁屏时输入密码,我也对这种有限的使用感到满意。

PAM 启用指纹解锁 sudo 应该不是完全不可能。我记得我 在 Ubuntu 中设置脸部解锁时,也可以用于 sudo。看看以后的版本是否会增加这个功能吧。

你有带指纹识别器的笔记本吗?你是否经常使用它,或者它只是你不关心的东西之一?


via: https://itsfoss.com/fingerprint-login-ubuntu/

作者:Abhishek Prakash 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

介绍

今天,Fedora 提供了多种方式来提高我们账户的身份认证的安全性。当然,它有我们熟悉的用户名密码登录方式,它也同样提供了其他的身份认证选项,比如生物识别、指纹、智能卡、一次性密码,甚至是 询问-响应 challenge-response 身份认证。

每种认证方式都有明确的优缺点。这点本身就可以成为一篇相当冗长的文章的主题。Fedora 杂志之前就已经介绍过了这其中的一些选项:

在现在的 Fedora 版本中,最安全的方法之一就是离线硬件询问-响应。它也同样是最容易部署的方法之一。下面是具体方法。

询问-响应认证

从技术上来讲,当你输入密码的时候,你就正在响应用户名询问。离线的询问、响应包含了这些部分:首先是需要你的用户名,接下来,Fedora 会要你提供一个加密的物理硬件的令牌。令牌会把另一个其存储的加密密钥通过 可插入式身份认证 Pluggable Authentication Module 模块(PAM)框架来响应询问。最后,Fedora 才会提示你输入密码。这可以防止其他人仅仅使用了找到的硬件令牌,或是只使用了账户名密码而没有正确的加密密钥。

这意味着除了你的账户名密码之外,你必须事先在你的操作系统中注册了一个或多个加密硬件令牌。你必须保证你的物理硬件令牌能够匹配你的用户名。

一些询问-响应的方法,比如一次性密码(OTP),在硬件令牌上获取加密的代码密钥,然后将这个密钥通过网络传输到远程身份认证服务器。然后这个服务器会告诉 Fedora 的 PAM 框架,这是否是该用户的一个有效令牌。如果身份认证服务器在本地网络上,这个方法非常好。但它的缺点是如果网络连接断开或是你在没有网的远程端工作。你会被锁在系统之外,直到你能通过网络连接到身份认证服务器。

有时候,生产环境会采用通过 Yubikey 使用一次性密码(OTP)的设置,然而,在家庭或个人的系统上,你可能更喜欢询问-响应设置。一切都是本地的,这种方法不需要通过远程网络调用。下面这些过程适用于 Fedora 27、28 和 29.

准备

硬件令牌密钥

首先,你需要一个安全的硬件令牌密钥。具体来说,这个过程需要一个 Yubikey 4、Yubikey NEO,或者是最近发布的、同样支持 FIDO2 的 Yubikey 5 系列设备。你应该购买它们中的两个,一个做备份,以避免其中一个丢失或遭到损坏。你可以在不同的工作地点使用这些密钥。较为简单的 FIDO 和 FIDO U2F 版本不适用于这个过程,但是非常适合使用 FIDO 的在线服务。

备份、备份,以及备份

接下来,为你所有的重要数据制作备份,你可能想在克隆在 VM 里的 Fedora 27/28/29 里测试配置,来确保你在设置你自己的个人工作环境之前理解这个过程。

升级,然后安装

现在,确定你的 Fedora 是最新的,然后通过 dnf 命令安装所需要的 Fedora Yubikey 包。

$ sudo dnf upgrade
$ sudo dnf install ykclient* ykpers* pam_yubico*

如果你使用的是 VM 环境,例如 Virtual Box,确保 Yubikey 设备已经插进了 USB 口,然后允许 VM 控制的 USB 访问 Yubikey。

配置 Yubikey

确认你的账户访问到了 USB Yubikey:

$ ykinfo -v
version: 3.5.0

如果 Yubikey 没有被检测到,会出现下面这些错误信息:

Yubikey core error: no yubikey present

接下来,通过下面这些 ykpersonalize 命令初始化你每个新的 Yubikey。这将设置 Yubikey 配置插槽 2 使用 HMAC-SHA1 算法(即使少于 64 个字符)进行询问响应。如果你已经为询问响应设置好了你的 Yubikey。你就不需要再次运行 ykpersonalize 了。

ykpersonalize -2 -ochal-resp -ochal-hmac -ohmac-lt64 -oserial-api-visible

一些用户在使用的时候将 YubiKey 留在了他们的工作站上,甚至用于对虚拟机进行询问-响应。然而,为了更好的安全性,你可能会更愿意使用手动触发 YubiKey 来响应询问。

要添加手动询问按钮触发器,请添加 -ochal-btn-trig 选项,这个选项可以使得 Yubikey 在请求中闪烁其 LED。等待你在 15 秒内按下硬件密钥区域上的按钮来生成响应密钥。

$ ykpersonalize -2 -ochal-resp -ochal-hmac -ohmac-lt64 -ochal-btn-trig -oserial-api-visible

为你的每个新的硬件密钥执行此操作。每个密钥执行一次。完成编程之后,使用下面的命令将 Yubikey 配置存储到 ~/.yubico

$ ykpamcfg -2 -v
debug: util.c:222 (check_firmware_version): YubiKey Firmware version: 4.3.4

Sending 63 bytes HMAC challenge to slot 2
Sending 63 bytes HMAC challenge to slot 2
Stored initial challenge and expected response in '/home/chuckfinley/.yubico/challenge-9992567'.

如果你要设置多个密钥用于备份,请将所有的密钥设置为相同,然后使用 ykpamcfg 工具存储每个密钥的询问-响应。如果你在一个已经存在的注册密钥上运行 ykpersonalize 命令,你就必须再次存储配置信息。

配置 /etc/pam.d/sudo

现在要去验证配置是否有效,在同一个终端窗口中,你需要设置 sudo 来要求使用 Yubikey 的询问-响应。将下面这几行插入到 /etc/pam.d/sudo 文件中。

auth required pam_yubico.so mode=challenge-response

将上面的 auth 行插入到文件中的 auth include system-auth 行的上面,然后保存并退出编辑器。在默认的 Fedora 29 设置中,/etc/pam.d/sudo 应该像下面这样:

#%PAM-1.0
auth required pam_yubico.so mode=challenge-response
auth include system-auth
account include system-auth
password include system-auth
session optional pam_keyinit.so revoke
session required pam_limits.so
session include system-auth

保持这个初始的终端窗口打开,然后打开一个新的终端窗口进行测试,在新的终端窗口中输入:

$ sudo echo testing

你应该注意到了 Yubikey 上的 LED 在闪烁。点击 Yubikey 按钮,你应该会看见一个输入 sudo 密码的提示。在你输入你的密码之后,你应该会在终端屏幕上看见 “testing” 的字样。

现在去测试确保失败也正常,启动另一个终端窗口,并从 USB 插口中拔掉 Yubikey。使用下面这条命令验证,在没有 Yubikey 的情况下,sudo 是否会不再正常工作。

$ sudo echo testing fail

你应该立刻被提示输入 sudo 密码,但即使你输入了正确密码,登录也应该失败。

设置 Gnome 桌面管理器(GDM)

一旦你的测试完成后,你就可以为图形登录添加询问-响应支持了。将你的 Yubikey 再次插入进 USB 插口中。然后将下面这几行添加到 /etc/pam.d/gdm-password 文件中:

auth required pam_yubico.so mode=challenge-response

打开一个终端窗口,然后运行下面这些命令。如果需要,你可以使用其他的编辑器:

$ sudo vi /etc/pam.d/gdm-password

你应该看到 Yubikey 上的 LED 在闪烁,按下 Yubikey 按钮,然后在提示符处输入密码。

修改 /etc/pam.d/gdm-password 文件,在已有的 auth substack password-auth 行上添加新的行。这个文件的顶部应该像下面这样:

auth [success=done ignore=ignore default=bad] pam_selinux_permit.so
auth required pam_yubico.so mode=challenge-response
auth substack password-auth
auth optional pam_gnome_keyring.so
auth include postlogin

account required pam_nologin.so

保存更改并退出编辑器,如果你使用的是 vi,输入键是按 Esc 键,然后在提示符处输入 wq! 来保存并退出。

结论

现在注销 GNOME。将 Yubikey 插入到 USB 口,在图形登录界面上点击你的用户名。Yubikey LED 会开始闪烁。触摸那个按钮,你会被提示输入你的密码。

如果你丢失了 Yubikey,除了重置密码之外,你还可以使用备份的 Yubikey。你还可以给你的账户增加额外的 Yubikey 配置。

如果有其他人获得了你的密码,他们在没有你的物理硬件 Yubikey 的情况下,仍然不能登录。恭喜!你已经显著提高了你的工作环境登录的安全性了。


via: https://fedoramagazine.org/login-challenge-response-authentication/

作者:nabooengineer 选题:lujun9972 译者:hopefully2333 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

无密码验证可以让你只输入一个 email 而无需输入密码即可登入系统。这是一种比传统的电子邮件/密码验证方式登入更安全的方法。

下面我将为你展示,如何在 Go 中实现一个 HTTP API 去提供这种服务。

流程

  • 用户输入他的电子邮件地址。
  • 服务器创建一个临时的一次性使用的代码(就像一个临时密码一样)关联到用户,然后给用户邮箱中发送一个“魔法链接”。
  • 用户点击魔法链接。
  • 服务器提取魔法链接中的代码,获取关联的用户,并且使用一个新的 JWT 重定向到客户端。
  • 在每次有新请求时,客户端使用 JWT 去验证用户。

必需条件

  • 数据库:我们为这个服务使用了一个叫 CockroachDB 的 SQL 数据库。它非常像 postgres,但它是用 Go 写的。
  • SMTP 服务器:我们将使用一个第三方的邮件服务器去发送邮件。开发的时我们使用 mailtrap。Mailtrap 发送所有的邮件到它的收件箱,因此,你在测试时不需要创建多个假邮件帐户。

Go 的主页 上安装它,然后使用 go version(1.10.1 atm)命令去检查它能否正常工作。

CockroachDB 的主页 上下载它,展开它并添加到你的 PATH 变量中。使用 cockroach version(2.0 atm)命令检查它能否正常工作。

数据库模式

现在,我们在 GOPATH 目录下为这个项目创建一个目录,然后使用 cockroach start 启动一个新的 CockroachDB 节点:

cockroach start --insecure --host 127.0.0.1

它会输出一些内容,找到 SQL 地址行,它将显示像 postgresql://[email protected]:26257?sslmode=disable 这样的内容。稍后我们将使用它去连接到数据库。

使用如下的内容去创建一个 schema.sql 文件。

DROP DATABASE IF EXISTS passwordless_demo CASCADE;
CREATE DATABASE IF NOT EXISTS passwordless_demo;
SET DATABASE = passwordless_demo;

CREATE TABLE IF NOT EXISTS users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email STRING UNIQUE,
    username STRING UNIQUE
);

CREATE TABLE IF NOT EXISTS verification_codes (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users ON DELETE CASCADE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

INSERT INTO users (email, username) VALUES
    ('[email protected]', 'john_doe');

这个脚本创建了一个名为 passwordless_demo 的数据库、两个名为 usersverification_codes 的表,以及为了稍后测试而插入的一些假用户。每个验证代码都与用户关联并保存创建时间,以用于去检查验证代码是否过期。

在另外的终端中使用 cockroach sql 命令去运行这个脚本:

cat schema.sql | cockroach sql --insecure

环境配置

需要配置两个环境变量:SMTP_USERNAMESMTP_PASSWORD,你可以从你的 mailtrap 帐户中获得它们。将在我们的程序中用到它们。

Go 依赖

我们需要下列的 Go 包:

go get -u github.com/lib/pq
go get -u github.com/matryer/way
go get -u github.com/dgrijalva/jwt-go

代码

初始化函数

创建 main.go 并且通过 init 函数里的环境变量中取得一些配置来启动。

var config struct {
    port        int
    appURL      *url.URL
    databaseURL string
    jwtKey      []byte
    smtpAddr    string
    smtpAuth    smtp.Auth
}

func init() {
    config.port, _ = strconv.Atoi(env("PORT", "80"))
    config.appURL, _ = url.Parse(env("APP_URL", "http://localhost:"+strconv.Itoa(config.port)+"/"))
    config.databaseURL = env("DATABASE_URL", "postgresql://[email protected]:26257/passwordless_demo?sslmode=disable")
    config.jwtKey = []byte(env("JWT_KEY", "super-duper-secret-key"))
    smtpHost := env("SMTP_HOST", "smtp.mailtrap.io")
    config.smtpAddr = net.JoinHostPort(smtpHost, env("SMTP_PORT", "25"))
    smtpUsername, ok := os.LookupEnv("SMTP_USERNAME")
    if !ok {
        log.Fatalln("could not find SMTP_USERNAME on environment variables")
    }
    smtpPassword, ok := os.LookupEnv("SMTP_PASSWORD")
    if !ok {
        log.Fatalln("could not find SMTP_PASSWORD on environment variables")
    }
    config.smtpAuth = smtp.PlainAuth("", smtpUsername, smtpPassword, smtpHost)
}

func env(key, fallbackValue string) string {
    v, ok := os.LookupEnv(key)
    if !ok {
        return fallbackValue
    }
    return v
}

  • appURL 将去构建我们的 “魔法链接”。
  • port 将要启动的 HTTP 服务器。
  • databaseURL 是 CockroachDB 地址,我添加 /passwordless_demo 前面的数据库地址去表示数据库名字。
  • jwtKey 用于签名 JWT。
  • smtpAddrSMTP_HOST + SMTP_PORT 的联合;我们将使用它去发送邮件。
  • smtpUsernamesmtpPassword 是两个必需的变量。
  • smtpAuth 也是用于发送邮件。

env 函数允许我们去获得环境变量,不存在时返回一个回退值。

主函数

var db *sql.DB

func main() {
    var err error
    if db, err = sql.Open("postgres", config.databaseURL); err != nil {
        log.Fatalf("could not open database connection: %v\n", err)
    }
    defer db.Close()
    if err = db.Ping(); err != nil {
        log.Fatalf("could not ping to database: %v\n", err)
    }

    router := way.NewRouter()
    router.HandleFunc("POST", "/api/users", jsonRequired(createUser))
    router.HandleFunc("POST", "/api/passwordless/start", jsonRequired(passwordlessStart))
    router.HandleFunc("GET", "/api/passwordless/verify_redirect", passwordlessVerifyRedirect)
    router.Handle("GET", "/api/auth_user", authRequired(getAuthUser))

    addr := fmt.Sprintf(":%d", config.port)
    log.Printf("starting server at %s \n", config.appURL)
    log.Fatalf("could not start server: %v\n", http.ListenAndServe(addr, router))
}

首先,打开数据库连接。记得要加载驱动。

import (
    _ "github.com/lib/pq"
)

然后,我们创建路由器并定义一些端点。对于无密码流程来说,我们使用两个端点:/api/passwordless/start 发送魔法链接,和 /api/passwordless/verify_redirect 用 JWT 响应。

最后,我们启动服务器。

你可以创建空处理程序和中间件去测试服务器启动。

func createUser(w http.ResponseWriter, r *http.Request) {
    http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
}

func passwordlessStart(w http.ResponseWriter, r *http.Request) {
    http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
}

func passwordlessVerifyRedirect(w http.ResponseWriter, r *http.Request) {
    http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
}

func getAuthUser(w http.ResponseWriter, r *http.Request) {
    http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
}

func jsonRequired(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        next(w, r)
    }
}

func authRequired(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        next(w, r)
    }
}

接下来:

go build
./passwordless-demo

我们在目录中有了一个 “passwordless-demo”,但是你的目录中可能与示例不一样,go build 将创建一个同名的可执行文件。如果你没有关闭前面的 cockroach 节点,并且你正确配置了 SMTP_USERNAMESMTP_PASSWORD 变量,你将看到命令 starting server at http://localhost/ 没有错误输出。

请求 JSON 的中间件

端点需要从请求体中解码 JSON,因此要确保请求是 application/json 类型。因为它是一个通用的东西,我将它解耦到中间件。

func jsonRequired(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ct := r.Header.Get("Content-Type")
        isJSON := strings.HasPrefix(ct, "application/json")
        if !isJSON {
            respondJSON(w, "JSON body required", http.StatusUnsupportedMediaType)
            return
        }
        next(w, r)
    }
}

实现很容易。首先它从请求头中获得内容的类型,然后检查它是否是以 “application/json” 开始,如果不是则以 415 Unsupported Media Type 提前返回。

响应 JSON 的函数

以 JSON 响应是非常通用的做法,因此我把它提取到函数中。

func respondJSON(w http.ResponseWriter, payload interface{}, code int) {
    switch value := payload.(type) {
    case string:
        payload = map[string]string{"message": value}
    case int:
        payload = map[string]int{"value": value}
    case bool:
        payload = map[string]bool{"result": value}
    }
    b, err := json.Marshal(payload)
    if err != nil {
        respondInternalError(w, fmt.Errorf("could not marshal response payload: %v", err))
        return
    }
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.WriteHeader(code)
    w.Write(b)
}

首先,对原始类型做一个类型判断,并将它们封装到一个 map。然后将它们编组到 JSON,设置响应内容类型和状态码,并写 JSON。如果 JSON 编组失败,则响应一个内部错误。

响应内部错误的函数

respondInternalError 是一个响应 500 Internal Server Error 的函数,但是也同时将错误输出到控制台。

func respondInternalError(w http.ResponseWriter, err error) {
    log.Println(err)
    respondJSON(w,
        http.StatusText(http.StatusInternalServerError),
        http.StatusInternalServerError)
}

创建用户的处理程序

下面开始编写 createUser 处理程序,因为它非常容易并且是 REST 式的。

type User struct {
    ID       string `json:"id"`
    Email    string `json:"email"`
    Username string `json:"username"`
}

User 类型和 users 表相似。

var (
    rxEmail = regexp.MustCompile("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$")
    rxUsername = regexp.MustCompile("^[a-zA-Z][\\w|-]{1,17}$")
)

这些正则表达式是分别用于去验证电子邮件和用户名的。这些都很简单,可以根据你的需要随意去适配。

现在,在 createUser 函数内部,我们将开始解码请求体。

var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
    respondJSON(w, err.Error(), http.StatusBadRequest)
    return
}
defer r.Body.Close()

我们将使用请求体去创建一个 JSON 解码器来解码出一个用户指针。如果发生错误则返回一个 400 Bad Request。不要忘记关闭请求体读取器。

errs := make(map[string]string)
if user.Email == "" {
    errs["email"] = "Email required"
} else if !rxEmail.MatchString(user.Email) {
    errs["email"] = "Invalid email"
}
if user.Username == "" {
    errs["username"] = "Username required"
} else if !rxUsername.MatchString(user.Username) {
    errs["username"] = "Invalid username"
}
if len(errs) != 0 {
    respondJSON(w, errs, http.StatusUnprocessableEntity)
    return
}

这是我如何做验证;一个简单的 map 并检查如果 len(errs) != 0,则使用 422 Unprocessable Entity 去返回。

err := db.QueryRowContext(r.Context(), `
    INSERT INTO users (email, username) VALUES ($1, $2)
    RETURNING id
`, user.Email, user.Username).Scan(&user.ID)

if errPq, ok := err.(*pq.Error); ok && errPq.Code.Name() == "unique_violation" {
    if strings.Contains(errPq.Error(), "email") {
        errs["email"] = "Email taken"
    } else {
        errs["username"] = "Username taken"
    }
    respondJSON(w, errs, http.StatusForbidden)
    return
} else if err != nil {
    respondInternalError(w, fmt.Errorf("could not insert user: %v", err))
    return
}

这个 SQL 查询使用一个给定的 email 和用户名去插入一个新用户,并返回自动生成的 id,每个 $ 将被接下来传递给 QueryRowContext 的参数替换掉。

因为 users 表在 emailusername 字段上有唯一性约束,因此我将检查 “unique\_violation” 错误并返回 403 Forbidden 或者返回一个内部错误。

respondJSON(w, user, http.StatusCreated)

最后使用创建的用户去响应。

无密码验证开始部分的处理程序

type PasswordlessStartRequest struct {
    Email       string `json:"email"`
    RedirectURI string `json:"redirectUri"`
}

这个结构体含有 passwordlessStart 的请求体:希望去登入的用户 email、来自客户端的重定向 URI(这个应用中将使用我们的 API)如:https://frontend.app/callback

var magicLinkTmpl = template.Must(template.ParseFiles("templates/magic-link.html"))

我们将使用 golang 模板引擎去构建邮件,因此需要你在 templates 目录中,用如下的内容创建一个 magic-link.html 文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Magic Link</title>
</head>
<body>
    Click <a href="{{ .MagicLink }}" target="_blank">here</a> to login.
    <br>
    <em>This link expires in 15 minutes and can only be used once.</em>
</body>
</html>

这个模板是给用户发送魔法链接邮件用的。你可以根据你的需要去随意调整它。

现在, 进入 passwordlessStart 函数内部:

var input PasswordlessStartRequest
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
    respondJSON(w, err.Error(), http.StatusBadRequest)
    return
}
defer r.Body.Close()

首先,我们像前面一样解码请求体。

errs := make(map[string]string)
if input.Email == "" {
    errs["email"] = "Email required"
} else if !rxEmail.MatchString(input.Email) {
    errs["email"] = "Invalid email"
}
if input.RedirectURI == "" {
    errs["redirectUri"] = "Redirect URI required"
} else if u, err := url.Parse(input.RedirectURI); err != nil || !u.IsAbs() {
    errs["redirectUri"] = "Invalid redirect URI"
}
if len(errs) != 0 {
    respondJSON(w, errs, http.StatusUnprocessableEntity)
    return
}

我们使用 golang 的 URL 解析器去验证重定向 URI,检查那个 URI 是否为绝对地址。

var verificationCode string
err := db.QueryRowContext(r.Context(), `
    INSERT INTO verification_codes (user_id) VALUES
        ((SELECT id FROM users WHERE email = $1))
    RETURNING id
`, input.Email).Scan(&verificationCode)
if errPq, ok := err.(*pq.Error); ok && errPq.Code.Name() == "not_null_violation" {
    respondJSON(w, "No user found with that email", http.StatusNotFound)
    return
} else if err != nil {
    respondInternalError(w, fmt.Errorf("could not insert verification code: %v", err))
    return
}

这个 SQL 查询将插入一个验证代码,这个代码通过给定的 email 关联到用户,并且返回一个自动生成的 id。因为有可能会出现用户不存在的情况,那样的话子查询可能解析为 NULL,这将导致在 user_id 字段上因违反 NOT NULL 约束而导致失败,因此需要对这种情况进行检查,如果用户不存在,则返回 404 Not Found 或者一个内部错误。

q := make(url.Values)
q.Set("verification_code", verificationCode)
q.Set("redirect_uri", input.RedirectURI)
magicLink := *config.appURL
magicLink.Path = "/api/passwordless/verify_redirect"
magicLink.RawQuery = q.Encode()

现在,构建魔法链接并设置查询字符串中的 verification_coderedirect_uri 的值。如:http://localhost/api/passwordless/verify_redirect?verification_code=some_code&redirect_uri=https://frontend.app/callback

var body bytes.Buffer
data := map[string]string{"MagicLink": magicLink.String()}
if err := magicLinkTmpl.Execute(&body, data); err != nil {
    respondInternalError(w, fmt.Errorf("could not execute magic link template: %v", err))
    return
}

我们将得到的魔法链接模板的内容保存到缓冲区中。如果发生错误则返回一个内部错误。

to := mail.Address{Address: input.Email}
if err := sendMail(to, "Magic Link", body.String()); err != nil {
    respondInternalError(w, fmt.Errorf("could not mail magic link: %v", err))
    return
}

现在来写给用户发邮件的 sendMail 函数。如果发生错误则返回一个内部错误。

w.WriteHeader(http.StatusNoContent)

最后,设置响应状态码为 204 No Content。对于成功的状态码,客户端不需要很多数据。

发送邮件函数

func sendMail(to mail.Address, subject, body string) error {
    from := mail.Address{
        Name:    "Passwordless Demo",
        Address: "noreply@" + config.appURL.Host,
    }
    headers := map[string]string{
        "From":         from.String(),
        "To":           to.String(),
        "Subject":      subject,
        "Content-Type": `text/html; charset="utf-8"`,
    }
    msg := ""
    for k, v := range headers {
        msg += fmt.Sprintf("%s: %s
", k, v)
    }
    msg += "
"
    msg += body

    return smtp.SendMail(
        config.smtpAddr,
        config.smtpAuth,
        from.Address,
        []string{to.Address},
        []byte(msg))
}

这个函数创建一个基本的 HTML 邮件结构体并使用 SMTP 服务器去发送它。邮件的内容你可以随意定制,我喜欢使用比较简单的内容。

无密码验证重定向的处理程序

var rxUUID = regexp.MustCompile("^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")

首先,这个正则表达式去验证一个 UUID(即验证代码)。

现在进入 passwordlessVerifyRedirect 函数内部:

q := r.URL.Query()
verificationCode := q.Get("verification_code")
redirectURI := q.Get("redirect_uri")

/api/passwordless/verify_redirect 是一个 GET 端点,以便于我们从查询字符串中读取数据。

errs := make(map[string]string)
if verificationCode == "" {
    errs["verification_code"] = "Verification code required"
} else if !rxUUID.MatchString(verificationCode) {
    errs["verification_code"] = "Invalid verification code"
}
var callback *url.URL
var err error
if redirectURI == "" {
    errs["redirect_uri"] = "Redirect URI required"
} else if callback, err = url.Parse(redirectURI); err != nil || !callback.IsAbs() {
    errs["redirect_uri"] = "Invalid redirect URI"
}
if len(errs) != 0 {
    respondJSON(w, errs, http.StatusUnprocessableEntity)
    return
}

类似的验证,我们保存解析后的重定向 URI 到一个 callback 变量中。

var userID string
if err := db.QueryRowContext(r.Context(), `
    DELETE FROM verification_codes
    WHERE id = $1
        AND created_at >= now() - INTERVAL '15m'
    RETURNING user_id
`, verificationCode).Scan(&userID); err == sql.ErrNoRows {
    respondJSON(w, "Link expired or already used", http.StatusBadRequest)
    return
} else if err != nil {
    respondInternalError(w, fmt.Errorf("could not delete verification code: %v", err))
    return
}

这个 SQL 查询通过给定的 id 去删除相应的验证代码,并且确保它创建之后时间不超过 15 分钟,它也返回关联的 user_id。如果没有检索到内容,意味着代码不存在或者已过期,我们返回一个响应信息,否则就返回一个内部错误。

expiresAt := time.Now().Add(time.Hour * 24 * 60)
tokenString, err := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
    Subject:   userID,
    ExpiresAt: expiresAt.Unix(),
}).SignedString(config.jwtKey)
if err != nil {
    respondInternalError(w, fmt.Errorf("could not create JWT: %v", err))
    return
}

这些是如何去创建 JWT。我们为 JWT 设置一个 60 天的过期值,你也可以设置更短的时间(大约 2 周),并添加一个新端点去刷新令牌,但是不要搞的过于复杂。

expiresAtB, err := expiresAt.MarshalText()
if err != nil {
    respondInternalError(w, fmt.Errorf("could not marshal expiration date: %v", err))
    return
}
f := make(url.Values)
f.Set("jwt", tokenString)
f.Set("expires_at", string(expiresAtB))
callback.Fragment = f.Encode()

我们去规划重定向;你可使用查询字符串去添加 JWT,但是更常见的是使用一个哈希片段。如:https://frontend.app/callback#jwt=token_here&expires_at=some_date.

过期日期可以从 JWT 中提取出来,但是这样做的话,就需要在客户端上实现一个 JWT 库来解码它,因此为了简化,我将它加到这里。

http.Redirect(w, r, callback.String(), http.StatusFound)

最后我们使用一个 302 Found 重定向。


无密码的流程已经完成。现在需要去写 getAuthUser 端点的代码了,它用于获取当前验证用户的信息。你应该还记得,这个端点使用了 guard 中间件。

使用 Auth 中间件

在编写 guard 中间件之前,我将编写一个不需要验证的分支。目的是,如果没有传递 JWT,它将不去验证用户。

type ContextKey struct {
    Name string
}

var keyAuthUserID = ContextKey{"auth_user_id"}

func withAuth(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        a := r.Header.Get("Authorization")
        hasToken := strings.HasPrefix(a, "Bearer ")
        if !hasToken {
            next(w, r)
            return
        }
        tokenString := a[7:]

        p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
        token, err := p.ParseWithClaims(
            tokenString,
            &jwt.StandardClaims{},
            func (*jwt.Token) (interface{}, error) { return config.jwtKey, nil },
        )
        if err != nil {
            respondJSON(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
            return
        }

        claims, ok := token.Claims.(*jwt.StandardClaims)
        if !ok || !token.Valid {
            respondJSON(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
            return
        }

        ctx := r.Context()
        ctx = context.WithValue(ctx, keyAuthUserID, claims.Subject)

        next(w, r.WithContext(ctx))
    }
}

JWT 将在每次请求时以 Bearer <token_here> 格式包含在 Authorization 头中。因此,如果没有提供令牌,我们将直接通过,进入接下来的中间件。

我们创建一个解析器来解析令牌。如果解析失败则返回 401 Unauthorized

然后我们从 JWT 中提取出要求的内容,并添加 Subject(就是用户 ID)到需要的地方。

Guard 中间件

func guard(next http.HandlerFunc) http.HandlerFunc {
    return withAuth(func(w http.ResponseWriter, r *http.Request) {
        _, ok := r.Context().Value(keyAuthUserID).(string)
        if !ok {
            respondJSON(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
            return
        }
        next(w, r)
    })
}

现在,guard 将使用 withAuth 并从请求内容中提取出验证用户的 ID。如果提取失败,它将返回 401 Unauthorized,提取成功则继续下一步。

获取 Auth 用户

getAuthUser 处理程序内部:

ctx := r.Context()
authUserID := ctx.Value(keyAuthUserID).(string)

user, err := fetchUser(ctx, authUserID)
if err == sql.ErrNoRows {
    respondJSON(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
    return
} else if err != nil {
    respondInternalError(w, fmt.Errorf("could not query auth user: %v", err))
    return
}

respondJSON(w, user, http.StatusOK)

首先,我们从请求内容中提取验证用户的 ID,我们使用这个 ID 去获取用户。如果没有获取到内容,则发送一个 418 I'm a teapot,或者一个内部错误。最后,我们将用这个用户去响应。

获取 User 函数

下面你看到的是 fetchUser 函数。

func fetchUser(ctx context.Context, id string) (User, error) {
    user := User{ID: id}
    err := db.QueryRowContext(ctx, `
        SELECT email, username FROM users WHERE id = $1
    `, id).Scan(&user.Email, &user.Username)
    return user, err
}

我将它解耦是因为通过 ID 来获取用户是个常做的事。


以上就是全部的代码。你可以自己去构建它和测试它。这里 还有一个 demo 你可以试用一下。

如果你在 mailtrap 上点击之后出现有关 脚本运行被拦截,因为文档的框架是沙箱化的,并且没有设置 'allow-scripts' 权限 的问题,你可以尝试右键点击 “在新标签中打开链接“。这样做是安全的,因为邮件内容是 沙箱化的。我在 localhost 上有时也会出现这个问题,但是我认为你一旦以 https:// 方式部署到服务器上应该不会出现这个问题了。

如果有任何问题,请在我的 GitHub repo 留言或者提交 PRs

以后,我为这个 API 写了一个客户端作为这篇文章的第二部分


via: https://nicolasparada.netlify.com/posts/passwordless-auth-server/

作者:Nicolás Parada 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

了解如何在 Linux 中创建登录导语,来向要登录或登录后的用户显示不同的警告或消息。

无论何时登录公司的某些生产系统,你都会看到一些登录消息、警告或关于你将登录或已登录的服务器的信息,如下所示。这些是 登录导语 login banner

Login welcome messages in Linux

在本文中,我们将引导你配置它们。

你可以配置两种类型的导语。

  1. 用户登录前显示的导语信息(在你选择的文件中配置,例如 /etc/login.warn
  2. 用户成功登录后显示的导语信息(在 /etc/motd 中配置)

如何在用户登录前连接系统时显示消息

当用户连接到服务器并且在登录之前,这个消息将被显示给他。意味着当他输入用户名时,该消息将在密码提示之前显示。

你可以使用任何文件名并在其中输入信息。在这里我们使用 /etc/login.warn 并且把我们的消息放在里面。

# cat /etc/login.warn
        !!!! Welcome to KernelTalks test server !!!!
This server is meant for testing Linux commands and tools. If you are
not associated with kerneltalks.com and not authorized please dis-connect
immediately.

现在,需要将此文件和路径告诉 sshd 守护进程,以便它可以为每个用户登录请求获取此标语。对于此,打开 /etc/sshd/sshd_config 文件并搜索 #Banner none

这里你需要编辑该配置文件,并写下你的文件名并删除注释标记(#)。它应该看起来像:Banner /etc/login.warn

保存文件并重启 sshd 守护进程。为避免断开现有的连接用户,请使用 HUP 信号重启 sshd。

root@kerneltalks # ps -ef | grep -i sshd
root     14255     1  0 18:42 ?        00:00:00 /usr/sbin/sshd -D
root     19074 14255  0 18:46 ?        00:00:00 sshd: ec2-user [priv]
root     19177 19127  0 18:54 pts/0    00:00:00 grep -i sshd

root@kerneltalks # kill -HUP 14255

就是这样了!打开新的会话并尝试登录。你将看待你在上述步骤中配置的消息。

Login banner in Linux

你可以在用户输入密码登录系统之前看到此消息。

如何在用户登录后显示消息

消息用户在成功登录系统后看到的 当天消息 Message Of The Day (MOTD)由 /etc/motd 控制。编辑这个文件并输入当成功登录后欢迎用户的消息。

root@kerneltalks # cat /etc/motd
           W E L C O M E
Welcome to the testing environment of kerneltalks.
Feel free to use this system for testing your Linux
skills. In case of any issues reach out to admin at
[email protected]. Thank you.

你不需要重启 sshd 守护进程来使更改生效。只要保存该文件,sshd 守护进程就会下一次登录请求时读取和显示。

motd in linux

你可以在上面的截图中看到:黄色框是由 /etc/motd 控制的 MOTD,绿色框就是我们之前看到的登录导语。

你可以使用 cowsaybannerfigletlolcat 等工具创建出色的引人注目的登录消息。此方法适用于几乎所有 Linux 发行版,如 RedHat、CentOs、Ubuntu、Fedora 等。


via: https://kerneltalks.com/tips-tricks/how-to-configure-login-banners-in-linux/

作者:kerneltalks 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出