基於JWT規範實現的認證微服務
本文由公眾號EAWorld翻譯發表,轉載需註明出處。
作者:Marcelo Fonseca
譯者:白小白
原題:Building an authentication micro-service with JWT standard
原文:
全文2326字,閱讀約需要5分鐘
目錄:
一、微服務介紹
二、隨之而來的認證和授權問題
三、專案架構通訊
四、用於簽名以及驗證的公鑰和私鑰令牌
五、專案資料庫同步問題
一、微服務介紹
微服務日漸流行,幾乎所有流行語言都提供了兩種框架實現,一是面向Web開發的大型框架,一是面向小型應用的微框架。輕量級框架作為微服務架構來說,是個好的選擇。微服務架構有很多優勢,諸如高可維護性,獨立部署等等。微服務架構讓我們可以針對特定語言選擇最優的解決方案來建立特定的服務,比如,針對爬蟲類應用或者AI場景,我們可以選擇建立一個Python服務;針對加密庫的場景建立JS服務;針對Active Record的場景建立Ruby服務等等。基於這樣的理念,我們不需要受限於使用單一語言來建立整個後端服務。
下面我列出了各種語言提供的微框架列表:
Python - Flask
Javascript - ExpressJS
Ruby - Sinatra
Go - Martini
Java - Spark
C# - nancy
C++ - Crow
PHP - silex
二、隨之而來的認證和授權問題
在微服務架構下,前後端的認證邏輯相比常規的CS應用要複雜的多。客戶端與後端的API伺服器並不是一對一的關係,我們需要管理很多的後端服務,需要對更多的應用路由提供保護。為了解決這一問題,人們實踐了很多方式來建立微服務架構下的認證和授權邏輯。本文展示了其中一種方案,基於JSON Web Tokens(JWT)標準來實現一個簡單的認證和授權服務。
三、專案架構通訊
簡化起見,示例中只實現了兩個後端服務。我將建立一個用於認證和授權的expressJS應用,以及一個Sinatra應用來作為部落格服務的後端。目前為止,在本例 中將有兩個後端以及一個前端。
下面介紹一下應用間通訊的實現機制。
前後端通訊機制
ExpressJS實現了前端應用的使用者註冊和登陸。
如果認證成功,ExpressJS應用將返回一個JWT令牌。
前端將這一令牌附加在請求的訊息頭中用以訪問Sinatra應用資料。
服務間通訊機制
當我們需要實現後端之間的通訊時,就需要利用這樣的機制。作為示例場景,假設還有一個Flask API後端用於爬取網路上的內容,並更新Sinatra部落格應用中的資料。這樣我們就一共有了三個後端和一個前端。
Flask應用向ExpressJS應用請求JWT令牌。
請求成功後,ExpressJS應用返回令牌。
Flask應用將令牌附加在請求的訊息頭,並訪問Sinatra應用的後端路由。
此處需要注意兩件事。無論是使用者發出請求或者後端發出請求,都需要合法的身份來進行認證以及訪問其他後端。但作為後端服務來講是不會使用郵件和密碼的,而是以API秘鑰作為身份的證明代之。比如,Flask應用向ExpressJS應用的路由傳送一個登陸秘鑰,只要秘鑰是正確的,就可以授權Flask服務獲得JWT令牌。
四、用於簽名以及驗證的
公鑰和私鑰令牌
在這套架構下,所有的微服務應用將使用其自身的JWT庫來對訪問請求進行認證並保護其API路由。此處我們將使用JWT RSA256策略。認證服務ExpressJS將同時持有私鑰和公鑰。使用私鑰來對使用者或應用的令牌進行簽名,用公鑰對令牌進行解碼和驗證。其他服務將僅持有公鑰來進行驗證。
使用RSA演算法需要生成一個公鑰/私鑰對。可以透過如下的程式碼在終端中實現,作為執行結果,程式碼將生成.pem檔案:
openssl genrsa -des3 -out private.pem 2048openssl rsa -in private.pem -outform PEM -pubout -out public.pem
(左右滑動檢視全部程式碼)
簽名令牌
在使用者或者API的登陸路由中實現令牌簽名。下面的程式碼示例了ExpressJS認證服務的使用者登陸路由。只要使用者身份是合法的,程式碼將訪問私鑰rsa2048priv.pem並且簽名一個新的JWT令牌。
// User sign-in route with JWT RSA algorithm example
var User = require('../models/user')
var express = require('express');
var router = express.Router();
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const fs = require('fs');
router.route('/sign-in').post(function(req, res, next){
User.find({ email: req.body.email}).then(user => {
if (user.length < 1)
return res.status(400).json({message: 'Authentication failed.'});
bcrypt.compare(req.body.password, user[0].passwordHash, (err, success) => {
if(success){
let cert = fs.readFileSync('../rsa_2048_priv.pem');
const token = jwt.sign(
{
email: user[0].email,
//id: user[0]._id,
},
cert,
{
expiresIn: '1h',
algorithm: 'RS256',
issuer: user[0].role,
}
);
res.status(200).json({token: token, message: 'Successfully authenticated.'});
}else
return res.status(400).json({message: 'Authentication failed.'});
});
});
});
(左右滑動檢視全部程式碼)
驗證令牌
所有的服務都需要對持有合法JWT令牌的進站請求進行驗證。這可以透過在應用中建立一箇中介軟體來實現。這一中介軟體將訪問公鑰pem檔案來對令牌進行解碼和驗證。在ExpressJS或者Sinatra服務中,這樣的中介軟體程式碼類似如下所示。
ExpressJS認證和授權中介軟體程式碼:
// JWT authentication middleware example.
// Uses RS256 strategy with .pem key pair files.
const fs = require('fs');
const jwt = require('jsonwebtoken');
module.exports = (req, res, next) => {
let publicKey = fs.readFileSync('../rsa_2048_pub.pem');
try{
const token = req.headers.authorization.split(' ')[1]; //req.headers.token;
console.log(token);
var decoded = jwt.verify(token, publicKey)
console.log(decoded);
next();
}catch(err){
return res.status(401).json({error: err, message: 'Invalid token.'});
}
};
(左右滑動檢視全部程式碼)
Sinatra認證和授權中介軟體程式碼:
# To connect this middleware.rb file to your sinatra app
# add 'use JWTAuthorization' as one of your first lines in
# your Application class.
# e.g.
# require 'middlewares.rb'
# class Application < Sinatra::Base
# use JWTAuthorization
# ...
# end
require 'sinatra/json'
require 'jwt'
class JWTAuthorization
def initialize app
@app = app
end
def call env
begin
# env.fetch gets http header
# bearer = env.fetch('HTTP_AUTHORIZATION', '').split(' ')[1] # also work
bearer = env.fetch('HTTP_AUTHORIZATION').slice(7..-1) # gets JWT token
key = OpenSSL::PKey::RSA.new File.read '../rsa_2048_pub.pem' # read public key pem file
payload = JWT.decode bearer, key, true, { algorithm: 'RS256'} # decode and verify token with pub key
claims = payload.first
# current_user is defined by env[:user].
# useful to define current_user if you are using pundit gem
if claims['iss'] == 'user'
env[:user] = User.find_by_email(claims['email'])
end
# access your claims here...
@app.call env
rescue JWT::DecodeError
[401, { 'Content-Type' => 'text/plain' }, ['A token must be passed.']]
rescue JWT::ExpiredSignature
[403, { 'Content-Type' => 'text/plain' }, ['The token has expired.']]
rescue JWT::InvalidIssuerError
[403, { 'Content-Type' => 'text/plain' }, ['The token does not have a valid issuer.']]
rescue JWT::InvalidIatError
[403, { 'Content-Type' => 'text/plain' }, ['The token does not have a valid "issued at" time.']]
# useful only if using pundit gem
rescue Pundit::NotAuthorizedError
[401, { 'Content-Type' => 'text/plain' }, ['Unauthorized access.']]
end
end
end
(左右滑動檢視全部程式碼)
五、專案資料庫同步問題
將部落格服務和認證服務分離,將引發同步問題。原因之一是,兩者都需要各自儲存使用者資訊。ExpressJS需要用到使用者的身份資訊,而Sinatra需要用到其他的使用者資訊(比如頭像,個人描述以及發帖、評論資料之間的關聯關係等),對於這個問題可以有多種解決方案:
方案一:在認證服務的使用者表中儲存全部使用者資訊。在部落格服務的使用者表中將僅儲存使用者的ExpressJS服務ID(即user_id)以用來在認證服務中索引和查詢使用者資料。
方案二:在部落格服務中不設使用者表。所有涉及到使用者資料的部落格資料庫表都將儲存ExpressJS使用者ID作為索引。
方案三:在認證服務中僅儲存身份資訊(如郵件地址和密碼),其餘的資訊儲存在部落格服務中。當需要在部落格服務中引用認證服務的使用者資料時,以使用者ID或者郵件地址作為唯一索引來關聯,當使用郵件地址時,需要在部落格服務中同時儲存使用者的郵件地址。
可以按自己的實際情況從上述的方案中做出選擇。我會選擇第三個方案,讓每個服務僅儲存自己所需要的合理的資料。這樣,只需要少量的程式碼修改,我就可以在未來的專案中複用這一認證服務,以期在Sinatra應用中充分利用Ruby的Active Record機制來進行使用者關係建模和查詢。要謹慎的時刻保持應用間的使用者資料同步,比如,如果在ExpressJS應用中刪除或者新建了一條使用者資訊,確保這一變更同步到Sinatra應用。
關於EAWorld:微服務,DevOps,資料治理,移動架構原創技術分享,長按二維碼關注
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31562043/viewspace-2637555/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 基於 go-zero 輕鬆實現 JWT 認證GoJWT
- go-kit微服務:JWT身份認證Go微服務JWT
- 基於JWT標準的使用者認證介面實現JWT
- koa 實現 jwt 認證JWT
- 微服務整合springsecurity實現認證微服務SpringGse
- 基於 JWT + Refresh Token 的使用者認證實踐JWT
- ASP.NET Core 基於JWT的認證(一)ASP.NETJWT
- ASP.NET Core 基於JWT的認證(二)ASP.NETJWT
- jwt 如何實現一個服務認證了另一個服務也可以認證JWT
- 實現基於JWT的Token登入驗證功能JWT
- 微服務架構 | 7.1 基於 OAuth2 的安全認證微服務架構OAuth
- 基於gin的golang web開發:認證利器jwtGolangWebJWT
- JWT實現登入認證例項JWT
- ASP.NET Core - JWT認證實現ASP.NETJWT
- 使用JWT實現Spring Boot令牌認證JWTSpring Boot
- SpringCloud微服務實戰——搭建企業級開發框架(二十三):Gateway+OAuth2+JWT實現微服務統一認證授權SpringGCCloud微服務框架GatewayOAuthJWT
- WebService基於SoapHeader實現安全認證WebHeader
- 基於token的身份認證以及JWT(第一版)JWT
- drf的JWT認證JWT
- express實現JWT使用者認證系統ExpressJWT
- 實現一個Promise(基於Promise/A+規範)Promise
- Node.js的Koa實現JWT使用者認證Node.jsJWT
- JWT 多表認證JWT
- 基於使用者認證的前後端實現後端
- 微服務架構 | 7.2 構建使用 JWT 令牌儲存的 OAuth2 安全認證微服務架構JWTOAuth
- 基於Oauth2.0實現SSO單點認證OAuth
- 重新認識Java微服務架構-認證服務Java微服務架構
- 四個微服務授權認證的最佳實踐 - thenewstack微服務
- egg基於jsonwebtoken的Token實現認證機制JSONWeb
- 瞭解JWT認證JWT
- DRF JWT認證(一)JWT
- DRF JWT認證(二)JWT
- DRF之JWT認證JWT
- 基於OpenTelemetry實現Java微服務呼叫鏈跟蹤Java微服務
- golang 基於 jwt 實現的登入授權GolangJWT
- 實戰模擬│JWT 登入認證JWT
- ASP.NET Web API 2系列(四):基於JWT的token身份認證方案ASP.NETWebAPIJWT
- 基於Golang的微服務——Micro實踐(一)Golang微服務