标签 密码 下的文章

无密码验证可以让你只输入一个 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中国 荣誉推出

如何快速简单地在 Fedora 、 CentOS 及类似的 Linux 发行版上重置 root 密码。

系统管理员可以轻松地为忘记密码的用户重置密码。但是如果系统管理员忘记 root 密码或他从公司离职了,会发生什么情况?本指南将向你介绍如何在不到 5 分钟的时间内在 Red Hat 兼容系统(包括 Fedora 和 CentOS)上重置丢失或忘记的 root 密码。

请注意,如果整个系统硬盘已用 LUKS 加密,则需要在出现提示时提供 LUKS 密码。此外,此过程适用于运行 systemd 的系统,该系统自 Fedora 15、CentOS 7.14.04 和 Red Hat Enterprise Linux 7.0 以来一直是缺省的初始系统。

首先你需要中断启动的过程,因此你需要启动或者如果已经启动就重启它。第一步可能有点棘手因为 GRUB 菜单会在屏幕上快速地闪烁过去。你可能需要尝试几次,直到你能够做到这一点。

当你看到这个屏幕时,按下键盘上的 e 键:

如果你正确地做了这点,你应该看到一个类似于这个的屏幕:

使用箭头键移动到 Linux16 这行:

使用你的 del 键或你的 backspace 键,删除 rhgb quiet 并替换为以下内容:

rd.break enforcing=0

设置 enforcing=0 可以避免执行完整的系统 SELinux 重标记。一旦系统重新启动,你只需要为 /etc/shadow 恢复正确的 SELinux 上下文。我会告诉你如何做到这一点。

按下 Ctrl-x 启动。

系统现在将处于紧急模式。

以读写权限重新挂载硬盘驱动器:

# mount –o remount,rw /sysroot

运行 chroot 来访问系统:

# chroot /sysroot

你现在可以更改 root 密码:

# passwd

出现提示时,输入新的 root 密码两次。如果成功,你应该看到一条消息显示 “all authentication tokens updated successfully”。

输入 exit 两次以重新启动系统。

以 root 身份登录并恢复 /etc/shadow 的 SELinux 标签。

# restorecon -v /etc/shadow

将 SELinux 回到 enforce 模式:

# setenforce 1

via: https://opensource.com/article/18/4/reset-lost-root-password

作者:Curt Warfield 选题:lujun9972 译者:geekpi 校对:wxy

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

给在终端窗口花费大量时间的人们的密码管理器。

我们都希望我们的密码安全可靠。为此,许多人转向密码管理应用程序,如 KeePassXBitwarden

如果你在终端中花费了大量时间而且正在寻找更简单的解决方案,那么你需要了解下诸多的 Linux 命令行密码管理器。它们快速,易于使用且安全。

让我们来看看其中的三个。

Titan

Titan 是一个密码管理器,也可作为文件加密工具。我不确定 Titan 在加密文件方面效果有多好;我只是把它看作密码管理器,在这方面,它确实做的很好。

Titan 将你的密码存储在加密的 SQLite 数据库中,你可以在第一次启动该应用程序时创建并添加主密码。告诉 Titan 增加一个密码,它需要一个用来识别它的名字、用户名、密码本身、URL 和关于密码的注释。

你可以让 Titan 为你生成一个密码,你可以通过条目名称或数字 ID、名称、注释或使用正则表达式来搜索数据库,但是,查看特定的密码可能会有点笨拙,你要么必须列出所有密码滚动查找你想要使用的密码,要么你可以通过使用其数字 ID(如果你知道)列出条目的详细信息来查看密码。

Gopass

Gopass 被称为“团队密码管理器”。不要因此感到失望,它对个人的使用也很好。

Gopass 是用 Go 语言编写的经典 Unix 和 Linux Pass 密码管理器的更新版本。安装纯正的 Linux 方式,你可以编译源代码使用安装程序以在你的计算机上使用 gopass。

