溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

基于JWT規(guī)范實現(xiàn)的認證微服務(wù)

發(fā)布時間:2020-08-08 03:52:43 來源:ITPUB博客 閱讀:120 作者:EAWorld 欄目:軟件技術(shù)

基于JWT規(guī)范實現(xiàn)的認證微服務(wù)本文由公眾號EAWorld翻譯發(fā)表,轉(zhuǎn)載需注明出處。

作者:Marcelo Fonseca

譯者:白小白 

原題:Building an authentication micro-service with JWT standard

原文:http://t.cn/EI67VmL

全文2326字,閱讀約需要5分鐘

目錄:

一、微服務(wù)介紹

二、隨之而來的認證和授權(quán)問題

三、項目架構(gòu)通信

四、用于簽名以及驗證的公鑰和私鑰令牌

五、項目數(shù)據(jù)庫同步問題

一、微服務(wù)介紹

微服務(wù)日漸流行,幾乎所有流行語言都提供了兩種框架實現(xiàn),一是面向Web開發(fā)的大型框架,一是面向小型應(yīng)用的微框架。輕量級框架作為微服務(wù)架構(gòu)來說,是個好的選擇。微服務(wù)架構(gòu)有很多優(yōu)勢,諸如高可維護性,獨立部署等等。微服務(wù)架構(gòu)讓我們可以針對特定語言選擇最優(yōu)的解決方案來建立特定的服務(wù),比如,針對爬蟲類應(yīng)用或者AI場景,我們可以選擇建立一個Python服務(wù);針對加密庫的場景建立JS服務(wù);針對Active Record的場景建立Ruby服務(wù)等等?;谶@樣的理念,我們不需要受限于使用單一語言來建立整個后端服務(wù)。

下面我列出了各種語言提供的微框架列表:

  • Python - Flask

  • Javascript - ExpressJS

  • Ruby - Sinatra

  • Go - Martini

  • Java - Spark

  • C# - nancy

  • C++ - Crow

  • PHP - silex

二、隨之而來的認證和授權(quán)問題

在微服務(wù)架構(gòu)下,前后端的認證邏輯相比常規(guī)的CS應(yīng)用要復雜的多??蛻舳伺c后端的API服務(wù)器并不是一對一的關(guān)系,我們需要管理很多的后端服務(wù),需要對更多的應(yīng)用路由提供保護。為了解決這一問題,人們實踐了很多方式來建立微服務(wù)架構(gòu)下的認證和授權(quán)邏輯。本文展示了其中一種方案,基于JSON Web Tokens(JWT)標準來實現(xiàn)一個簡單的認證和授權(quán)服務(wù)。

三、項目架構(gòu)通信

簡化起見,示例中只實現(xiàn)了兩個后端服務(wù)。我將建立一個用于認證和授權(quán)的expressJS應(yīng)用,以及一個Sinatra應(yīng)用來作為博客服務(wù)的后端。目前為止,在本例 中將有兩個后端以及一個前端。

下面介紹一下應(yīng)用間通信的實現(xiàn)機制。

前后端通信機制

基于JWT規(guī)范實現(xiàn)的認證微服務(wù)

  1. ExpressJS實現(xiàn)了前端應(yīng)用的用戶注冊和登陸。

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

  3. 前端將這一令牌附加在請求的消息頭中用以訪問Sinatra應(yīng)用數(shù)據(jù)。

服務(wù)間通信機制

當我們需要實現(xiàn)后端之間的通信時,就需要利用這樣的機制。作為示例場景,假設(shè)還有一個Flask API后端用于爬取網(wǎng)絡(luò)上的內(nèi)容,并更新Sinatra博客應(yīng)用中的數(shù)據(jù)。這樣我們就一共有了三個后端和一個前端。

基于JWT規(guī)范實現(xiàn)的認證微服務(wù)

  1. Flask應(yīng)用向ExpressJS應(yīng)用請求JWT令牌。

  2. 請求成功后,ExpressJS應(yīng)用返回令牌。

  3. Flask應(yīng)用將令牌附加在請求的消息頭,并訪問Sinatra應(yīng)用的后端路由。

