jwt與session的登陸鑑權

image

前言

本文是我對本身學習的一個總結,我只是一名菜鳥,若是您有更好的方案或者意見請務必指正出來。個人🐧:1025873823javascript

session鑑權

什麼是session?

session是一種服務器機制,是存儲在服務器上的信息。存儲方式多種多樣,能夠是服務器的內存中,或者是mongo數據庫,redis內存數據庫中。而session是基於cookie實現的(服務器會生成sessionID)經過set-cookie的方式寫入到客戶端的cookie中。每一次的請求都會攜帶服務器寫入的sessionID發送給服務端,經過解析sessionID與服務器端保存的session,來判斷用戶是否登陸。前端

鑑權步驟以下:java

  1. 客戶端發起登陸請求,服務器端建立session,並經過set-cookie將生成的sessionID寫入的客戶端的cookie中。
  2. 在發起其餘須要權限的接口的時候,客戶端的請求體的Header部分會攜帶sessionID發送給服務端。而後根據這個sessionId去找服務器端保存的該客戶端的session,而後判斷該請求是否合法。

session鑑權的示例

基於Passport和Express的實現,Passport的詳細文檔請參考,我這篇文章只是使用的介紹,更詳細的方法是閱讀文檔。node

跨域的解決

因爲是前端分離的項目,前端的靜態資源服務和後端的接口可能不在同一個域名下,這就致使了服務器沒法在瀏覽器上寫入cookie。須要經過設置CORS解決。先後端都須要額外的設置,代碼以下
// 基於axios代碼設置以下, withCredentials: true是否容許跨域修改cookie

