基於JWT規範實現的認證微服務

EAWorld發表於2019-03-05

基於JWT規範實現的認證微服務本文由公眾號EAWorld翻譯發表,轉載需註明出處。

作者: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應用來作為部落格服務的後端。目前為止,在本例 中將有兩個後端以及一個前端。

下面介紹一下應用間通訊的實現機制。

前後端通訊機制

基於JWT規範實現的認證微服務

  1. ExpressJS實現了前端應用的使用者註冊和登陸。

  2. 如果認證成功,ExpressJS應用將返回一個JWT令牌。

  3. 前端將這一令牌附加在請求的訊息頭中用以訪問Sinatra應用資料。

服務間通訊機制

當我們需要實現後端之間的通訊時,就需要利用這樣的機制。作為示例場景,假設還有一個Flask API後端用於爬取網路上的內容,並更新Sinatra部落格應用中的資料。這樣我們就一共有了三個後端和一個前端。

基於JWT規範實現的認證微服務

  1. Flask應用向ExpressJS應用請求JWT令牌。

  2. 請求成功後,ExpressJS應用返回令牌。

  3. 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令牌。

  1. // User sign-in route with JWT RSA algorithm example

  2. var User = require('../models/user')

  3. var express = require('express');

  4. var router = express.Router();

  5. const mongoose = require('mongoose');

  6. const bcrypt = require('bcrypt');

  7. const jwt = require('jsonwebtoken');

  8. const fs = require('fs');


  9. router.route('/sign-in').post(function(req, res, next){

  10.  User.find({ email: req.body.email}).then(user => {

  11.    if (user.length < 1)

  12.      return res.status(400).json({message: 'Authentication failed.'});


  13.    bcrypt.compare(req.body.password, user[0].passwordHash, (err, success) => {

  14.      if(success){

  15.        let cert = fs.readFileSync('../rsa_2048_priv.pem');


  16.        const token = jwt.sign(

  17.          {

  18.            email: user[0].email,

  19.            //id: user[0]._id,

  20.          },

  21.          cert,

  22.          {

  23.            expiresIn: '1h',

  24.            algorithm: 'RS256',

  25.            issuer: user[0].role,

  26.          }

  27.        );

  28.        res.status(200).json({token: token, message: 'Successfully authenticated.'});


  29.      }else

  30.        return res.status(400).json({message: 'Authentication failed.'});


  31.    });

  32.  });

  33. });

(左右滑動檢視全部程式碼)

驗證令牌

所有的服務都需要對持有合法JWT令牌的進站請求進行驗證。這可以通過在應用中建立一箇中介軟體來實現。這一中介軟體將訪問公鑰pem檔案來對令牌進行解碼和驗證。在ExpressJS或者Sinatra服務中,這樣的中介軟體程式碼類似如下所示。

ExpressJS認證和授權中介軟體程式碼:

  1. // JWT authentication middleware example.

  2. // Uses RS256 strategy with .pem key pair files.


  3. const fs = require('fs');

  4. const jwt = require('jsonwebtoken');


  5. module.exports = (req, res, next) => {


  6.    let publicKey = fs.readFileSync('../rsa_2048_pub.pem');


  7.    try{

  8.        const token = req.headers.authorization.split(' ')[1]; //req.headers.token;

  9.        console.log(token);

  10.        var decoded = jwt.verify(token, publicKey)

  11.        console.log(decoded);



  12.        next();


  13.    }catch(err){

  14.      return res.status(401).json({error: err, message: 'Invalid token.'});

  15.    }

  16. };

(左右滑動檢視全部程式碼)

Sinatra認證和授權中介軟體程式碼:

  1. # To connect this middleware.rb file to your sinatra app

  2. # add 'use JWTAuthorization' as one of your first lines in

  3. # your Application class.

  4. # e.g.

  5. # require 'middlewares.rb'

  6. # class Application < Sinatra::Base

  7. #   use JWTAuthorization

  8. #   ...

  9. # end


  10. require 'sinatra/json'

  11. require 'jwt'


  12. class JWTAuthorization


  13.  def initialize app

  14.    @app = app

  15.  end


  16.  def call env


  17.    begin

  18.      # env.fetch gets http header


  19.      # bearer = env.fetch('HTTP_AUTHORIZATION', '').split(' ')[1]    # also work

  20.      bearer = env.fetch('HTTP_AUTHORIZATION').slice(7..-1)           # gets JWT token

  21.      key = OpenSSL::PKey::RSA.new File.read '../rsa_2048_pub.pem'    # read public key pem file

  22.      payload = JWT.decode bearer, key, true, { algorithm: 'RS256'}   # decode and verify token with pub key

  23.      claims = payload.first


  24.      # current_user is defined by env[:user].

  25.      # useful to define current_user if you are using pundit gem

  26.      if claims['iss'] == 'user'

  27.        env[:user] = User.find_by_email(claims['email'])

  28.      end


  29.      # access your claims here...


  30.      @app.call env

  31.    rescue JWT::DecodeError

  32.      [401, { 'Content-Type' => 'text/plain' }, ['A token must be passed.']]

  33.    rescue JWT::ExpiredSignature

  34.      [403, { 'Content-Type' => 'text/plain' }, ['The token has expired.']]

  35.    rescue JWT::InvalidIssuerError

  36.      [403, { 'Content-Type' => 'text/plain' }, ['The token does not have a valid issuer.']]

  37.    rescue JWT::InvalidIatError

  38.      [403, { 'Content-Type' => 'text/plain' }, ['The token does not have a valid "issued at" time.']]

  39.    # useful only if using pundit gem

  40.    rescue Pundit::NotAuthorizedError

  41.      [401, { 'Content-Type' => 'text/plain' }, ['Unauthorized access.']]

  42.    end

  43.  end


  44. 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/,如需轉載,請註明出處,否則將追究法律責任。

相關文章