此處需要注意兩件事。無論是用戶發(fā)出請求或者后端發(fā)出請求,都需要合法的身份來進行認證以及訪問其他后端。但作為后端服務(wù)來講是不會使用郵件和密碼的,而是以API秘鑰作為身份的證明代之。比如,F(xiàn)lask應(yīng)用向ExpressJS應(yīng)用的路由發(fā)送一個登陸秘鑰,只要秘鑰是正確的,就可以授權(quán)Flask服務(wù)獲得JWT令牌。

四、用于簽名以及驗證的

公鑰和私鑰令牌

在這套架構(gòu)下,所有的微服務(wù)應(yīng)用將使用其自身的JWT庫來對訪問請求進行認證并保護其API路由。此處我們將使用JWT RSA256策略。認證服務(wù)ExpressJS將同時持有私鑰和公鑰。使用私鑰來對用戶或應(yīng)用的令牌進行簽名,用公鑰對令牌進行解碼和驗證。其他服務(wù)將僅持有公鑰來進行驗證。

使用RSA算法需要生成一個公鑰/私鑰對??梢酝ㄟ^如下的代碼在終端中實現(xiàn),作為執(zhí)行結(jié)果,代碼將生成.pem文件:

openssl genrsa -des3 -out private.pem 2048openssl rsa -in private.pem -outform PEM -pubout -out public.pem

(左右滑動查看全部代碼)

簽名令牌

在用戶或者API的登陸路由中實現(xiàn)令牌簽名。下面的代碼示例了ExpressJS認證服務(wù)的用戶登陸路由。只要用戶身份是合法的,代碼將訪問私鑰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. });

(左右滑動查看全部代碼)

驗證令牌

所有的服務(wù)都需要對持有合法JWT令牌的進站請求進行驗證。這可以通過在應(yīng)用中建立一個中間件來實現(xiàn)。這一中間件將訪問公鑰pem文件來對令牌進行解碼和驗證。在ExpressJS或者Sinatra服務(wù)中,這樣的中間件代碼類似如下所示。

ExpressJS認證和授權(quán)中間件代碼:

  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認證和授權(quán)中間件代碼:

  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

(左右滑動查看全部代碼)

五、項目數(shù)據(jù)庫同步問題

將博客服務(wù)和認證服務(wù)分離,將引發(fā)同步問題。原因之一是,兩者都需要各自保存用戶信息。ExpressJS需要用到用戶的身份信息,而Sinatra需要用到其他的用戶信息(比如頭像,個人描述以及發(fā)帖、評論數(shù)據(jù)之間的關(guān)聯(lián)關(guān)系等),對于這個問題可以有多種解決方案

  • 方案一:在認證服務(wù)的用戶表中保存全部用戶信息。在博客服務(wù)的用戶表中將僅保存用戶的ExpressJS服務(wù)ID(即user_id)以用來在認證服務(wù)中索引和查詢用戶數(shù)據(jù)。

  • 方案二:在博客服務(wù)中不設(shè)用戶表。所有涉及到用戶數(shù)據(jù)的博客數(shù)據(jù)庫表都將保存ExpressJS用戶ID作為索引。

  • 方案三:在認證服務(wù)中僅保存身份信息(如郵件地址和密碼),其余的信息保存在博客服務(wù)中。當需要在博客服務(wù)中引用認證服務(wù)的用戶數(shù)據(jù)時,以用戶ID或者郵件地址作為唯一索引來關(guān)聯(lián),當使用郵件地址時,需要在博客服務(wù)中同時保存用戶的郵件地址。

可以按自己的實際情況從上述的方案中做出選擇。我會選擇第三個方案,讓每個服務(wù)僅保存自己所需要的合理的數(shù)據(jù)。這樣,只需要少量的代碼修改,我就可以在未來的項目中復用這一認證服務(wù),以期在Sinatra應(yīng)用中充分利用Ruby的Active Record機制來進行用戶關(guān)系建模和查詢。要謹慎的時刻保持應(yīng)用間的用戶數(shù)據(jù)同步,比如,如果在ExpressJS應(yīng)用中刪除或者新建了一條用戶信息,確保這一變更同步到Sinatra應(yīng)用。


關(guān)于EAWorld微服務(wù),DevOps,數(shù)據(jù)治理,移動架構(gòu)原創(chuàng)技術(shù)分享,長按二維碼關(guān)注

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI