介绍
认证用户身份的过程涉及获得凭据并利用这些凭据来验证用户的身份。我们所有人在日常生活中都可以验证几项服务,例如登录PC,登录电子邮件等。它有助于我们获得对应用程序的个性化访问。身份验证可以帮助用户通过其数据维护隐私。
身份验证和授权之间的差异
对于这些术语如何传达不同的含义,一见钟情似乎令人困惑。人们通常会互换使用它,这是一个误解。虽然身份验证是验证用户是谁的过程。另一方面,授权是授予对特定资源的访问的过程,具体取决于用户必须访问它们的许可。
基于角色的访问控制(RBAC)ð©âð»
基于角色的访问控制是一种定义用户可以根据分配给他们的角色获得的权限的方法。角色将被分配做一些特定的操作或访问某些资源。在这里,当用户被授权使用某些功能或访问某些资源时,“授权”一词非常适合。
。了解身份验证的客户服务器模型ð»
在这种情况下,我们定义了两种类型的JWT令牌,它们可以在客户端和服务器之间传递以获得授权,即accessToken
和refreshToken
-
accessToken
:服务器仅在客户希望客户端访问受保护的路由时才发出访问令牌。 访问令牌是短暂的。注意:
- 服务器发出访问令牌以授权客户端,因此这些令牌不应存储在持续的浏览器存储中,例如
localStorage
或cookies
。将它们存储的推荐方法是将其存储在内存存储中,以便在应用程序关闭时丢失。 -
accessToken
被内存的原因是防止黑客访问这些令牌。由于内存存储无法访问JavaScript,这意味着即使黑客试图注入恶意JavaScript代码,也无法检索accessToken
。
- 服务器发出访问令牌以授权客户端,因此这些令牌不应存储在持续的浏览器存储中,例如
-
refreshToken
:用户对应用程序进行身份验证时会发出刷新令牌。 刷新令牌长期存在。注意:
-
refreshToken
以httpOnly
cookie的形式存储在浏览器中,JavaScript无法访问并且仅在用户身份验证之前才有效,换句话说,仅当用户登录 中时
-
refreshToken
可以生成新的accessToken
时,当kude12 rest api的端点被调用。 -
refreshToken
的引用存储在数据库中,因此,如果用户决定提早注销,则可以删除存储在数据库中的refreshToken
,并且可以终止会话 - 存储在数据库中的
refreshToken,
的引用与refreshToken
在client-request上存储在httpOnly
cookie中的refreshToken
进行了交叉验证,以恢复API以验证会话。 -
refreshToken
不应被允许生成新的刷新口,以防止无授权的人无限期地访问,如果他们设法以某种方式获得refreshToken
-
后端实现
代码实现将使用客户端的React和后端侧的ExpressJ进行。 mongodb用作数据库。
下面提到了后端所需的package.json
:
// package.json for backend
"dependencies": {
"bcrypt": "^5.1.0",
"body-parser": "^1.20.1",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.1",
"jsonwebtoken": "^8.5.1",
"mongoose": "^6.5.4",
"nodemon": "^2.0.19"
}
发行refreshToken
-
当用户登录应用程序
时const login = (req, res, next) => { const { email, password } = req.body; let errors = []; if (!email || !password) { errors.push({ msg: 'Please fill in all fields' }); res.status(400).json({ errors }); } User.findOne({ email }) .then(user => { if (!user) { errors.push({ msg: 'Email is not registered' }); res.status(400).json({ errors }); } else { bcrypt.compare(password, user.password, async (err, isMatch) => { if (err) throw err; if (isMatch) { // accessToken and refreshToken is being issued const accessToken = jwt.sign( { userInfo: { name: user.name, role: user.role }, } , process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' }); const refreshToken = jwt.sign({ name: user.name, role: user.role }, process.env.REFRESH_TOKEN_SECRET, { expiresIn: '1d' }); // adding a refreshToken in mongodb await User.findOneAndUpdate({ _id: user._id }, { refreshToken: refreshToken }) res.cookie('refreshToken', refreshToken, { maxAge: 1000 * 60 * 60 * 24, sameSite: 'none', httpOnly: true, secure: true }); res.status(200).json({ role: user.role, accessToken: accessToken }); } else { errors.push({ msg: 'Password is incorrect' }); res.status(400).json({ errors }); } }) } }) }
-
验证
refreshToken
何时accessToken
到期
const User = require('../Models/user') const jwt = require('jsonwebtoken') const handleRefreshToken = async (req, res) => { const cookies = req.cookies; // ensure cookie is set if (!cookies?.refreshToken) res.status(401).json({ msg: 'No cookies' }); const refreshToken = cookies.refreshToken; // cross verify refreshToken with the stored refreshToken in DB const person = await User.findOne({ refreshToken }) if (!person) res.status(401).json({ msg: 'No person' }); // evalutate jwt jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => { const role = Object.values(user.role) if (err) res.status(403).json({ msg: 'Invalid token' }); // accessToken is issued on successful verification const accessToken = jwt.sign({ userInfo: { name: user.name, role: user.role }, }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' }); res.status(200).json({ accessToken: accessToken, role: role }); }) }
在这里,我们正在使用AccessToken和Refreshtoken加密用户角色,以便在服务器端实现RBAC
当用户成功登录网站
时 refreshToken
作为httpOnly
cookie在响应标头中发出。 cookie设置在浏览器中,该浏览器将随后用于检索新的accessToken
。
accessToken
存储在浏览器的内存中,因此看不到。但是登录控制台。我们验证它已发行。 在生产环境中,切勿将其记录到控制台。
accessToken
作为请求授权标题中的携带者令牌传递,然后帮助客户访问受保护的路线。
我们创建一个中间件功能来验证accessToken
const verify = (req, res, next) => {
const authHeader = req.headers.authorization || req.headers.Authorization;
if(!authHeader?.startsWith('Bearer ')) return res.status(401).json({ msg: 'Unauthorized' });
const token = authHeader.split(' ')[1];
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, decoded)=> {
if(err) return res.status(403).json({ msg: 'Invalid token' });
console.log(decoded)
req.role = decoded.userInfo.role
next()
})
}
角色的验证是由另一个中间件功能完成的
const verifyRoles = (...allowedRoles) => {
return (req, res, next) => {
if (!req?.role) return res.status(401).json({ msg: 'You are not authenticated' });
const rolesArray = [...allowedRoles]
const result = req.role.map(role => rolesArray.includes(role)).find(val => val === true);
if (!result) return res.status(401).json({ msg: 'You are not authorized to access this route' });
next();
}
}
在前端实施受保护的路线ð
react-router-dom v6
允许我们根据分配给用户的角色保护路线。这些角色将根据AccessToken从后端获取。
实现如下:
App.jsx
import { Routes, Route } from "react-router-dom"
import Home from "./Pages/Home";
import Loginscreen from "./Pages/Loginscreen";
import Unauthorised from "./Components/Unauthorised";
import UserProvider from "./context/Usercontext";
import Admin from "./Pages/Admin";
import Layout from "./Layout";
import Registerscreen from "./Pages/Registerscreen";
import Missing from "./Missing";
import RequireAuth from "./RequireAuth";
import EditorScreen from "./Pages/EditorScreen";
import Persistlogin from "./Components/Persistlogin";
function App() {
return (
<UserProvider>
<Routes>
{/* Public Routes (without logged in) */}
<Route path="/login" element={<Loginscreen />} />
<Route path="/unauthorised" element={<Unauthorised />} />
<Route path="/register" element={<Registerscreen />} />
<Route element={<Persistlogin />}>
{/* Any User can access with user role */}
<Route element={<RequireAuth allowedRoles={[2000]} />}>
<Route path="/" element={<Home />} />
</Route>
{/* Protected Routes (Admin) */}
<Route element={<RequireAuth allowedRoles={[1000]} />}>
<Route path="/admin" element={<Admin />} />
</Route>
{/* Protected Routes(Editor) */}
<Route element={<RequireAuth allowedRoles={[3000]} />}>
<Route path="/editor" element={<EditorScreen />} />
</Route>
</Route>
{/* Catch all */}
<Route path="*" element={<Missing />} />
</Routes>
</UserProvider>
);
}
export default App;
创建了requireAuth.jsx
组件来管理受保护的路由访问
import { useLocation, Navigate, Outlet } from "react-router-dom"
import useAuth from "./hooks/useAuth"
const RequireAuth = ({ allowedRoles }) => {
// useAuth is a custom hook created to access the global user context store
const { auth } = useAuth()
const location = useLocation()
return (
<div>
{auth?.role?.find(role1 => allowedRoles.includes(role1))
? <Outlet />
: auth?.email ?
<Navigate to="/unauthorised" state={{ from: location }} replace />
: <Navigate to="/login" state={{ from: location }} replace />
}
</div>
)
}
<Outlet/>
组件是react-router-dom v6
的内置功能,有助于渲染包裹在<Route></Route>
组件周围的children
组件。
当用户试图刷新页面时,一个主要问题是一个主要问题,当窗口刷新窗口时,AUTH状态将重置。为了保持持久登录状态,我们定义另一个组件PersistLogin.jsx
import useRefreshToken from "../hooks/useRefreshToken"
import useAuth from "../hooks/useAuth"
import { useState, useEffect } from "react"
import { Outlet } from "react-router-dom"
const Persistlogin = () => {
const refresh = useRefreshToken()
const { auth } = useAuth()
const [loading, setLoading] = useState(true)
useEffect(() => {
const verifyRefreshToken = async () => {
try {
await refresh()
} catch (error) {
console.error(error)
}
finally {
setLoading(false)
}
}
!auth?.accessToken ? verifyRefreshToken() : setLoading(false)
}, [])
useEffect(() => {
console.log(`loading: ${loading}`)
console.log(`accessToken: ${JSON.stringify(auth?.accessToken)}`)
}, [loading])
return (
<>
{loading ? <p>Loading...</p> : <Outlet />}
</>
)
}
export default Persistlogin
访问受保护的路线
当管理员登录应用程序并尝试访问管理页面时,他将获得访问权限。
但是当管理员导航到编辑页面时,他会收到以下错误。
accessToken
到期后,/refresh
端点被打击以获取新的accessToken
展示了网络活动:
/getUser
路线受到保护,当通过过期的访问访问访问时,我们会得到403禁止响应。 refreshToken
随后被调用以获取新的访问权限。这是在Axios拦截器的帮助下实现的。
Axios拦截器
让我们演示有关新的accessTokens
在使用Axios interceptor
Axios是一个强大的基于承诺的HTTP库。它最酷的功能之一是Axios拦截器。
Axios拦截器与ExpressJS的中间件功能相似。
请求拦截器帮助我们定义是否要在向服务器发送请求之前进行任何操作。
axios.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
在我们的应用程序中,我们定义了请求拦截器将AccessToken通过作为授权标题中的承载令牌。
const requestIntercept = axiosPrivate.interceptors.request.use(
config => {
if (!config.headers['Authorization']) {
config.headers['Authorization'] = `Bearer ${auth?.accessToken}`
}
return config
},
error => Promise.reject(error)
)
类似地,
响应拦截器被定义为当我们收到服务器的响应后要进行任何操作时。
axios.interceptors.response.use(function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
return response;
}, function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
return Promise.reject(error);
});
在此应用程序中,我们已经实施了一个响应拦截器,以重新将accessToken
作为请求标头重新传输,一旦我们检测到403禁止状态,该状态将用于过期的accessToken
。
const reponseIntercept = axiosPrivate.interceptors.response.use(
// if successful return response
(response) => response,
async (error) => {
// if there is an error, which comes from expired accessToken
const prevRequest = error?.config;
if (error?.response?.status === 403 && !prevRequest?.sent) {
// issue new accessToken from refresh endpoint and send it to the request header
const newAccessToken = await refresh()
return axiosPrivate({
...prevRequest,
headers: { ...prevRequest.headers, Authorization: `Bearer ${newAccessToken}` },
sent: true
});
}
return Promise.reject(error);
}
)
登录用户
当用户尝试注销时,删除了浏览器存储的httpOnly
cookie存储的refreshToken
。 DB存储的refreshToken
也被同时清除。
const logout = (req, res) => {
const cookies = req.cookies;
if (!cookies?.refreshToken) res.status(204).json({ msg: 'No cookies' });
const refreshToken = cookies.refreshToken;
User.findOneAndUpdate({ refreshToken }, { refreshToken: '' }, (err, doc) => {
if (err) {
res.status(500).json({ msg: 'Something went wrong' })
}
// maxAge need not be set during clearCookie
res.clearCookie('refreshToken', { httpOnly: true, sameSite: 'none', secure: true });
res.status(200).json({ msg: 'Logged out' });
})
}
要记住的事情
交叉原始资源访问
我们的React应用程序在http://localhost:3000/
上运行
和我们在http://localhost:5000/
上运行的后端Express应用
很明显,我们正在尝试从后端访问其他来源,即我们的React App。
。这将给我们带来CORS错误,该错误可以通过阻止各种恶意的跨站点脚本活动来保持站点的安全。
在开发环境中,我们可能会遇到此错误,因此,为防止它,我们使用CORS中间件,指定始终可以通过此策略的来源。
CORS中间件可以由此命令
安装 npm i cors
或yarn add cors
const allowedOrigin = [
'http://localhost:3000',
'http://localhost:5000',
'http://localhost:3001',
'http://localhost:5001',
]
const corsOption = {
origin: (origin, cb)=> {
if(allowedOrigin.indexOf(origin) !== -1 || !origin){
cb(null, true)
}
else{
cb(new Error("Not allowed by CORS"))
}
},
optionsSuccessStatus: 200,
}
我们通过CORS中间件中的corsOption
如下
app.use(corsOption)
配置cookie允许CORS
后端:
发送和接收cookie时,请确保设置secure: true
,然后sameSite: 'none'
允许cookie共享cross-origin
res.cookie('refreshToken', refreshToken, { maxAge: 1000 * 60 * 60 * 24, sameSite: 'none', httpOnly: true, secure: true });
同样,
res.clearCookie('refreshToken', { httpOnly: true, sameSite: 'none', secure: true });
注意:要通过Postman或任何其他类似的应用程序测试后端功能,请确保设置
secure: false
,因为它不允许在应用程序中设置Cookie,最终导致错误。
前端,
在提出Axios请求时,请确保设置withCredentials: true
以允许浏览器在提出请求之前设置cookie。
await axios.get('/refresh', { withCredentials: true })
资源链接
提供了一些有用资源的链接,您可以将其引用,以获取此顶部的更多深入知识。 ð
-
React React登录播放列表由Dave Gray:
https://www.youtube.com/playlist?list=PL0Zuz27SZ 6PRCpm9clX0WiBEMB70FWwd
-
您需要了解有关在前端存储JWT的所有信息:https://dev.to/cotter/localstorage-vs-cookies-all-you-need-to-know-about-storing-jwt-tokens-securely-in-the-front-end-15id
-
休息安全备忘单:
https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html