正確的客戶端身份驗證方式(Cookie 與本地存儲)
期望
當您登錄應用程序時,您期望下次在瀏覽器中打開新選項卡或窗口時,您仍會登錄到該應用程序。這意味著以某種方式、形狀或形式,客戶端(瀏覽器)必須保持對您的引用 為了讓您保持登錄狀態。
我在哪裡可以在客戶端持久化狀態?
在前端應用程序中處理安全性和身份驗證可能是一個難題。在 Web 應用程序中,通常有兩種方法可以在客戶端上維護狀態:
- 本地存儲
- Cookie
漏洞是什麼?
這兩種方法都存在潛在的相關安全問題:
方法 | 漏洞 |
---|---|
本地存儲 | XSS - 跨站腳本 |
Cookies | CSRF - 跨站請求偽造 |
- 一個XSS 該漏洞使攻擊者能夠將 JavaScript 注入網站。
- A CSRF 該漏洞使攻擊者能夠通過經過身份驗證的用戶在網站上執行操作。
關於這兩個漏洞之間的一些差異及其原因的很好的入門可以在哪裡存儲您的 JWT - Cookie 與 HTML5 Web 存儲中找到。
我怎樣才能繞過它?
如果本地存儲可以被第三方腳本(例如瀏覽器擴展中的腳本)利用,並且如果身份驗證可以通過 cookie 進行欺騙,那麼在哪裡可以接受客戶端狀態?
在 Auth0 文檔中使用 Cookie 進行單頁應用程序身份驗證中,我們了解到,如果您的應用程序:
- 使用您自己的後端向客戶提供服務
- 與您的後端具有相同的域
- 進行需要對您的後端進行身份驗證的 API 調用
那麼有一種方法可以安全地使用 cookie 進行身份驗證 .
它看起來像什麼?
設置的真實示例:
- a 反應 前端的單頁應用程序 (SPA)
- a 節點 + 表達 服務器後端
- 網絡 Cookies (安全、HttpOnly、同一站點)
Express 服務器將從所有路由中為 React SPA 提供服務,除了那些以 /api
開頭的路由 . React 應用程序將訪問所有端點的 Express 服務器。使用這種方法,您的前端應用程序位於同一個域中,並且有一個服務器,允許您使用 HttpOnly、Secure 和 Same Site 選項來保護 cookie。
從這裡,您可以對微服務或某些受保護的服務器進行 API 調用。瀏覽器將看不到實際的 API 端點和訪問令牌。
下面我將列出為全棧應用程序設置此架構的一些主要概念(而不是實際的教程演練)。
在 Express 中使用 HTTP cookie
為了在 Express 中使用 cookie,您使用 cookie-parser
模塊。
解析cookies
const cookieParser = require('cookie-parser')
app.use(cookieParser())
設置一個cookie
在路由中,您可以在 response
上設置 cookie 對象,具有一些重要的屬性:
// Set a cookie
response.cookie('nameOfCookie', 'cookieValue', {
maxAge: 60 * 60 * 1000, // 1 hour
httpOnly: true,
secure: true,
sameSite: true,
})
- 同一站點 - 防止 cookie 在跨站請求中發送
- 僅 HTTP - cookie 只能從服務器訪問
- 安全 - cookie 必須通過 HTTPS 傳輸
獲取 cookie
現在可以在後續響應中讀取 cookie。
// Get a cookie
response.cookies.nameOfCookie
清除 cookie
註銷身份驗證時,您需要清除 cookie。
// Clear a cookie
response.clearCookie('nameOfCookie')
Express 中間件中的局部值
Express 在中間件上運行。如果您想在一個中間件中更新 cookie 並在下一個中間件中使用它,您可以將其存儲為 Express 本地。如果您必須在 preAuth 路由中刷新 JWT 訪問令牌,在處理程序中使用該身份驗證,並在最後在響應中發送 cookie,這可能會派上用場。
// Create a local
const refreshMiddleware = (request, response, next) => {
const accessToken = getNewAccessToken(refreshToken)
// Set local
response.locals.accessToken = accessToken
next()
}
// Use a local
const handler = (request, response) => {
const updatedAccessToken = response.locals.accessToken
}
router.post('/app/user', refreshMiddleware, handler)
服務前端 React 應用程序
這個設置的一個很好的例子可以在 Simple React Full Stack 樣板設置中找到。最終,您的應用程序的佈局將如下所示:
- dist # Distribution folder of the production React SPA build
- src
- client # React source files
- server # Express server files
在這種情況下,您的服務器文件將如下所示:
src/server/index.js// Initialize Express app
const express = require('express')
const app = express()
const router = require('./router')
// Serve all static files from the dist folder
app.use(express.static(path.join(__dirname, '../../dist/')))
// Set up express router to serve all api routes (more on this below)
app.use('/api', router)
// Serve any other file as the distribution index.html
app.get('*', (request, response) => {
response.sendFile(path.join(__dirname, '../../dist/index.html'))
})
快速路由和處理程序
使用 Express Router 類,您可以將所有 API 路由組織到子目錄中,並在主服務器入口點中通過一行將它們引入。
- src
- server
- router
- handlers
- index.js
路由都可以組織到單獨的子目錄中。
src/server/routes/index.jsconst router = require('express').Router()
const bookRoutes = require('./books')
const authorRoutes = require('./authors')
router.use('/books', bookRoutes)
router.use('/authors', authorRoutes)
module.exports = router
在一組路由中,我們可以定義所有 GET
, POST
, DELETE
路由等,因為路由器使用 /api
,作者路線使用 /authors
, 對 /api/authors/jk-rowling
的 GET API 調用 會調用 getAuthor
處理程序,在本例中。
const router = require('express').Router()
const authorHandlers = require('../handlers/authors')
// Get
router.get('/', authorHandlers.getAllAuthors)
router.get('/:author', authorHandlers.getAuthor)
// Post
router.post('/', authorHandlers.addAuthor)
module.exports = router
您可以將所有相關的作者處理程序放在 handlers
子目錄。
module.exports = {
getAllAuthors: async (request, response) => {
// Some logic...
if (success) {
response.status(200).send(authors)
} else {
response.status(400).send({ message: 'Something went wrong' })
}
},
addAuthor: async (request, response) => { ... },
}
這讓我們回到了服務器入口點,它引入了 /api
的所有路由 .
// Set up all API routes
const router = require('./router')
// Use all API routes
app.use('/api', router)
React 單頁應用
Tyler McGinnis 有一篇關於 React Router 的 Protected Routes and Authentication 的精彩文章,其中演示瞭如何製作 PrivateRoute
和 PublicRoute
組件。
這是僅前端的身份驗證保護,不能信任它來保護敏感數據 - 應該由需要訪問令牌(或任何安全方法)返迴響應的後端 API 保護。
使用上述文章中路由的基本示例,您可以通過以下方式從 React 對 Express 服務器進行 API 調用,驗證一些全局 Context 狀態,並通過前端路由應用程序。
App.jsimport React, {Component} from 'react'
import {BrowserRouter as Router, Switch, Route, Redirect} from 'react-router-dom'
import axios from 'axios'
// ...plus page and context imports
export default class App extends Component {
static contextType = AuthContext
state = {loading: true}
async componentDidMount() {
const Auth = this.context
try {
const response = await axios('/api/auth')
Auth.authenticate()
} catch (error) {
console.log(error)
} finally {
this.setState({loading: false})
}
}
render() {
const Auth = this.context
const {loading} = this.state
if (loading) {
return <div>Loading...</div>
}
return (
<Router>
<Switch>
<PublicRoute exact path="/login" component={LoginPage} />
<ProtectedRoute exact path="/dashboard" component={DashboardPage} />
<Route exact path="/logout" component={LogoutPage} />
<Redirect to="/login" />
</Switch>
</Router>
)
}
}
現在,開發服務器將根據您的身份驗證狀態將您引導到正確的路由。在生產模式下,分配 index.html
文件將被提供 - 更多內容如下。
生產和開發
通過生產設置,構建了一個完整的 React 應用程序以進行分發,並且 Express 應用程序在所有路由上為 SPA 提供服務。
包.json// Production
{
"build": "cross-env NODE_ENV=production webpack --config config/webpack.prod.js",
"start": "npm run build && node src/server/index.js"
}
這對開發來說很麻煩。處理開發的最佳方式是像往常一樣在 Webpack 開發服務器上提供 React,並將所有 API 請求代理到 Express 服務器。
包.json// Development
{
"client": "cross-env NODE_ENV=development webpack-dev-server --config config/webpack.dev.js",
"server": "nodemon src/server/index.js",
"dev": "concurrently \"npm run server\" \"npm run client\""
}
您可能會在端口 3000
上提供 React 應用程序 和 5000
上的服務器 ,可以在開發Webpack的配置文件中設置。
devServer: {
historyApiFallback: true,
proxy: {
'/api': 'http://localhost:5000',
},
open: true,
compress: true,
hot: true,
port: 3000,
}
設置 historyApiFallback
將確保 SPA 路由正常工作。設置 publicPath
也很重要 在 Webpack 中到 /
, 以確保生產中的路由從根為捆綁包提供服務。
Webpack Boilerplate 是一個很好的示例,用於設置 Webpack(在這種情況下,您只需將所有內容從構建直接移動到 src
構建到 src/client
)。
結論
希望此資源可以幫助您了解與持久客戶端存儲(XSS 和 CSRF)相關的各種類型的漏洞,以及我們可以採取的一些方法來緩解潛在攻擊,即 HttpOnly、SameSite、安全 Web Cookie。
如果您有任何其他見解可以使本文變得更好,請隨時告訴我。