在开始使用 gopass 之前,确保你的系统上有 GNU Privacy Guard (GPG)Git。前者对你的密码存储进行加密和解密,后者将提交到一个 Git 仓库。如果 gopass 是给个人使用,你仍然需要 Git。你不需要担心提交到仓库。如果你感兴趣,你可以在文档中了解这些依赖关系。

当你第一次启动 gopass 时,你需要创建一个密码存储库并生成一个密钥以确保存储的安全。当你想添加一个密码(gopass 中称之为“secret”)时,gopass 会要求你提供一些信息,比如 URL、用户名和密码。你可以让 gopass 为你添加的“secret”生成密码,或者你可以自己输入密码。

根据需要,你可以编辑、查看或删除密码。你还可以查看特定的密码或将其复制到剪贴板,以将其粘贴到登录表单或窗口中。

Kpcli

许多人选择的是开源密码管理器 KeePassKeePassXKpcli 将 KeePass 和 KeePassX 的功能带到你的终端窗口。

Kpcli 是一个键盘驱动的 shell,可以完成其图形化的表亲的大部分功能。这包括打开密码数据库、添加和编辑密码和组(组帮助你组织密码),甚至重命名或删除密码和组。

当你需要时,你可以将用户名和密码复制到剪贴板以粘贴到登录表单中。为了保证这些信息的安全,kpcli 也有清除剪贴板的命令。对于一个小终端应用程序来说还不错。

你有最喜欢的命令行密码管理器吗?何不通过发表评论来分享它?


via: https://opensource.com/article/18/4/3-password-managers-linux-command-line

作者:Scott Nesbitt 选题:lujun9972 译者:MjSeven 校对:wxy

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

目标

学习在 Linux 上使用 pass 密码管理器来管理你的密码

条件

  • 需要 root 权限来安装需要的包

难度

简单

约定

  • # - 执行指定命令需要 root 权限,可以是直接使用 root 用户来执行或者使用 sudo 命令来执行
  • $ - 使用普通的非特权用户执行指定命令

介绍

如果你有根据不同的意图设置不同密码的好习惯,你可能已经感受到需要一个密码管理器的必要性了。在 Linux 上有很多选择,可以是专有软件(如果你敢用的话)也可以是开源软件。如果你跟我一样喜欢简洁的话,你可能会对 pass 感兴趣。

第一步

pass 作为一个密码管理器,其实际上是一些你可能早已每天使用的、可信赖且实用的工具的一种封装,比如 gpggit 。虽然它也有图形界面,但它专门设计能成在命令行下工作的:因此它也可以在 headless 机器上工作(LCTT 译注:根据 wikipedia 的说法,所谓 headless 是指没有显示器、键盘和鼠标的机器,一般通过网络链接来控制)。

安装

pass 在主流的 Linux 发行版中都是可用的,你可以通过包管理器安装:

Fedora

# dnf install pass

RHEL 和 CentOS

pass 不在官方仓库中,但你可以从 epel 中获取道它。要在 CentOS7 上启用后面这个源,只需要执行:

# yum install epel-release

然而在 Red Hat 企业版的 Linux 上,这个额外的源是不可用的;你需要从 EPEL 官方网站上下载它。

Debian 和 Ubuntu

# apt-get install pass

Arch Linux

# pacman -S pass

初始化密码仓库

安装好 pass 后,就可以开始使用和配置它了。首先,由于 pass 依赖于 gpg 来对我们的密码进行加密并以安全的方式进行存储,我们必须准备好一个 gpg 密钥对。

首先我们要初始化密码仓库:这就是一个用来存放 gpg 加密后的密码的目录。默认情况下它会在你的 $HOME 创建一个隐藏目录,不过你也可以通过使用 PASSWORD_STORE_DIR 这一环境变量来指定另一个路径。让我们运行:

$ pass init

然后 password-store 目录就创建好了。现在,让我们来存储我们第一个密码:

$ pass edit mysite

这会打开默认文本编辑器,我么只需要输入密码就可以了。输入的内容会用 gpg 加密并存储为密码仓库目录中的 mysite.gpg 文件。

