第68章 第 68 章:JSON Web Tokens
JSON Web Tokens
介绍
JSON Web Token (JWT) 是一种紧凑且 URL 安全的表示声明(claims)的方式,用于在两个主体之间传输。JWT 中的声明编码为 JSON 对象,用作 JSON Web Signature (JWS) 结构的负载或 JSON Web Encryption (JWE) 结构的明文。这使得声明可以被数字签名或通过消息认证码 (MAC) 进行完整性保护,也可以被加密。
何时使用 JSON Web Tokens?
以下是一些 JSON Web Tokens 有用的场景:
- 授权 (Authorization):这是使用 JWT 最常见的场景。用户登录后,后续每个请求都会包含 JWT,允许用户访问该令牌被授权的路由、服务和资源。单点登录 (Single Sign On) 如今广泛使用 JWT,因为其开销小且能轻松跨不同域名使用。
- 信息交换 (Information Exchange):JSON Web Tokens 是各主体间安全传输信息的好方法。因为 JWT 可以被签名(例如使用公钥/私钥对),你可以确信发送者正是其所声称的身份。此外,由于签名是根据头部和负载计算的,你也可以验证内容未被篡改。
更多详情参阅:https://jwt.io/introduction
在 Iris 中使用 JWT
Iris 的 JWT 中间件以安全、性能和简洁为设计理念,保护你的令牌免受其他库中可能遇到的关键漏洞影响。它基于 kataras/jwt 包。
示例参见:https://github.com/kataras/iris/blob/main/_examples/auth/jwt
示例代码
package main
import (
"time"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/jwt"
)
var (
secret = []byte("signature_hmac_secret_shared_key")
)
type fooClaims struct {
Foo string `json:"foo"`
}
func main() {
app := iris.New()
signer := jwt.NewSigner(jwt.HS256, secret, 10*time.Minute)
// 启用负载加密:
// signer.WithEncryption(encKey, nil)
app.Get("/", generateToken(signer))
verifier := jwt.NewVerifier(jwt.HS256, secret)
// 启用服务端令牌黑名单功能(即使在过期时间之前):
verifier.WithDefaultBlocklist()
// 启用负载解密:
// verifier.WithDecryption(encKey, nil)
verifyMiddleware := verifier.Verify(func() interface{} {
return new(fooClaims)
})
protectedAPI := app.Party("/protected")
// 注册 verify 中间件,仅允许授权客户端访问。
protectedAPI.Use(verifyMiddleware)
// ^ 或使用 UseRouter(verifyMiddleware) 来禁止未授权的 http 错误处理器。
protectedAPI.Get("/", protected)
// 通过服务端使令牌失效,即使它尚未过期。
protectedAPI.Get("/logout", logout)
app.Listen(":8080")
}
func generateToken(signer *jwt.Signer) iris.Handler {
return func(ctx iris.Context) {
claims := fooClaims{Foo: "bar"}
token, err := signer.Sign(claims)
if err != nil {
ctx.StopWithStatus(iris.StatusInternalServerError)
return
}
ctx.Write(token)
}
}
func protected(ctx iris.Context) {
// 获取已验证并解码的声明。
claims := jwt.Get(ctx).(*fooClaims)
// 如果需要操作令牌信息,可选择性地获取它们。
// 这里只是一个示例,展示如何检索
// 所有标准声明(由签名者的最大有效期设置,如 "exp")。
standardClaims := jwt.GetVerifiedToken(ctx).StandardClaims
expiresAtString := standardClaims.ExpiresAt().
Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())
timeLeft := standardClaims.Timeleft()
ctx.Writef("foo=%s\nexpires at: %s\ntime left: %s\n", claims.Foo, expiresAtString, timeLeft)
}
func logout(ctx iris.Context) {
err := ctx.Logout()
if err != nil {
ctx.WriteString(err.Error())
} else {
ctx.Writef("token invalidated, a new token is required to access the protected API")
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
shhttp://localhost:8080 http://localhost:8080/protected?token=$token (或 Authorization: Bearer $token) http://localhost:8080/protected/logout?token=$token http://localhost:8080/protected?token=$token (返回 401)1
2
3
4
分步实现 该中间件包含两个结构体:Signer 和 Verifier。Signer 用于生成 token,Verifier 用于校验请求中的 token。1. 在代码中导入 import "github.com/kataras/iris/v12/middleware/jwt"2. 在 Handler 之外初始化 Signer。算法有多种可选,这里我们使用 HMAC(HS256 共享密钥,稍后在 Verifier 中也会用到同一个密钥):gosigner := jwt.NewSigner(jwt.HS256, []byte("secret"), 15*time.Minute)1
NewSigner 的参数:签名算法签名的私钥(或共享密钥)过期时长3. 声明一个自定义 claims 结构体(可选,也可以直接用 map):go// UserClaims 自定义 claims 结构体。 // * 字段必须导出, // 才能在之后被解码。 type UserClaims struct { Username string `json:"username"` }1
2
3
4
5
6
4. 使用 Sign 生成 token 并发送给客户端:goapp.Post("/signin", func(ctx iris.Context) { claims := UserClaims{ Username: "kataras", } token, err := signer.Sign(claims) if err != nil { ctx.StopWithStatus(iris.StatusInternalServerError) return } ctx.Write(token) })1
2
3
4
5
6
7
8
9
10
11
12
13
4 要校验 token,需在 Handler 之外初始化一个 Verifier:goverifier := jwt.NewVerifier(jwt.HS256, []byte("secret"))1
5 在 Handler 之外,为特定的 claims 类型创建中间件:goverifyMiddleware := verifier.Verify(func() interface{} { return new(UserClaims) // 必须返回指向 claims 结构体类型的指针。 })1
2
3
6 将中间件注册到某个 Party:goprotected := app.Party("/protected") protected.Use(verifyMiddleware)1
2
6.1 或者将中间件注册到单个路由: goapp.Get("/todos", verifyMiddleware, getTodos)1
6.2 或者在 Handler 内部调用中间件(不推荐): gohandler := func(ctx iris.Context) { ok := ctx.Proceed(verifyMiddleware) if !ok { ctx.StopWithStatus(iris.StatusUnauthorized) return } claims := jwt.Get(ctx).(*UserClaims) } app.Get("/protected", handler)1
2
3
4
5
6
7
8
9
10
11
7. 使用包级的 jwt.Get 函数从路由处理程序中获取 claims: goprotected.Get("/todos", func(ctx iris.Context) { claims := jwt.Get(ctx).(*UserClaims) ctx.WriteString(claims.Username) })1
2
3
4
7.1 如果 claims 实现了一个或多个 Context.User 方法,可以获取 Context 中的 User: gouser := ctx.User() username, _ := user.GetUsername()1
2
7.2 获取 Verified Token 信息: goverifiedToken := jwt.GetVerifiedToken(ctx) verifiedToken.Token // 原始请求 token。1
2
VerifiedToken 结构如下: gotype VerifiedToken struct { Token []byte // 原始 token。 Header []byte // Header(解码后)部分。 Payload []byte // Payload(解码后)部分。 Signature []byte // 签名(解码后)部分。 StandardClaims Claims // 从 payload 中提取的任意标准 claims。 }1
2
3
4
5
6
7
Token 提取器 默认情况下,新的 Verifier 会从 URL 参数 ?token=$token 或请求头 Authorization: Bearer $token 中提取 token。你可以修改其 Extractors []TokenExtractor 字段来改变这一行为。 TokenExtractor 类型如下: gotype TokenExtractor func(iris.Context) string1
中间件提供了 3 个令牌提取器辅助函数:
FromHeader - 从 Authorization: Bearer {token} 请求头中提取令牌(默认行为)。
FromQuery - 从 URL 查询参数 "token" 中提取令牌(默认行为)。
FromJSON(jsonKey string) - 从请求载荷(body)中提取令牌,指定键名为 "jsonKey"。例如,FromJSON("access_token") 会从如下请求体中获取令牌:{"access_token": "$TOKEN", ...}。
Verifier.Extractors 字段(NewVerifier 函数的返回值)默认为 []TokenExtractor{FromHeader, FromQuery},即先尝试从 Authorization: Bearer 请求头读取令牌,若未找到,则尝试通过 "token" URL 查询参数提取。你可以根据应用需求自定义该切片,例如添加一个自定义提取器:
verifier := jwt.NewVerifier(jwt.HS256, []byte("secret"))
verifier.Extractors = append(verifier.Extractors, func(ctx iris.Context) string {
// 返回请求中的原始令牌字符串。
//
// 为演示起见,若之前的所有提取器都失败,
// 我们将尝试通过自定义请求头 "X-Token" 来获取令牌:
return ctx.GetHeader("X-Token")
})
若希望仅从 Authorization 请求头中提取令牌:
verifier.Extractors = []jwt.TokenExtractor{jwt.FromHeader}
令牌载荷加密
JWE(加密的 JWT)不在本中间件的讨论范围内,取而代之的是提供令牌载荷的线路加密以保护数据。如果应用需要传输包含敏感数据的令牌,则需在签名(Sign)时加密数据,并在验证(Verify)时解密。可调用 Signer.WithEncryption 和 Verifier.WithDecryption 方法应用任意类型的加密。
该中间件提供了一种最流行且常用的数据安全方式:GCM 模式 + AES 密码套件。我们遵循大多数研究人员推荐的“先加密后签名”流程(这更安全,可防止填充预言攻击)。
signer := jwt.NewSigner(jwt.HS256, []byte("secret"), 15*time.Minute)
// key 参数应为 AES 密钥,
// 长度设为 16、24 或 32 字节,
// 以选择 AES-128、AES-192 或 AES-256。
//
// 第二个参数是附加数据,可以为 nil。
signer.WithEncryption([]byte("itsa16bytesecret"), nil)
以及 Verifier(验证器),用于解密加密后的载荷:
goverifier := jwt.NewVerifier(jwt.HS256, []byte("secret"))
verifier.WithDecryption([]byte("itsa16bytesecret"), nil)
// [...]
阻塞 Token
用户登出时,客户端应删除内存中的 token,防止其继续发起授权请求。但只要该 token 未过期且被他人获取,仍可能被滥用。因此,服务端无效化对这类场景确实有效:服务端收到登出请求后,取出 token 并通过 Context.Logout 方法写入 Blocklist;后续每次授权请求在 Verifier.Verify 校验时都会查询 Blocklist,确认 token 是否已被拉黑。为控制查询空间,过期 token 会自动从 Blocklist 中清除。
Iris JWT 中间件提供两种 Blocklist:In-Memory(内存)与 Redis。
示例代码(仔细阅读注释):
gopackage main
import (
"time"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/jwt"
"github.com/kataras/iris/v12/middleware/jwt/blocklist/redis"
// 可选:设置 token 标识
"github.com/google/uuid"
)
var (
signatureSharedKey = []byte("sercrethatmaycontainch@r32length")
signer = jwt.NewSigner(jwt.HS256, signatureSharedKey, 15*time.Minute)
verifier = jwt.NewVerifier(jwt.HS256, signatureSharedKey)
)
type userClaims struct {
Username string `json:"username"`
}
func main() {
app := iris.New()
// 重要提示
//
// 若使用内存版 blocklist,只需:
// verifier.WithDefaultBlocklist()
// 若使用持久化 blocklist(如 Redis):
// 先启动 redis-server,然后:
blocklist := redis.NewBlocklist()
// 配置单节点或集群客户端:
// blocklist.ClientOptions.Addr = "127.0.0.1:6379"
// blocklist.ClusterOptions.Addrs = []string{...}
// 为 jwt id 设置前缀:
// blocklist.Prefix = "myapp-"
//
// 如需手动连接并检查错误后再继续:
// err := blocklist.Connect()
// 默认情况下,verifier 会尝试连接;失败则抛出 HTTP 错误。
//
// 注册 blocklist:
verifier.Blocklist = blocklist
verifyMiddleware := verifier.Verify(func() interface{} {
return new(userClaims)
})
app.Get("/", authenticate)
protectedAPI := app.Party("/protected", verifyMiddleware)
protectedAPI.Get("/", protected)
protectedAPI.Get("/logout", logout)
// http://localhost:8080
// http://localhost:8080/protected?token=$token
// http://localhost:8080/logout?token=$token
// http://localhost:8080/protected?token=$token (401)
app.Listen(":8080")
}
func authenticate(ctx iris.Context) {
claims := userClaims{
Username: "kataras",
}
// 生成 JWT ID
random, err := uuid.NewRandom()
if err != nil {
ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
id := random.String()
// 通过 jwt.ID 设置 ID
token, err := signer.Sign(claims, jwt.ID(id))
if err != nil {
ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
ctx.Write(token)
}
func protected(ctx iris.Context) {
claims := jwt.Get(ctx).(*userClaims)
// 标准 claims(例如自动生成的 ID)可访问:
// jwt.GetVerifiedToken(ctx).StandardClaims.ID
ctx.WriteString(claims.Username)
}
func logout(ctx iris.Context) {
ctx.Logout()
ctx.Redirect("/", iris.StatusTemporaryRedirect)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
默认情况下,唯一标识符通过 "jti"(Claims{ID})获取,如果为空则直接用原始 token 作为 map 的键。想修改这个行为,只需更改 blocklist.GetKey 字段即可。 JSON Web Algorithms JWA(JSON Web Algorithms)规范定义了多种签名算法。规范要求所有实现必须支持一种算法: 使用 SHA-256 的 HMAC,在 JWA 规范中称为 HS256。 规范还定义了一系列推荐算法: 使用 SHA-256 的 RSASSA PKCS1 v1.5,在 JWA 规范中称为 RS256。 使用 P-256 和 SHA-256 的 ECDSA,在 JWA 规范中称为 ES256。 本实现支持以上所有算法,外加 RSA-PSS 和新的 Ed25519。详见 alg.go 源码文件。简要如下:
| Algorithm | jwt.Sign | jwt.Verify |
|---|---|---|
| jwt.HS256 / HS384 / HS512 | []byte | The same sign key |
| jwt.RS256 / RS384 / RS512 | *rsa.PrivateKey | *rsa.PublicKey |
| jwt.PS256 / PS384 / PS512 | *rsa.PrivateKey | *rsa.PublicKey |
| jwt.ES256 / ES384 / ES512 | *ecdsa.PrivateKey | *ecdsa.PublicKey |
| jwt.EdDSA | ed25519.PrivateKey | ed25519.PublicKey |
2
3
4
5
6
```html
// 生成 HMAC
sharedKey := make([]byte, 32)
_, _ = rand.Read(sharedKey)
// 生成 RSA
bitSize := 2048
privateKey, _ := rsa.GenerateKey(rand.Reader, bitSize)
publicKey := &privateKey.PublicKey
// 生成 ECDSA
c := elliptic.P256()
privateKey, _ := ecdsa.GenerateKey(c, rand.Reader)
publicKey := &privateKey.PublicKey
// 生成 EdDSA
publicKey, privateKey, _ := ed25519.GenerateKey(rand.Reader)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
加载与解析密钥
该包提供了加载和解析 PEM 格式密钥所需的所有辅助函数。
所有可用的辅助函数:
// HMAC
MustLoadHMAC(filenameOrRaw string) []byte2
// RSA
MustLoadRSA(privFile, pubFile string) (*rsa.PrivateKey, *rsa.PublicKey)
LoadPrivateKeyRSA(filename string) (*rsa.PrivateKey, error)
LoadPublicKeyRSA(filename string) (*rsa.PublicKey, error)
ParsePrivateKeyRSA(key []byte) (*rsa.PrivateKey, error)
ParsePublicKeyRSA(key []byte) (*rsa.PublicKey, error)2
3
4
5
6
// ECDSA
MustLoadECDSA(privFile, pubFile string) (*ecdsa.PrivateKey, *ecdsa.PublicKey)
LoadPrivateKeyECDSA(filename string) (*ecdsa.PrivateKey, error)
LoadPublicKeyECDSA(filename string) (*ecdsa.PublicKey, error)
ParsePrivateKeyECDSA(key []byte) (*ecdsa.PrivateKey, error)
ParsePublicKeyECDSA(key []byte) (*ecdsa.PublicKey, error)2
3
4
5
6
// EdDSA
MustLoadEdDSA(privFile, pubFile string) (ed25519.PrivateKey, ed25519.PublicKey)
LoadPrivateKeyEdDSA(filename string) (ed25519.PrivateKey, error)
LoadPublicKeyEdDSA(filename string) (ed25519.PublicKey, error)
ParsePrivateKeyEdDSA(key []byte) (ed25519.PrivateKey, error)
ParsePublicKeyEdDSA(key []byte) (ed25519.PublicKey, error)2
3
4
5
6
示例代码
import "github.com/kataras/iris/v12/middleware/jwt"privateKey, publicKey := jwt.MustLoadEdDSA("./private_key.pem", "./public_key.pem")
signer := jwt.NewSigner(jwt.EdDSA, privateKey, 15*time.Minute)
verifier := jwt.NewVerifier(jwt.EdDSA, publicKey)2
3
4
``` Refresh Token Access tokens 用于在不访问数据库的情况下识别用户。Refresh tokens 则用于刷新已过期的 access tokens。当 access token 失效后,refresh token 将被用来获取一对新的 access token 和 refresh token。 示例代码: `go` package main import ( "fmt" "time" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/middleware/jwt" ) const ( accessTokenMaxAge = 10 * time.Minute refreshTokenMaxAge = time.Hour ) var ( privateKey, publicKey = jwt.MustLoadRSA("rsa_private_key.pem", "rsa_public_key.pem") signer = jwt.NewSigner(jwt.RS256, privateKey, accessTokenMaxAge) verifier = jwt.NewVerifier(jwt.RS256, publicKey) ) // UserClaims 自定义的 access claims 结构体。 type UserClaims struct { ID string `json:"user_id"` // 使用 `json:"username,required"` 可让此字段变为必填项 // 或者参考下方的 Validate 方法。 Username string `json:"username"` } // GetID 实现了部分上下文中的用户 ID 接口。 // 注意,如果 claims 是一个 map,则无需实现任何方法, // claims 的值会自动转换为 UserClaims。 // // 当使用多种认证方式(如 basic auth、jwt)时, // 但它们共享某些方法时,这种方式很有用。 func (u *UserClaims) GetID() string { return u.ID } // GetUsername 实现了部分上下文中的用户名接口。 func (u *UserClaims) GetUsername() string { return u.Username } // Validate 完成了中间件的自定义 ClaimsValidator。 // 此方法不接受缺少 username 字段的 token // (这在避免接受由相同算法生成的 refresh token 时很有用)。 func (u *UserClaims) Validate() error { if u.Username == "" { return fmt.Errorf("username field is missing") } return nil } // 对于 refresh token,我们直接使用包含标准 JWT 字段的 // jwt.Claims 结构体。 func main() { app := iris.New() app.OnErrorCode(iris.StatusUnauthorized, handleUnauthorized) app.Get("/authenticate", generateTokenPair) app.Get("/refresh", refreshToken) protectedAPI := app.Party("/protected") { verifyMiddleware := verifier.Verify(func() interface{} { return new(UserClaims) }) protectedAPI.Use(verifyMiddleware) protectedAPI.Get("/", func(ctx iris.Context) { // 通过 jwt.Get 访问 claims: // claims := jwt.Get(ctx).(*UserClaims) // ctx.Writef("Username: %s\n", claims.Username) // // 或通过 context 的 user 访问(如果我们的 UserClaims 实现了至少一个方法): user := ctx.User() id, _ := user.GetID() username, _ := user.GetUsername() ctx.Writef("ID: %s\nUsername: %s\n", id, username) }) } // http://localhost:8080/protected (401) // http://localhost:8080/authenticate (200) (响应 JSON {access_token, refresh_token}) // http://localhost:8080/protected?token={access_token} (200) // http://localhost:8080/protected?token={refresh_token} (401) // http://localhost:8080/refresh?refresh_token={refresh_token} // 或 http://localhost:8080/refresh (请求 JSON{refresh_token = {refresh_token}}) (200) (响应 JSON {access_token, refresh_token}) // http://localhost:8080/refresh?refresh_token={access_token} (401) app.Listen(":8080") } func generateTokenPair(ctx iris.Context) { // 模拟一个用户... userID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149" // 将当前用户与 refresh token 映射, // 以便在 refresh 路由中确保该 refresh token // 属于该用户,然后再生成新的 token。 refreshClaims := jwt.Claims{Subject: userID} accessClaims := UserClaims{ ID: userID, Username: "kataras", } // 生成一对 token,refresh token 的生命周期较长,例如 1 小时。 // 第一个参数是 access claims, // 第二个参数是 refresh claims, // 第三个参数是 refresh token 的最大有效期。 tokenPair, err := signer.NewTokenPair(accessClaims, refreshClaims, refreshTokenMaxAge) if err != nil { ctx.Application().Logger().Errorf("token pair: %v", err) ctx.StopWithStatus(iris.StatusInternalServerError) return } // 将生成的 token 对发送给客户端。 // tokenPair 看起来像这样:{"access_token": $token, "refresh_token": $token} ctx.JSON(tokenPair) } // 根据应用需求,refresh token 有各种处理方式。 // 在这个示例中,我们只接受 refresh token, // 只验证 refresh token,并重新生成整对新的 token。 // 另一种方式是同时接受 access 和 refresh token, // 验证 refresh token,并设置 Leeway 时间验证 access token, // 检查其是否即将过期,然后生成单个 access token。 func refreshToken(ctx iris.Context) { // 假设你有权访问当前用户,例如通过 sessions。 // // 模拟一次针对我们 jwt subject 的数据库调用, // 以确认该 refresh token 是由该用户生成的一对 token。 // * 注意:你可以移除 ExpectSubject,稍后自行进行此验证。 currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149" // 从 ?refresh_token=$token 获取 refresh token,或者 // 从请求体中的 JSON{"refresh_token": "$token"} 获取。 refreshToken := []byte(ctx.URLParam("refresh_token")) if len(refreshToken) == 0 { // 你也可以通过 ctx.GetBody/ReadBody 读取整个 body。 var tokenPair jwt.TokenPair if err := ctx.ReadJSON(&tokenPair); err != nil { ctx.StopWithError(iris.StatusBadRequest, err) return } refreshToken = tokenPair.RefreshToken } // 验证 refresh token,其 subject 必须匹配 "currentUserID"。 _, err := verifier.VerifyToken(refreshToken, jwt.Expected{Subject: currentUserID}) if err != nil { ctx.Application().Logger().Errorf("verify refresh token: %v", err) ctx.StatusCode(iris.StatusUnauthorized) return } /* 也可以在 Verify 调用之后执行自定义验证检查: currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149" userID := verifiedToken.StandardClaims.Subject if userID != currentUserID { ctx.StopWithStatus(iris.StatusUnauthorized) return } */ // 一切正常,重新生成新的 token 对并发送给客户端, // 我们也可以选择只生成 access token。 generateTokenPair(ctx) } func handleUnauthorized(ctx iris.Context) { if err := ctx.GetErr(); err != nil { ctx.Application().Logger().Errorf("unauthorized: %v", err) } ctx.WriteString("Unauthorized") }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183