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





5.00/5 (3投票s)
本文介绍了一个基于 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 内部解耦的说明