pass 以目录树的形式存储加密后的文件,也就是说我们可以在逻辑上将多个文件放在子目录中以实现更好的组织形式,我们只需要在创建文件时指定存在哪个目录下就行了,像这样:

$ pass edit foo/bar

跟上面的命令一样,它也会让你输入密码,但是创建的文件是放在密码仓库目录下的 foo 子目录中的。要查看文件组织结构,只需要不带任何参数运行 pass 命令即可:

$ pass
Password Store
├── foo
│   └── bar
└── mysite

若想修改密码,只需要重复创建密码的操作就行了。

获取密码

有两种方法可以获取密码:第一种会显示密码到终端上,方法是运行:

pass mysite

然而更好的方法是使用 -c 选项让 pass 将密码直接拷贝到剪切板上:

pass -c mysite

这种情况下剪切板中的内容会在 45 秒后自动清除。两种方法都会要求你输入 gpg 密码。

生成密码

pass 也可以为我们自动生成(并自动存储)安全密码。假设我们想要生成一个由 15 个字符组成的密码:包含字母,数字和特殊符号,其命令如下:

pass generate mysite 15

若希望密码只包含字母和数字则可以是使用 --no-symbols 选项。生成的密码会显示在屏幕上。也可以通过 --clip-c 选项让 pass 把密码直接拷贝到剪切板中。通过使用 -q--qrcode 选项来生成二维码:

qrcode

从上面的截屏中可看出,生成了一个二维码,不过由于运行该命令时 mysite 的密码已经存在了,pass 会提示我们确认是否要覆盖原密码。

pass 使用 /dev/urandom 设备作为(伪)随机数据生成器来生成密码,同时它使用 xclip 工具来将密码拷贝到粘帖板中,而使用 qrencode 来将密码以二维码的形式显示出来。在我看来,这种模块化的设计正是它最大的优势:它并不重复造轮子,而只是将常用的工具包装起来完成任务。

你也可以使用 pass mvpass cppass rm 来重命名、拷贝和删除密码仓库中的文件。

将密码仓库变成 git 仓库

pass 另一个很棒的功能就是可以将密码仓库当成 git 仓库来用:通过版本管理系统能让我们管理密码更方便。

pass git init

这会创建 git 仓库,并自动提交所有已存在的文件。下一步就是指定跟踪的远程仓库了:

pass git remote add <name> <url>

我们可以把这个密码仓库当成普通仓库来用。唯一的不同点在于每次我们新增或修改一个密码,pass 都会自动将该文件加入索引并创建一个提交。

pass 有一个叫做 qtpass 的图形界面,而且也支持 Windows 和 MacOs。通过使用 PassFF 插件,它还能获取 firefox 中存储的密码。在它的项目网站上可以查看更多详细信息。试一下 pass 吧,你不会失望的!


via: https://linuxconfig.org/how-to-organize-your-passwords-using-pass-password-manager

作者:Egidio Docile 译者:lujun9972 校对:Locez

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

当你在 Web 浏览器或任何 GUI 登录中输入密码时,密码会被标记成星号 ******** 或圆点符号 ••••••••••••• 。这是内置的安全机制,以防止你附近的用户看到你的密码。但是当你在终端输入密码来执行任何 sudosu 的管理任务时,你不会在输入密码的时候看见星号或者圆点符号。它不会有任何输入密码的视觉指示,也不会有任何光标移动,什么也没有。你不知道你是否输入了所有的字符。你只会看到一个空白的屏幕!

看看下面的截图。

正如你在上面的图片中看到的,我已经输入了密码,但没有任何指示(星号或圆点符号)。现在,我不确定我是否输入了所有密码。这个安全机制也可以防止你附近的人猜测密码长度。当然,这种行为可以改变。这是本指南要说的。这并不困难。请继续阅读。

当你在终端输入密码时显示星号

要在终端输入密码时显示星号,我们需要在 /etc/sudoers 中做一些小修改。在做任何更改之前,最好备份这个文件。为此,只需运行:

