在构建一个管理页面时,经常需要实现一个认证机制来确保只有授权用户才能访问。本文将介绍如何使用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网关进行一些额外的工作。