65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Go 和 AWS Lambda 构建身份验证端点

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2020年2月19日

CPOL

4分钟阅读

viewsIcon

12883

本文介绍了一个基于 AWS Lambda 的极简认证端点。

引言

当我玩我的个人项目 Kyiv Station Walk 时,我注意到手动删除测试数据很麻烦,我需要一个管理员页面的概念。这需要某种认证端点。一个非常轻量级的服务,可以检查登录名和密码是否与一对超级用户凭据匹配。

无服务器对于这个简单的微服务非常有用。这带来了一些成本节省,因为由于我预计我的不太受欢迎的服务上的管理页面的执行率很低,无服务器对我来说几乎是免费的。此外,我认为这给我带来了一些架构上的好处,因为它允许我将核心领域与横切关注点分开。对于我的任务,我决定使用 AWS Lambda。我也决定使用 Go,因为它具有极简的特性,这对于 Lambda 实例化很有用。

安装

我们的 Lambda 函数将通过 HTTP 从外部调用,所以我们在它前面放置一个 HTTP 网关,这样它在 AWS 控制台中看起来会像下面这样。

项目结构

为了解耦我们的认证逻辑与 FaaS 内部,我们的项目将包含两个文件:auth.go 将存放认证逻辑,main.go 将我们的逻辑与 AWS Lambda 集成。

main.go 的内容如下:

func clientError(status int) (events.APIGatewayProxyResponse, error) {
    return events.APIGatewayProxyResponse{
        StatusCode: status,
        Body:       http.StatusText(status),
    }, nil
}

func HandleRequest(req events.APIGatewayProxyRequest) 
    (events.APIGatewayProxyResponse, error) {
    jwtToken, err := Auth(req.Body)

    if err != nil {
        return clientError(http.StatusForbidden)
    }

    return events.APIGatewayProxyResponse{
        StatusCode: http.StatusOK,
        Body:       jwtToken,
    }, nil
}

func main() {
    lambda.Start(HandleRequest)
}

为了让这段代码工作,我们需要 "github.com/aws/aws-lambda-go/lambda" 包。

为了让我们的端点可以从外部访问,我们必须为 API 网关提供特殊格式的响应。因此,我们还安装了 github.com/aws/aws-lambda-go/events 包。

让我们来重点看一下上面代码片段中你可能注意到的成功响应示例。

events.APIGatewayProxyResponse{
    StatusCode: http.StatusOK,
    Body:       jwtToken,
}

错误响应如下:

events.APIGatewayProxyResponse{
    StatusCode: status,
    Body:       http.StatusText(status),
}

身份验证

为了我们的目的,我们将省略持久化存储的使用,因为一对凭据就足够了。不过,我们需要用哈希函数来哈希存储的密码,这样可以使防御者在可接受的时间内验证密码,但会耗费攻击者大量资源来从哈希值猜出密码。Argon2被推荐用于此类任务。所以,首先,我们需要 "github.com/aws/aws-lambda-go/lambda" 包。

func main() {
    lambda.Start(HandleRequest)
}

Argon2 在 "golang.org/x/crypto/argon2" 中实现,所以认证非常直接。

func HandleRequest(ctx context.Context, credentials Credentials) (string, error) {
    password := []byte{221, 35, 76, 136, 29, 114, 39, 75, 41, 248, 62, 216, 149, 39, 
    248, 154, 243, 203, 188, 106, 206, 74, 122, 47, 255, 61, 173, 43, 102, 173, 222, 125}

    if credentials.Login != login {
        return "auth failed", errors.New("auth failed")
    }
    key := argon2.Key([]byte(credentials.Password), []byte(salt), 3, 128, 1, 32)
    if areSlicesEqual(key, password) {
        return "ok", nil
    }
    return "auth failed", errors.New("auth failed")
}

注意,对于错误的登录和不正确的密码,我们都返回相同的消息,以尽量少地暴露信息。这可以防止账户枚举攻击。

构建

go build -o main main.go
And zipping it
~\Go\Bin\build-lambda-zip.exe -o main.zip main

使用 Windows

如果您是 Windows 用户,在构建之前需要设置以下环境变量:

利用环境变量

目前,我们看到凭据硬编码在代码库中。这是一种不良实践,因为它们很容易被自动收集凭据

您可以改用环境变量,借助 os 包。

login := os.Getenv("LOGIN")
salt := os.Getenv("SALT")

这是您在 AWS 控制台中设置它们的方法。

JWT 生成

一旦服务验证了凭据有效,它就会发出一个令牌,允许其持有者充当超级用户。为此,我们将使用JWT,它是访问令牌的事实标准格式。

我们需要以下包:

"github.com/dgrijalva/jwt-go"

JWT 生成代码如下:

type Claims struct {
    Username string `json:"username"`
    jwt.StandardClaims
}

func issueJwtToken(login string) (string, error) {
    jwtKey := []byte(os.Getenv("JWTKEY"))

    expirationTime := time.Now().Add(1 * time.Hour)
    claims := &Claims{
        Username: login,
        StandardClaims: jwt.StandardClaims{
            // In JWT, the expiry time is expressed as unix milliseconds
            ExpiresAt: expirationTime.Unix(),
        },
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtKey)
}

由于拦截此令牌的对手可能会冒充超级用户行事,因此我们不希望此令牌无限期有效,因为这将授予对手无限特权。所以我们将令牌过期时间设置为一小时。

测试 API Gateway

此时,我们的 API 已准备好被调用。这是主服务中的一个简短片段,仅在用户拥有足够权限时删除一个路由。

let delete (id: string) =
    fun (next: HttpFunc) (httpContext : HttpContext) ->
    let result =
        AuthApi.authorize httpContext
        |> Result.bind (fun _ -> ElasticAdapter.deleteRoute id)
    match result with
    | Ok _ -> text "" next httpContext
    | Error "ItemNotFound" -> RequestErrors.BAD_REQUEST "" next httpContext
    | Error "Forbidden" -> RequestErrors.FORBIDDEN "" next httpContext
    | Error _ -> ServerErrors.INTERNAL_ERROR "" next httpContext

let authorize (httpContext : HttpContext) =
    let authorizationHeader = httpContext.GetRequestHeader "Authorization"
    let authorizationResult =
        authorizationHeader
        |> Result.bind JwtValidator.validateToken
    authorizationResult

let validateToken (token: string) =
    try
        let tokenHandler = JwtSecurityTokenHandler()
        let validationParameters = createValidationParameters
        let mutable resToken : SecurityToken = null
        tokenHandler.ValidateToken(token, validationParameters, &resToken)
        |> ignore
        Result.Ok()
    with
    | _ -> Result.Error "Forbidden"

最小化攻击面

此时,我们的函数容易受到一些漏洞的攻击,所以我们必须在 API 网关上做一些额外的工作。

端点节流

对于不经常调用的授权函数来说,默认设置太高了。让我们来更改一下。

IP 白名单

我们也不希望我们的函数可以从任何 IP 地址访问。在 API 网关的“资源策略”设置部分中使用以下代码片段,我们可以创建一个可以访问我们 Lambda 函数的 IP 地址白名单。

为了获得 ARN,我们可以导航回 Lambda 配置页面,通过点击 API Gateway 图标来查看它。

结论

对于小型微服务来说,无服务器是一个很好的选择。由于其极简的设计理念,Go 不仅适用于利用复杂并发性的应用程序,也适用于上述这样简单的操作。

历史

  • 2020年2月19日:初始版本
  • 2022年8月3日:添加了关于将业务逻辑与 FaaS 内部解耦的说明
© . All rights reserved.