构建轻量级认证服务

在构建一个管理页面时,经常需要实现一个认证机制来确保只有授权用户才能访问。本文将介绍如何使用Go语言AWS Lambda创建一个轻量级的认证服务。这种服务不仅成本低廉,而且由于其无服务器的特性,可以有效地将核心业务逻辑与横切关注点分离。

项目结构

为了将认证逻辑与函数即服务(FaaS)的内部实现解耦,项目将包含两个文件:

  • auth.go - 存放认证逻辑。
  • main.go - 与AWS Lambda集成的逻辑。

main.go 内容

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) }

为了使代码能够工作,需要引入以下包:

  • "github.com/aws/aws-lambda-go/lambda"
  • "github.com/aws/aws-lambda-go/events"

认证

在应用中,省略了持久存储的使用,因为只需要一对凭证。然而,仍然需要使用哈希函数存储密码,以便在合理的时间内验证密码,同时要求攻击者需要大量的资源来猜测哈希密码。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令牌生成

一旦服务验证凭证有效,它会发放一个令牌,允许其持有者以超级用户的身份行动。为此目的,将使用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网关

此时,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网关进行一些额外的工作。

  • 端点限制:默认设置对于不经常调用的认证函数来说太高了。让改变这一点。
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485