JWT 认证 + 角色权限:充电桩平台的安全体系搭建
在前三篇文章中,我们搭建了充电桩平台的基础后端服务、数据模型以及设备接入模块。随着平台功能逐渐完善,安全成为不可回避的核心问题------如何确保只有合法用户才能访问 API?如何区分管理员和普通充电用户?本文将使用 Express.js + JWT + bcryptjs 构建一套完整的认证授权体系,并配合前端 React + AuthContext 实现路由保护。完整的代码示例带你一步步落地。
一、JWT 认证原理:为什么选择它?
传统的 session 认证需要服务器存储会话信息,在分布式环境中需要共享存储。而 JWT(JSON Web Token) 是无状态的:服务器生成一个签名的 token 发给客户端,后续请求客户端在 Authorization 头中携带它,服务器验证签名即可确认用户身份。
一个 JWT 由三部分组成:Header(算法和类型)、Payload(用户信息、过期时间等)、Signature(使用密钥对前两部分签名)。核心优势是自包含------无需查数据库即可验证身份,非常适合 RESTful API。
二、密码加密存储:bcryptjs 的实战应用
用户的密码绝不能明文存储。bcryptjs 是一个慢哈希算法库,内置盐值生成和验证功能。
安装依赖:
bash
npm install bcryptjs jsonwebtoken express-validator
注册时加密密码:
javascript
// models/User.js (Mongoose 示例)
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
role: { type: String, enum: ['admin', 'operator', 'user'], default: 'user' }
});
// 保存前自动哈希密码
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// 验证密码的方法
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
三、用户注册与登录接口
注册接口:POST /api/auth/register
javascript
// routes/auth.js
const express = require('express');
const { body, validationResult } = require('express-validator');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const router = express.Router();
router.post('/register', [
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 6 }),
body('role').optional().isIn(['admin', 'operator', 'user'])
], async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
const { email, password, role } = req.body;
try {
// 检查用户是否已存在
const existingUser = await User.findOne({ email });
if (existingUser) return res.status(400).json({ message: '邮箱已被注册' });
const user = new User({ email, password, role });
await user.save();
// 生成 JWT
const token = jwt.sign(
{ userId: user._id, email: user.email, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.status(201).json({
token,
user: { id: user._id, email: user.email, role: user.role }
});
} catch (err) {
res.status(500).json({ message: '服务器错误' });
}
});
登录接口:POST /api/auth/login
javascript
router.post('/login', [
body('email').isEmail(),
body('password').notEmpty()
], async (req, res) => {
const { email, password } = req.body;
try {
const user = await User.findOne({ email });
if (!user) return res.status(401).json({ message: '邮箱或密码错误' });
const isMatch = await user.comparePassword(password);
if (!isMatch) return res.status(401).json({ message: '邮箱或密码错误' });
const token = jwt.sign(
{ userId: user._id, email: user.email, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.json({
token,
user: { id: user._id, email: user.email, role: user.role }
});
} catch (err) {
res.status(500).json({ message: '服务器错误' });
}
});
四、中间件实现路由保护
创建一个通用的 JWT 验证中间件,解析 token 后挂载 req.user:
javascript
// middleware/auth.js
const jwt = require('jsonwebtoken');
const verifyToken = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: '未提供认证令牌' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // 包含 userId, email, role
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ message: '令牌已过期' });
}
return res.status(403).json({ message: '无效的令牌' });
}
};
module.exports = { verifyToken };
使用示例:保护任何需要登录的路由
javascript
router.get('/profile', verifyToken, (req, res) => {
res.json({ user: req.user });
});
五、三种角色权限设计(admin/operator/user)
在充电桩平台中,角色权限定义如下:
- admin:系统级管理员,可创建/删除运营商,查看所有充电桩数据,分配角色。
- operator:场站运营商,管理自己名下的充电桩、查看订单、设置电价。
- user:普通充电用户,仅能查看附近充电桩、发起充电、查看自己的订单。
授权中间件
javascript
// middleware/auth.js
const requireRole = (allowedRoles) => {
return (req, res, next) => {
if (!req.user) return res.status(401).json({ message: '未认证' });
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({ message: '权限不足' });
}
next();
};
};
实际应用示例
javascript
// 只有 admin 可以查看所有用户
router.get('/users', verifyToken, requireRole(['admin']), async (req, res) => {
const users = await User.find().select('-password');
res.json(users);
});
// operator 和 admin 可以管理充电桩
router.post('/stations', verifyToken, requireRole(['admin', 'operator']), (req, res) => {
// 创建充电桩逻辑...
});
// user 可以查看自己的订单
router.get('/orders/my', verifyToken, requireRole(['user', 'operator', 'admin']), (req, res) => {
// 返回 req.user.userId 关联的订单
});
六、前端实现:AuthContext 与 ProtectedRoute
前端使用 React + React Router v6 + Axios。首先创建一个认证上下文来管理 token 和用户状态。
1. AuthContext 定义
javascript
// context/AuthContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';
import axios from 'axios';
const AuthContext = createContext();
export const useAuth = () => useContext(AuthContext);
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const token = localStorage.getItem('token');
const userInfo = localStorage.getItem('user');
if (token && userInfo) {
setUser(JSON.parse(userInfo));
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
}
setLoading(false);
}, []);
const login = async (email, password) => {
const response = await axios.post('/api/auth/login', { email, password });
const { token, user } = response.data;
localStorage.setItem('token', token);
localStorage.setItem('user', JSON.stringify(user));
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
setUser(user);
return user;
};
const logout = () => {
localStorage.removeItem('token');
localStorage.removeItem('user');
delete axios.defaults.headers.common['Authorization'];
setUser(null);
};
const value = { user, login, logout, loading };
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
2. ProtectedRoute 组件
javascript
// components/ProtectedRoute.jsx
import { Navigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
const ProtectedRoute = ({ children, allowedRoles = [] }) => {
const { user, loading } = useAuth();
if (loading) return <div>加载中...</div>;
if (!user) return <Navigate to="/login" replace />;
if (allowedRoles.length && !allowedRoles.includes(user.role)) {
return <Navigate to="/unauthorized" replace />;
}
return children;
};
export default ProtectedRoute;
3. 在路由中使用
javascript
// App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext';
import ProtectedRoute from './components/ProtectedRoute';
import LoginPage from './pages/LoginPage';
import AdminDashboard from './pages/AdminDashboard';
import OperatorPanel from './pages/OperatorPanel';
import UserProfile from './pages/UserProfile';
function App() {
return (
<BrowserRouter>
<AuthProvider>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/admin" element={
<ProtectedRoute allowedRoles={['admin']}>
<AdminDashboard />
</ProtectedRoute>
} />
<Route path="/operator" element={
<ProtectedRoute allowedRoles={['admin', 'operator']}>
<OperatorPanel />
</ProtectedRoute>
} />
<Route path="/profile" element={
<ProtectedRoute allowedRoles={['admin', 'operator', 'user']}>
<UserProfile />
</ProtectedRoute>
} />
</Routes>
</AuthProvider>
</BrowserRouter>
);
}
4. Axios 拦截器自动刷新 token(进阶)
在实际生产环境中,token 过期后需要静默刷新。可以添加响应拦截器:
javascript
// axiosConfig.js
import axios from 'axios';
let refreshPromise = null;
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// 调用刷新 token 接口(需要后端支持)
const refreshToken = localStorage.getItem('refreshToken');
const { data } = await axios.post('/api/auth/refresh', { refreshToken });
localStorage.setItem('token', data.token);
axios.defaults.headers.common['Authorization'] = `Bearer ${data.token}`;
originalRequest.headers['Authorization'] = `Bearer ${data.token}`;
return axios(originalRequest);
} catch (err) {
// 刷新失败,跳转登录
window.location.href = '/login';
return Promise.reject(err);
}
}
return Promise.reject(error);
}
);
七、总结
现在,你的充电桩平台已经拥有一套完整的认证和权限体系:
- 后端:基于 JWT 的无状态认证,bcryptjs 加密密码,中间件 + 角色守卫确保每个 API 的访问控制。
- 前端:AuthContext 统一管理登录状态,ProtectedRoute 实现客户端路由保护,支持三种角色的差异化界面。
安全是一个持续演进的话题,后续还可以加入:限流防暴力破解、HTTPS 强制、日志审计、API 请求签名(防止重放攻击)等。