您好,登錄后才能下訂單哦!
作者:Marcelo Fonseca
譯者:白小白
原題:Building an authentication micro-service with JWT standard
原文:http://t.cn/EI67VmL
全文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,數據治理,移動架構原創技術分享,長按二維碼關注
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。