在构建一个管理页面时,经常需要实现一个认证机制来确保只有授权用户才能访问。本文将介绍如何使用Go语言和AWS Lambda创建一个轻量级的认证服务。这种服务不仅成本低廉,而且由于其无服务器的特性,可以有效地将核心业务逻辑与横切关注点分离。
为了将认证逻辑与函数即服务(FaaS)的内部实现解耦,项目将包含两个文件:
main.go 文件将包含以下内容:
package main
import (
"net/http"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-lambda-go/events"
)
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)
}
为了使代码能够工作,需要引入以下包:
在应用中,省略了持久存储的使用,因为只需要一对凭证。然而,仍然需要使用哈希函数存储密码,以便在合理的时间内验证密码,同时要求攻击者需要大量的资源来猜测哈希密码。Argon2是推荐用于此任务的算法。因此,需要引入以下包:
"golang.org/x/crypto/argon2"
认证过程相对直接:
package main
import (
"errors"
"context"
)
func HandleRequest(ctx context.Context, credentials Credentials) (string, error) {
password := []byte{
// 省略密码哈希字节数组...
}
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
然后将其打包为zip:
~\Go\Bin\build-lambda-zip.exe -o main.zip main
如果是Windows用户,在构建之前需要设置以下环境变量:
LEVERAGING_ENVIRONMENT_VARIABLES
目前,凭证是硬编码在代码库中的,这是不良实践,因为它们可能会被自动收集。可以使用os包来利用环境变量:
package main
import (
"os"
)
login := os.Getenv("LOGIN")
salt := os.Getenv("SALT")
以下是在AWS控制台中设置它们的方法:
一旦服务验证凭证有效,它会发放一个令牌,允许其持有者以超级用户的身份行动。为此目的,将使用JWT,这是访问令牌的事实标准格式。需要以下包:
"github.com/dgrijalva/jwt-go"
JWT生成代码如下:
package main
import (
"time"
"github.com/dgrijalva/jwt-go"
"os"
)
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{
ExpiresAt: expirationTime.Unix(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtKey)
}
由于拦截此类令牌的对手可能会以超级用户的身份行事,不希望此令牌无限期有效,因为这将授予对手无限的权限。因此,将令牌的有效期设置为一小时。
此时,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网关进行一些额外的工作。