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
    106
    openssl 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动态下发token
    1
    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!