sudo cp /etc/sudoers{,.bak}

上述命令将 /etc/sudoers 备份成名为 /etc/sudoers.bak。你可以恢复它,以防万一在编辑文件后出错。

接下来,使用下面的命令编辑 /etc/sudoers

sudo visudo

找到下面这行:

Defaults env_reset

在该行的末尾添加一个额外的单词 ,pwfeedback,如下所示。

Defaults env_reset,pwfeedback

然后,按下 CTRL + xy 保存并关闭文件。重新启动终端以使更改生效。

现在,当你在终端输入密码时,你会看到星号。

如果你对在终端输入密码时看不到密码感到不舒服,那么这个小技巧会有帮助。请注意,当你输入输入密码时其他用户就可以预测你的密码长度。如果你不介意,请按照上述方法进行更改,以使你的密码可见(当然,显示为星号!)。

现在就是这样了。还有更好的东西。敬请关注!

干杯!


via: https://www.ostechnix.com/display-asterisks-type-password-terminal/

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

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

学习使用 8 种 Linux 原生命令或第三方实用程序来生成随机密码。

在这篇文章中,我们将引导你通过几种不同的方式在 Linux 终端中生成随机密码。其中几种利用原生 Linux 命令,另外几种则利用极易在 Linux 机器上安装的第三方工具或实用程序实现。在这里我们利用像 openssl, dd, md5sum, tr, urandom 这样的原生命令和 mkpasswd,randpw,pwgen,spw,gpg,xkcdpass,diceware,revelation,keepaasx,passwordmaker 这样的第三方工具。

其实这些方法就是生成一些能被用作密码的随机字母字符串。随机密码可以用于新用户的密码,不管用户基数有多大,这些密码都是独一无二的。话不多说,让我们来看看 8 种不同的在 Linux 上生成随机密码的方法吧。

使用 mkpasswd 实用程序生成密码

mkpasswd 在基于 RHEL 的系统上随 expect 软件包一起安装。在基于 Debian 的系统上 mkpasswd 则在软件包 whois 中。直接安装 mkpasswd 软件包将会导致错误:

  • RHEL 系统:软件包 mkpasswd 不可用。
  • Debian 系统:错误:无法定位软件包 mkpasswd。

所以按照上面所述安装他们的父软件包,就没问题了。

运行 mkpasswd 来获得密码

root@kerneltalks# mkpasswd << on RHEL
zt*hGW65c

root@kerneltalks# mkpasswd teststring << on Ubuntu
XnlrKxYOJ3vik

这个命令在不同的系统上表现得不一样,所以工作方式各异。你也可以通过参数来控制长度等选项,可以查阅 man 手册来探索。

使用 openssl 生成密码

几乎所有 Linux 发行版都包含 openssl。我们可以利用它的随机功能来生成可以用作密码的随机字母字符串。

root@kerneltalks # openssl rand -base64 10
nU9LlHO5nsuUvw==

这里我们使用 base64 编码随机函数,最后一个数字参数表示长度。

使用 urandom 生成密码

设备文件 /dev/urandom 是另一个获得随机字符串的方法。我们使用 tr 功能并裁剪输出来获得随机字符串,并把它作为密码。

root@kerneltalks # strings /dev/urandom |tr -dc A-Za-z0-9 | head -c20; echo
UiXtr0NAOSIkqtjK4c0X

使用 dd 命令生成密码

我们甚至可以使用 /dev/urandom 设备配合 dd 命令 来获取随机字符串。

root@kerneltalks# dd if=/dev/urandom bs=1 count=15|base64 -w 0
15+0 records in
15+0 records out
15 bytes (15 B) copied, 5.5484e-05 s, 270 kB/s
QMsbe2XbrqAc2NmXp8D0

我们需要将结果通过 base64 编码使它能被人类可读。你可以使用数值来获取想要的长度。想要获得更简洁的输出的话,可以将“标准错误输出”重定向到 /dev/null。简洁输出的命令是:

