Vert.x JWT认证实战
生成KeyStore
- 通过openssl生成公私钥对:
1
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
106openssl genpkey -algorithm RSA -out private.key -pkeyopt rsa_keygen_bits:2048
````
* 通过**openssl**生成自签名证书
``` bash
openssl req -new -x509 -key private.key -out certificate.crt -days 3650
````
* 通过**openssl**将证书和公私钥存储到**keystore**
* 由于Vert.x的[源码](https://github.com/eclipse-vertx/vertx-auth/blob/76717f72da6977e14394ef4ca67dfca47d430f31/vertx-auth-common/src/main/java/io/vertx/ext/auth/impl/jose/JWK.java#L168)中加载keystore时,仅识别指定的别名,因此这里选择对应的别名:**RS256**,也即**RSA**秘钥及**SHA256**哈希算法
* keystore的密码为:`123456`
``` bash
openssl pkcs12 -export -in certificate.crt -inkey private.key -out keystore.p12 -name "RS256" -password pass:123456
#另外,keystore提取私钥的方式:
openssl pkcs12 -in keystore.p12 -nodes -nocerts -out extracted_private.key -password pass:123456
````
* 将生成的`keystore.p12`文件拷贝到项目工程的`resources`目录下
# JWT认证及授权服务端
* 编写vert.x http服务端,并且使用jwt认证
``` java
package com.example.vertxStudy
import io.vertx.core.Future
import io.vertx.core.VerticleBase
import io.vertx.core.json.JsonObject
import io.vertx.ext.auth.JWTOptions
import io.vertx.ext.auth.KeyStoreOptions
import io.vertx.ext.auth.authorization.PermissionBasedAuthorization
import io.vertx.ext.auth.jwt.JWTAuth
import io.vertx.ext.auth.jwt.JWTAuthOptions
import io.vertx.ext.auth.jwt.authorization.impl.JWTAuthorizationImpl
import io.vertx.ext.web.Router
import io.vertx.ext.web.handler.AuthorizationHandler
import io.vertx.ext.web.handler.BodyHandler
import io.vertx.ext.web.handler.JWTAuthHandler
import io.vertx.ext.web.handler.SecurityAuditLoggerHandler
import io.vertx.kotlin.core.http.httpServerOptionsOf
class JWTAuthVerticle : VerticleBase() {
override fun start(): Future<*> {
val router = Router.router(vertx)
router.route().handler(SecurityAuditLoggerHandler.create()).handler(BodyHandler.create())
val config = JWTAuthOptions()
.setKeyStore(
KeyStoreOptions()
.setType("pkcs12") // 或 "pkcs12"
.setPath("keystore.p12")
.setPassword("123456")
)
.setJWTOptions(JWTOptions().setAlgorithm("RS256"))
val jwtAuthProvider = JWTAuth.create(vertx, config)
val jWTAuthHandler = JWTAuthHandler.create(jwtAuthProvider)
router.post("/login").handler { ctx ->
val body = ctx.body().asJsonObject()
if (body == null) {
ctx.response()
.setStatusCode(400) // Bad Request
.end("Bad Request: Body is missing or not a valid JSON. Please ensure the 'Content-Type' header is 'application/json'.")
return@handler
}
// 现在可以安全地访问 body
val role = body.getString("role")
// 为简单起见,我们为示例用户创建一个token
val claims = JsonObject()
//这里应该使用真正的用户
.put("sub", "987654321")
.put("name", "Nancy")
//指定授权,可以不用放在realm_access下,而是直接放在roles下,这里是为了后面的演示特意为之
.put("realm_access", JsonObject().put("roles", listOf(role)))
val token = jwtAuthProvider.generateToken(claims)
ctx.response().putHeader("content-type", "text/plain").end(token)
}
//指定permission列表所在的路径,需要和login返回的token的路径保持一致
val rootClaim = "realm_access/roles"
val authorizationProvider = AuthorizationHandler
.create(PermissionBasedAuthorization.create("admin"))
.addAuthorizationProvider(JWTAuthorizationImpl(rootClaim))
router.route("/protected")
//先认证身份
.handler(jWTAuthHandler)
//再验证授权(需要有admin的授权)
.handler(authorizationProvider)
router.get("/protected").handler { ctx ->
ctx.response().end("Hello ${ctx.user().get<String>("name")}!")
}
return vertx
.createHttpServer(httpServerOptionsOf(registerWriteHandler = true))
.requestHandler(router)
.listen(8888).onSuccess { http ->
println("HTTP server started on port ${http.actualPort()}")
}
}
}
生成token
方式1
- 打开Online JWT tool,在Decoded Header中输入:
1
{"alg":"RS256","typ":"JWT"}
- 在Decoded Payload中输入授权服务器生成的业务数据(本示例没有admin身份):
1
2
3
4
5
6
7
8
9
10
11{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022,
"realm_access": {
"roles": [
"user"
]
}
} - 在Private Key中,输入
private.key
中的私钥,例如:1
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-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDSXVvT5zcW3laj
0FUZELJrJUpKhsguYCBnWFJIk/54IxEuLh53G6xeI8dWhhSIRwDy6wr/UFfx40pD
44WUBtrOREC599g2yCaUku1b8hf6T7+gU4vpGdBt5HEggGWf9vVsIy6AEFiFx6ta
0wqU2EfX6tVeU37KLmAboRNhQQmsFdxl0TaYv8Z3sVN3UQ61axL/1gpcTQqGw0TC
hvDwXXAodSj6/3LPEts707IJ5S5QKanKEjdLdO0JOdcaR69k5iG+1NMgktQBm5Wb
/ym0rAfP5syC0hhHY+EfLl8sqOzls4u1L4zsr0pTZmDd+IV/GcbeR6saxskQW10J
IWwuNh3FAgMBAAECggEBAIGV1wJS9SGWNwLUTAeTvIS6ROhh3KCMwIlI4+8nk2MI
HwY2jLfMiUK4gYvwLmfAbXo2heC+6l3sKIxndJq6GkFj1Ad7AGrGotV1KNgx6slc
JOwraw1dxIJdbb6auX9vzsEdbwCekRlf5VOem28TSDZex69CeeEr7jI26ExJlx60
fZOVm9f6sqH8xs2w1Subdsjrpcqv11TZJ0D9RwhBZYBnE1bP70LzCUlOYl2KAEFT
ob/oh34Xr0Ra1kdwWPsPbCXFs5JGqEW8IGUZcA/YpADNOEsw2f4arbHFs5nGUrRb
4+dkTVLquIQm3AmQyPucx/Wpd9pfpZqnsgMXUIFvHiECgYEA/uqIMZpLOLk2WhCf
Kk9cPy2pGO7htUBtm5pRkQCD+GUsYYDzrGXQTLLKuH1+Cqr7TnDpu1/CZjJGJnOB
/IsS3LbQ6t5wwXEqfi44vIpOGF9Uw6ESoNQhiYl29QmgqkxeWgIzpMSwmXvwlTJl
9GK4rnzfbh0C4+ysc1chFXjijCkCgYEA00JVeIuwZ31cMggEV1YRWUZwPnBiB8zT
/T2C8kNCO2pAGYnqjzfd2dWu3L9NH2ah4UvKBfIBd0JVOETFh/Z9wEigZjHFeZzC
maziNmUWlDMvtSmzTzP4SOITEvYn+2iSVyx8dQl2OCnRK74o3/RjDRCkpCAUnlaQ
IobAmyM7+D0CgYEAlZOanLhzgPI0sT5llpcAgtXRDh9Fc2w9pHs1d0b9KOh81S2s
TbFkO00B5KVGKw5O7aUVkOvMjtjbDr7iPASC6d2f0uD4+pjjnSyUABWLY4O0oiHG
Y3Z1w1VU5s1iZ+rtxhTapsj+8uClt3XeGzs+MKx3Eg6V3pXGTtuGPyoyjJkCgYEA
0eYXe+T2yWxhnQggIBJQvG3i+fa5P9zAR93E7CXteZEQzQ8dsVylnVjVG1krLGbR
skKWICAaWr7aY0TZKkS7HsBKNh9/gFxDcWK3g6oeq/LLlOkw0iHlr6yHnRBcG0wE
En8NzU8wWY8a16ZhgFqVy5ZcrwF82vFQ1i0i00FHxVUCgYBUc6JC1DePyCApdQXJ
Qn/pYtPwBKSmllLkEkrzUPmJSlWVgdzfynjGU5WF/2DFOaKHxoKW+npaRnXgi3c4
oJs1Ow/nrYFZZSxnXgI/hWCC/+eu2JfHOnPqIcgRniJ25jEINcoK+LjSAVthfad7
vjkKAV0kmZtN6kmFfBNS34k+XA==
-----END PRIVATE KEY----- - 生成token,例如:
1
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImFkbWluIiwidXNlciJdfX0.QPrVDABsyOkI4qUhxhhWis5o21T1xYYJqWw5Itp31jirQLHREvwF3lx0DEkFESguJ95qcnwJH9HHAcOT-y_BGyQaosktnBRJsgedAr4FByRO2hwzrAvUpAYHXcLfzbL_5H8KmuheN5sf3TxdFp3LWdvFSiHhom3oPALIkdrOzwSGx75ET4O6SvTTO86N6XimIPbJuhPRl7QIDzYKMPELiQH7LyooXlS0-hQCRIV6n82wYOH8UM1lM75lp4bUtPM4F3w7__Fc3LFzBKSMiOs-fkn-oYkRpv8d6fbjMA39HkN0AOZj4RmCM54-HCFETIWvLuOZ2Yivt6QgCO-F2Su6SQ
方式2
- 通过
/login
动态下发token1
token=$(curl -XPOST -d '{"role" : "admin"}' -H 'Content-Type: application/json' http://10.12.16.218:8888/login) && echo $token
验证token
- 在curl中发送请求:
1
2
3
4
5
6
7
8
9#针对方式1(且没有admin授权)
$ curl -i -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbInVzZXIiXX19.mQm9BLP-RkKJDaiiuzy-YmtO8-wQyjOpHlb2i_hrrTjwoYFx1AqdP7egvaAhxLdxwgMI9qgstTA-WOA6Vi01jfVoTk0RfV0rzocDDMOHrNaevlqkBSPIB2Jx8EYWQNCTZ8vjvTPezmgWgQjxaA7l21IwzOqA23N-v9ziEdJx46EkRCeee6EO-Bb9jN63DekbiYAz-ccsPT3PxjdFtSklxLmDePNNXeS7HjwnHqbYj6fFAd0RlbXq71lQS5MfgPhkC_SdCjdtOUIe6v6twSMDZcDrpGfX47RY1jDOJ-f-QjnbKbyWdZ6j7_uasgDr9BGlIpcXVV7F4nkeuNMA4kQp7g' http://10.12.16.218:8888/protected
HTTP/1.1 403 Forbidden
content-length: 9
Forbidden
#针对方式2
$ curl -H "Authorization: Bearer ${token}" http://10.12.16.218:8888/protected Hello Nancy!