ทำให้ Token ซับซ้อนด้วย JWT ใน Spring Boot
เราได้เพิ่มความปลอดภัยให้กับ Microservice ด้วยการใช้ OAuth2 protocal ซึ่งจากที่เราใช้ Basic Auth มาเป็นใช้ Token ในการ Authentication ของ Microservice แต่ใน Resource Server หรือตัว Microservice ของเราก็ต้องไปถาม Authorization Server ว่า Token ที่ได้มานั้นถูกต้องจริงไหม ทำให้ Microservice ไม่อิสระจะต้องขึ้นอยู่กับ Authorization Server ตลอดเวลาเมื่อมีการ Authentication
แต่ก็มีอีกวิธีหนึ่งคือการใช้ Token ที่เป็นแบบ JWT ที่ Authorization Server จะเข้ารหัสมาด้วย Key และที่ Resource Server ก็ใช้ Key เดียวกันในการถอดรหัสในการ Authentication ทำให้ Resource Server เป็นอิสระจาก Authorization Sever ได้ แต่ก็ต้องมี Key ที่ใช้ถอดรหัสกันได้
JWT คืออะไร
JSON Web Token (JWT) เป็น token ในรูปแบบ JSON สำหรับสร้าง access token ที่สามารถใส่ค่าบางอย่างไว้สำหรับตรวจสอบได้ ด้วยมาตรฐาน RFC 7519
ที่เป็น Stateless Authentication นั้นคือ state ของ user จะไม่ถูกเก็บไว้ที่ server ประกอบด้วย 3 ส่วน แยกกันด้วย จุด (.) คือ header
.payload
.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJleHAiOjE1MzY5NDU0MTksInVzZXJfbmFtZSI6ImpvaG4iLCJqdGkiOiI2M2Q2MDhhOS1hYTc4LTRmZTYtYWNlNy00NTE4ZjQ0YzNmODEiLCJjbGllbnRfaWQiOiJjbGllbnRJZCIsInNjb3BlIjpbIm9wZW5pZCJdfQ
.zFycCczWj1kMKT3LXvbh-kpTK10sNT9y9gFDKxpnzok
Header
เป็นส่วนหัวของ token มี 2 ส่วนคือ hasing algorithm ที่ใช้และ type ของ token โดยจะถูกเข้ารหัสด้วย Base64Encoded ถอดรหัสจะได้ดังนี้
{
"alg": "HS256",
"typ": "JWT"
}
ใช้ hasing algorithm เป็น HS256
และ type แบบ JWT
Payload
เป็นส่วนของการ claim ของ token หรือเป็นส่วนของ ข้อมูล ของ tokenโดยจะถูกเข้ารหัสด้วย Base64Encoded ถอดรหัสจะได้ดังนี้
{
"exp": 1536945419,
"user_name": "john",
"jti": "63d608a9-aa78-4fe6-ace7-4518f44c3f81",
"client_id": "clientId",
"scope": [
"openid"
]
}
Signature
เป็นส่วนที่ทำให้เชื่อว่า token ไม่ได้ถูกเปลี่ยนแปลงระหว่างทาง ซึ่งเป็นค่าที่ได้จากการคำนวน จากสูตรนี้
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
เราสามารใช้เว็บ jwt.io ในการตรวจสอบ JWT token ได้โดยเว็บนี้จะ decode ให้และตรวจสอบ Signature ได้ด้วย
เพิ่ม JWT
Authorization Server
เรามา refactor Authorization Server ให้เปลี่ยนจากใช้ generated token มาเป็น JWT ด้วยการเปลี่ยนจาก application config มาเป็น Java config โดยสร้างคลาสที่ เป็น @Configuration ที่ extents จาก AuthorizationServerConfigurerAdapter
โดยการแปลง token ด้วย JwtTokenStore
ด้วย key 123456789
ดังนี้
ตัวอย่างใช้ key ง่ายๆๆ
Resource Server
ต่อมา refactor Resource Server ให้รองรับการ JWT token ด้วยการสร้างคลาสที่เป็น @Configuration ที่ extends มาจาก ResourceServerConfigurerAdapter
ด้วยการตรวจสอบ token ที่ได้รับด้วย JwtTokenStore
ด้วย key เดียวกับ Authorization Server ดังนี
จะเห็นได้ว่าการ Authentication แบบที่ใช้ key เดียวทั้งสองฝั่งนี้ใช้วิธี Symmetic cryptography ซึ่งเป็นวิธีที่ง่ายแต่ก็ต้องเก็บรักษา key นี้ไว้ให้ปลอดภัย
ทดสอบ JWT เบื้องต้น
หลังจากที่เราได้ refactor แล้วทั้ง 2 ฝั่งทั้ง Authorization Server และ Resource Server ก็ลองรันทั้ง 2 และลองใช้ Postman request ไปที่ Authorization Server จะได้ token ในรูปแบบของ JWT
ใช้ access token ที่ได้รับมา request ไปที่ Resource Server ถ้าถูกต้องจะได้ code 201 (Created) สำหรับ POST
ลองแก้ไขส่วน payload ของ JWT token และ request ไปที่ Resource Server อีกครั้งจะได้ code 401 (Unauthorized)
จะเห็นได้ว่า JWT token ใน OAuth2 ของเราทำงานได้ดี
ใช้ Asymmetric Key
สร้าง Java KeyStore
เริ่มแรกเราต้องสร้าง Java KeyStore file (JKS) ก่อน ด้วย keytoole
โดยใช้คำสั่ง
$ keytool -genkeypair -alias myapi -keyalg RSA -keystore apikey.jks -deststoretype pkcs12
ใส่ keystore password และข้อมูลต่างๆ เสร็จแล้วก็จะได้ไฟล์ myapi.jks
ซึ่งเก็บ key ของเราอยู่ทั้ง private key และ public key และ copy ไฟล์ไปใส่ใน resource ของ Authorization Server
Export Public Key
ต่อไปเราต้อง export Public Key ที่เราสร้าง Java KeyStore ด้วยคำสั่งนี้
$ keytool -list -rfc --keystore apikey.jks | openssl x509 -inform pem -pubkey
เก็บส่วนของ public key ไว้ในไฟล์ public.pem
และ copy ไฟล์ไปใส่ใน resource ของ Resource Server
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtdYbbdEvjmP5wiTlGhFH
OemY+HCsk2vTbchJt0ed4o2DJh+x9cQ8DuyXMAnVPWNXq3kb1wxEIUXtkHTiaBHk
MoumUgB0Ri4AD13ZHgrl9wyHtPrAMHjIClBSua5mQakaA6s1X5RIJiCmtDERo3Rz
zpACqAzXqrIbVa79zIECenOdpcLrCE2Xas1EVWN5oIMlG1ZZ+PR1wgicipUDjriY
2rtuHwL4/Z8d3cOYuJJ2PVPW/Ubk44Lm8Y2dJ2Rg3/pn5PP5PyeV/PijUHxYbqMe
IOqaMGvdVk/Mk/Lw7PMnkQVJe1uxTMyeml5JgdvFbkNHitRFQ+WEDvBJfczZ7C45
UQIDAQAB
-----END PUBLIC KEY-----
Authorization Server
หลังจากที่ได้ Java KeyStore แล้วก็ refactor JwtAccessTokenConverter
ให้อ่านไฟล์ apikey.jks ด้วย KeyStoreKeyFactory
แล้ว setKeyPair
Resource Server
หลังจากที่เอา Public key ไปใส่ไว้ใน class resource ของ project แล้วก็ refactor JwtAccessTokenConverter
ให้อ่านไฟล์ public.pem ได้เป็น String แล้วใส่เป็น verify key เพื่อใช้ verify JWT token ที่ request มา
ทดสอบ JWT (Asymmetric Key)
ลองทดสอบว่า JWT ด้วย Asymmetric Key ทำงานได้ไหม ก็ใช้วิธีทดสอบเหมือนข้างต้นเลย ถ้าทำงานได้จะได้ส่วน Signature ที่ได้จาก Authorization Server จะมีขนาดยาวกว่าตอนต้น
ใช้ JWT token ที่ได้มา request ไปที่ Resource Server ถูกต้องจะได้ code 201 (Created) สำหรับ POST
สรุป
จากที่ได้เพิ่ม JWT แทน token แบบเดิม ทำให้ Resource Server ไม่จำเป็นต้องตรวจสอบ token ทุกครั้งกับ Authorization Server โดยวิธีแรกจะใช้ key เดียวกันทั้ง 2 ฝั่ง ที่เรียกว่า Symmetric key ในการสร้างส่วนของ signature และวิธีที่สองเป็นใช้คนล่ะ key โดยที่ Authorization Server ใช้ private key และ Resource Server ใช้ public key ทำให้ไม่จำเป็นต้องรู้ key จริงว่าเป็นอะไรก็สามารถ verify signature ได้