const Axios = axios.create({
  baseURL: 'http://127.0.0.1:3000/',
  timeout: 1000,
  withCredentials: true,
  responseType: "json",
  headers: {
    "Content-Type": "application/x-www-form-urlencoded;charset=utf-8"
  }
})
// 使用CORS模塊,並配置容許跨域請求
app.use(cors({
  origin: 'http://127.0.0.1:8080',
  credentials: true,
  methods: ['PUT', 'POST', 'GET', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Content-Length', 'Authorization', 'Accept', 'X-Requested-With']
}))

⚠️:這裏有一個坑,origin不能設置爲通配符*,stackoverflow上的解答ios

Passport local本地驗證

環境配置
const session = require('express-session')
const MongoStore = require('connect-mongo')(session)
const bodyParser = require('body-parser')
const passport = require('passport')

app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
// 這裏我將session存儲到mongo中,更好的作法是存儲到redis中
app.use(session({
  resave: false,
  saveUninitialized: false,
  secret: sessionConfig.secret,
  store: new MongoStore({
    mongooseConnection: connection
  }),
  cookie: {
    maxAge: 60 * 1000 * 30
  }
}))
app.use(passport.initialize())
app.use(passport.session())
配置策略

local驗證默認使用密碼和用戶名驗證,首先須要對local策略作出配置。如下是官方示例給出的代碼,我直接copy過來使用。代碼很是簡單。User是Mongoose的Model(須要本身建立),經過findOne方法查找用戶名對應的用戶,並對查找的結果做出判斷,並經過調用passport的done方法做出驗證回調。因爲User密碼不是明文存儲的,經過了bcrypt模塊進行了加密。因此須要經過bcrypt.compare方法進行密碼校驗操做。web

done方法是由passport提供的,用於回調操做的方法。對於不一樣的結果執行不一樣的回調操做。redis

const passport = require('passport')
const LocalStrategy = require('passport-local').Strategy
const User = require('../models/user')
const bcrypt = require('../util/bcrypt')

passport.use(new LocalStrategy((username, password, done) => {
  User.findOne({ username }, (err, user) => {
    if (err) return done(err)
    if (!user) {
      return done(null, false, { message: '用戶名不存在' })
    }
    if (!bcrypt.compare(password, user.password)) {
      return done(null, false, { message: '用戶名或密碼錯誤' })
    }
    return done(null, user)
  })
}))
session序列化與反序列化

serializeUser序列化,將用戶信息存儲到session中,這段信息便是sessionID,同時會將sessionID存儲到客戶端的cookie中的過程。數據庫

deserializeUser反序列化,參數是用戶提交的sessionID,若是存在則從數據庫中查詢user並存儲與req.user中。express

passport.serializeUser((user, done) => {
  done(null, user.id)
})

passport.deserializeUser((id, done) => {
  User.findById(id, (err, user) => {
    done(null, user)
  })
})

😖😢對於這段代碼具體實現的細節,我一開始也明白。有一天我在stackoverflow上找到解答Understanding passport serialize deserializenpm

Q: serializeUser 作了什麼?

A: 經過done將user存儲到了session中, 並將sessionID寫入到客戶端的cookie上, 將用戶信息附加到請求對象req.session.passport.user上。

Q:deserializeUser 作了什麼?

A:deserializeUser的第一個參數就是你存儲的sessionID,經過Model的findById方法查找數據庫,並將用戶信息附加到請求對象req.user上

passport.serializeUser(function(user, done) {
    done(null, user.id);
                 |
});              | 
                 |
                 |____________________> saved to session req.session.passport.user = {id:'..'}
                                   |
                                  \|/           
passport.deserializeUser(function(id, done) {
                   ________________|
                   |
                  \|/ 
    User.findById(id, function(err, user) {
        done(err, user);
                   |______________>user object attaches to the request as req.user

});
logIn, logOut, isAuthenticated
passport爲request對象擴展的方法
  • logIn(), 用戶登錄操做,即初始化session
  • logOut(), 用戶登出操做,刪除用戶的session信息
  • isAuthenticated(), 用來判斷用戶是否登錄
接口示例
// 用戶登陸
router.get('/login', (req, res, next) => {
  // 登陸認證,使用local策略
  passport.authenticate('local', (err, user, info) => {
    if (err) return next(err)
    if (!user) return res.status(400).json({
      message: info.message
    })
    // 初始化session信息
    req.logIn(user, (err) => {
      if (err) return next(err)
      res.status(200).json({ code: 200, message: '登錄成功' })
    })
  })(req, res, next)
})

// 用戶登出
router.get('/logout', isAuthenticated, (req, res) => {
  // 刪除mongo中的session信息
  req.logout()
  res.status(200).json({ code: 200, message: '登出成功' })
})

// 用戶詳情(須要權限的接口)
// isAuthenticated是經過passport提供的isAuthenticated()封裝的簡單中間件
// 添加isAuthenticated接口則是須要登錄權限的接口
router.get('/details', isAuthenticated, (req, res) => {
  const { _id } = req.user
  UserService.getUserDetail(_id).then(data => {
    res.status(200).json({ code: 200, message: 'success', data })
  }).catch(err => {
    res.status(400).json({ message: '用戶信息不存在' })
  })
})
module.exports = function isAuthenticated (req, res, next) {
  if (req.isAuthenticated()) return next()
  res.status(403).json({ message: '沒有權限' })
}

jwt鑑權

什麼是jwt?

Json web token (JWT), 是爲了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準((RFC 7519).該token被設計爲緊湊且安全的,特別適用於分佈式站點的單點登陸(SSO)場景。JWT的聲明通常被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也能夠增長一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。

jwt鑑權的流程

鑑權的流程:

  1. 瀏覽器發起登陸請求
  2. 請求經過後,服務器會向瀏覽器返回token
  3. 瀏覽器接收到token後須要講token保存到本地(好比localStorage)
  4. 瀏覽器在下一次請求的時候會攜帶token信息
  5. 服務器收到請求,去驗證token驗證成功後會返回信息

乍一看,token是相似sessionID的存在。其實token和sessionID仍是有必定的不一樣的。sessionID是基於cookie實現的,而token不須要基於cookie。這就致使了sessionID只能用在瀏覽器上,對於原生的應用沒法實現。原生的應用是不具有cookie的特性的。另外sessionID能夠實現服務端註銷會話,而token不能(固然你能夠把用戶登錄的token存入到redis中,可是不推薦token入庫)

jwt鑑權的示例

示例代碼我是基本照抄這一篇教程,英文好的同窗推薦閱讀原版 Authenticate a Node.js API with JSON Web Tokens

CORS設置

因爲咱們須要經過請求體的headers傳遞token,因此咱們須要對CORS模塊進行額外的配置,代碼以下

// 咱們將會經過headers的x-access-token字段向服務端傳遞token
app.use(cors({
  origin: 'http://127.0.0.1:8080',
  credentials: true,
  methods: ['PUT', 'POST', 'GET', 'DELETE', 'OPTIONS'],
  allowedHeaders: [
    'Content-Type',
    'Content-Length',
    'Authorization',
    'Accept',
    'X-Requested-With',
    'x-access-token']
}))

環境配置

咱們須要下載如下的依賴包, 以及用於加密token的字符串secret(能夠是一個隨機字符串)

npm install --save jsonwebtoken

獲取token

咱們一般經過登錄操做獲取token,服務端生成token後,會交由瀏覽器端管理token

const jwt = require('jsonwebtoken')
const User = require('../models/user')
const bcrypt = require('../util/bcrypt')
const secret = require('../config/index').secret

// ...

login (name, password) {
  return new Promise((resolve, reject) => {
    User.findOne({ name: name }, (err, user) => {
      if (err) return reject(err)
      if (!user) return reject('用戶名不存在')
      // bcrypt用於加密的包
      if (!bcrypt.compare(password, user.password)) return reject('用戶名或密碼錯誤')
      // 根據id信息以及secret生成,對應的token,並設置token的過時時間
      const token = jwt.sign({ id: user._id }, secret, {
        expiresIn: 60 * 60 // token過時時間
      })
      // 返回token
      resolve(token)
    })
  })
}

受保護的接口

有一些API接口,將會受到token的保護,若是請求沒有包含token信息,請求將會失敗。咱們這裏將會封裝一箇中間件,幫助咱們用來判斷請求是否包含token信息,以及token信息是否過時,代碼以下

const jwt = require('jsonwebtoken')
const secret = require('../config/index').secret

module.exports = function (req, res, next) {
  // 獲取請求的token信息
  const token = req.body.token || req.query.token || req.headers['x-access-token']
  if (token) {
    // 檢驗token信息是否過時
    jwt.verify(token, secret, function(err, decoded) {      
      if (err) {
        return res.status(403).json({ code: 'error', error: 'token失效' })    
      } else {
        req.decoded = decoded    
        next()
      }
    })
  } else {
    res.status(403).json({code: 'error', error: '沒有權限'})
  }
}

接下來咱們將封裝的中間件,應用到咱們的接口中。在這裏,獲取所有用戶信息的接口將會收到token的保護,若是不包含token,將會返回403錯誤

const express = require('express')
const router = express.Router()
const UserService = require('../service/user.service')
const AuthenticationToken = require('../middleware/AuthenticationToken')

// AuthenticationToken中間件保護/users接口
router.get('/users', AuthenticationToken, (req, res) => {
  UserService.users().then(result => {
    res.status(200).json({code: 'ok', data: result})
  }).catch(error => {
    res.status(500).json({code: 'error', error})
  })
})
相關文章
相關標籤/搜索