root@kerneltalks # dd if=/dev/urandom bs=1 count=15 2>/dev/null|base64 -w 0
F8c3a4joS+a3BdPN9C++

使用 md5sum 生成密码

另一种获取可用作密码的随机字符串的方法是计算 MD5 校验值!校验值看起来确实像是随机字符串组合在一起,我们可以用作密码。确保你的计算源是个变量,这样的话每次运行命令时生成的校验值都不一样。比如 datedate 命令 总会生成不同的输出。

root@kerneltalks # date |md5sum
4d8ce5c42073c7e9ca4aeffd3d157102  -

在这里我们将 date 命令的输出通过 md5sum 得到了校验和!你也可以用 cut 命令 裁剪你需要的长度。

使用 pwgen 生成密码

pwgen 软件包在类似 EPEL 软件仓库(LCTT 译注:企业版 Linux 附加软件包)中。pwgen 更专注于生成可发音的密码,但它们不在英语词典中,也不是纯英文的。标准发行版仓库中可能并不包含这个工具。安装这个软件包然后运行 pwgen 命令行。Boom !

root@kerneltalks # pwgen
thu8Iox7 ahDeeQu8 Eexoh0ai oD8oozie ooPaeD9t meeNeiW2 Eip6ieph Ooh1tiet
cootad7O Gohci0vo wah9Thoh Ohh3Ziur Ao1thoma ojoo6aeW Oochai4v ialaiLo5
aic2OaDa iexieQu8 Aesoh4Ie Eixou9ph ShiKoh0i uThohth7 taaN3fuu Iege0aeZ
cah3zaiW Eephei0m AhTh8guo xah1Shoo uh8Iengo aifeev4E zoo4ohHa fieDei6c
aorieP7k ahna9AKe uveeX7Hi Ohji5pho AigheV7u Akee9fae aeWeiW4a tiex8Oht

你的终端会呈现出一个密码列表!你还想要什么呢?好吧。你还想再仔细探索的话, pwgen 还有很多自定义选项,这些都可以在 man 手册里查阅到。

使用 gpg 工具生成密码

GPG 是一个遵循 OpenPGP 标准的加密及签名工具。大部分 gpg 工具都预先被安装好了(至少在我的 RHEL7 上是这样)。但如果没有的话你可以寻找 gpggpg2 软件包并安装它。

使用下面的命令以从 gpg 工具生成密码。

root@kerneltalks # gpg --gen-random --armor 1 12
mL8i+PKZ3IuN6a7a

这里我们传了生成随机字节序列选项(--gen-random),质量为 1(第一个参数),次数 12 (第二个参数)。选项 --armor 保证以 base64 编码输出。

使用 xkcdpass 生成密码

著名的极客幽默网站 xkcd,发表了一篇非常有趣的文章,是关于好记但又复杂的密码的。你可以在这里阅读。所以 xkcdpass 工具就受这篇文章启发,做了这样的工作!这是一个 Python 软件包,可以在这里的 Python 的官网上找到它。

所有的安装使用说明都在上面那个页面提及了。这里是安装步骤和我的测试 RHEL 服务器的输出,以供参考。

root@kerneltalks # wget https://pypi.python.org/packages/b4/d7/3253bd2964390e034cf0bba227db96d94de361454530dc056d8c1c096abc/xkcdpass-1.14.3.tar.gz#md5=5f15d52f1d36207b07391f7a25c7965f
--2018-01-23 19:09:17--  https://pypi.python.org/packages/b4/d7/3253bd2964390e034cf0bba227db96d94de361454530dc056d8c1c096abc/xkcdpass-1.14.3.tar.gz
Resolving pypi.python.org (pypi.python.org)... 151.101.32.223, 2a04:4e42:8::223
Connecting to pypi.python.org (pypi.python.org)|151.101.32.223|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 871848 (851K) [binary/octet-stream]
Saving to: ‘xkcdpass-1.14.3.tar.gz’

100%[==============================================================================================================================>] 871,848     --.-K/s   in 0.01s

2018-01-23 19:09:17 (63.9 MB/s) - ‘xkcdpass-1.14.3.tar.gz’ saved [871848/871848]


root@kerneltalks # tar -xvf xkcdpass-1.14.3.tar.gz
xkcdpass-1.14.3/
xkcdpass-1.14.3/examples/
xkcdpass-1.14.3/examples/example_import.py
xkcdpass-1.14.3/examples/example_json.py
xkcdpass-1.14.3/examples/example_postprocess.py
xkcdpass-1.14.3/LICENSE.BSD
xkcdpass-1.14.3/MANIFEST.in
xkcdpass-1.14.3/PKG-INFO
xkcdpass-1.14.3/README.rst
xkcdpass-1.14.3/setup.cfg
xkcdpass-1.14.3/setup.py
xkcdpass-1.14.3/tests/
xkcdpass-1.14.3/tests/test_list.txt
xkcdpass-1.14.3/tests/test_xkcdpass.py
xkcdpass-1.14.3/tests/__init__.py
xkcdpass-1.14.3/xkcdpass/
xkcdpass-1.14.3/xkcdpass/static/
xkcdpass-1.14.3/xkcdpass/static/eff-long
xkcdpass-1.14.3/xkcdpass/static/eff-short
xkcdpass-1.14.3/xkcdpass/static/eff-special
xkcdpass-1.14.3/xkcdpass/static/fin-kotus
xkcdpass-1.14.3/xkcdpass/static/ita-wiki
xkcdpass-1.14.3/xkcdpass/static/legacy
xkcdpass-1.14.3/xkcdpass/static/spa-mich
xkcdpass-1.14.3/xkcdpass/xkcd_password.py
xkcdpass-1.14.3/xkcdpass/__init__.py
xkcdpass-1.14.3/xkcdpass.1
xkcdpass-1.14.3/xkcdpass.egg-info/
xkcdpass-1.14.3/xkcdpass.egg-info/dependency_links.txt
xkcdpass-1.14.3/xkcdpass.egg-info/entry_points.txt
xkcdpass-1.14.3/xkcdpass.egg-info/not-zip-safe
xkcdpass-1.14.3/xkcdpass.egg-info/PKG-INFO
xkcdpass-1.14.3/xkcdpass.egg-info/SOURCES.txt
xkcdpass-1.14.3/xkcdpass.egg-info/top_level.txt


root@kerneltalks # cd xkcdpass-1.14.3

root@kerneltalks # python setup.py install
running install
running bdist_egg
running egg_info
writing xkcdpass.egg-info/PKG-INFO
writing top-level names to xkcdpass.egg-info/top_level.txt
writing dependency_links to xkcdpass.egg-info/dependency_links.txt
writing entry points to xkcdpass.egg-info/entry_points.txt
reading manifest file 'xkcdpass.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
writing manifest file 'xkcdpass.egg-info/SOURCES.txt'
installing library code to build/bdist.linux-x86_64/egg
running install_lib
running build_py
creating build
creating build/lib
creating build/lib/xkcdpass
copying xkcdpass/xkcd_password.py -> build/lib/xkcdpass
copying xkcdpass/__init__.py -> build/lib/xkcdpass
creating build/lib/xkcdpass/static
copying xkcdpass/static/eff-long -> build/lib/xkcdpass/static
copying xkcdpass/static/eff-short -> build/lib/xkcdpass/static
copying xkcdpass/static/eff-special -> build/lib/xkcdpass/static
copying xkcdpass/static/fin-kotus -> build/lib/xkcdpass/static
copying xkcdpass/static/ita-wiki -> build/lib/xkcdpass/static
copying xkcdpass/static/legacy -> build/lib/xkcdpass/static
copying xkcdpass/static/spa-mich -> build/lib/xkcdpass/static
creating build/bdist.linux-x86_64
creating build/bdist.linux-x86_64/egg
creating build/bdist.linux-x86_64/egg/xkcdpass
copying build/lib/xkcdpass/xkcd_password.py -> build/bdist.linux-x86_64/egg/xkcdpass
copying build/lib/xkcdpass/__init__.py -> build/bdist.linux-x86_64/egg/xkcdpass
creating build/bdist.linux-x86_64/egg/xkcdpass/static
copying build/lib/xkcdpass/static/eff-long -> build/bdist.linux-x86_64/egg/xkcdpass/static
copying build/lib/xkcdpass/static/eff-short -> build/bdist.linux-x86_64/egg/xkcdpass/static
copying build/lib/xkcdpass/static/eff-special -> build/bdist.linux-x86_64/egg/xkcdpass/static
copying build/lib/xkcdpass/static/fin-kotus -> build/bdist.linux-x86_64/egg/xkcdpass/static
copying build/lib/xkcdpass/static/ita-wiki -> build/bdist.linux-x86_64/egg/xkcdpass/static
copying build/lib/xkcdpass/static/legacy -> build/bdist.linux-x86_64/egg/xkcdpass/static
copying build/lib/xkcdpass/static/spa-mich -> build/bdist.linux-x86_64/egg/xkcdpass/static
byte-compiling build/bdist.linux-x86_64/egg/xkcdpass/xkcd_password.py to xkcd_password.pyc
byte-compiling build/bdist.linux-x86_64/egg/xkcdpass/__init__.py to __init__.pyc
creating build/bdist.linux-x86_64/egg/EGG-INFO
copying xkcdpass.egg-info/PKG-INFO -> build/bdist.linux-x86_64/egg/EGG-INFO
copying xkcdpass.egg-info/SOURCES.txt -> build/bdist.linux-x86_64/egg/EGG-INFO
copying xkcdpass.egg-info/dependency_links.txt -> build/bdist.linux-x86_64/egg/EGG-INFO
copying xkcdpass.egg-info/entry_points.txt -> build/bdist.linux-x86_64/egg/EGG-INFO
copying xkcdpass.egg-info/not-zip-safe -> build/bdist.linux-x86_64/egg/EGG-INFO
copying xkcdpass.egg-info/top_level.txt -> build/bdist.linux-x86_64/egg/EGG-INFO
creating dist
creating 'dist/xkcdpass-1.14.3-py2.7.egg' and adding 'build/bdist.linux-x86_64/egg' to it
removing 'build/bdist.linux-x86_64/egg' (and everything under it)
Processing xkcdpass-1.14.3-py2.7.egg
creating /usr/lib/python2.7/site-packages/xkcdpass-1.14.3-py2.7.egg
Extracting xkcdpass-1.14.3-py2.7.egg to /usr/lib/python2.7/site-packages
Adding xkcdpass 1.14.3 to easy-install.pth file
Installing xkcdpass script to /usr/bin

Installed /usr/lib/python2.7/site-packages/xkcdpass-1.14.3-py2.7.egg
Processing dependencies for xkcdpass==1.14.3
Finished processing dependencies for xkcdpass==1.14.3

现在运行 xkcdpass 命令,将会随机给出你几个像下面这样的字典单词:

root@kerneltalks # xkcdpass
broadside unpadded osmosis statistic cosmetics lugged

你可以用这些单词作为其他命令,比如 md5sum 的输入,来获取随机密码(就像下面这样),甚至你也可以用每个单词的第 N 个字母来生成你的密码!

root@kerneltalks # xkcdpass |md5sum
45f2ec9b3ca980c7afbd100268c74819  -

root@kerneltalks # xkcdpass |md5sum
ad79546e8350744845c001d8836f2ff2  -

或者你甚至可以把所有单词串在一起作为一个超长的密码,不仅非常好记,也不容易被电脑程序攻破。

Linux 上还有像 DicewareKeePassXRevelationPasswordMaker 这样的工具,也可以考虑用来生成强随机密码。


via: https://kerneltalks.com/tips-tricks/8-ways-to-generate-random-password-in-linux/

作者:kerneltalks 译者:heart4lor 校对:Locez

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