本文详细介绍了JWT单点登录的原理和实现方法,帮助读者理解JWT的工作机制和组成部分。通过具体步骤和代码示例,展示了如何使用JWT实现单点登录,并讨论了JWT单点登录的优势和未来的安全发展趋势。JWT单点登录原理学习涵盖了从理论到实战的全过程。
JWT基础概念介绍JWT (JSON Web Token) 是一种开放标准 (RFC 7519),用于在网络间安全地传输信息。JWT由三部分组成的字符串,它们之间由点(.
)分隔。JWT由三部分组成:头部(Header)、负载(Payload)、签名(Signature)。它是一个紧凑的、自包含的标准,广泛用于身份验证和信息交换领域。
{ "alg": "HS256", "typ": "JWT" }
iss
(颁发者),iat
(发行时间),exp
(过期时间),nbf
(生效时间),aud
(受众),sub
(主题),jti
(JWT ID)。
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
单点登录 (Single Sign-On, SSO) 是一种身份验证模式。在该模式下,用户只需一次登录,就可以访问多个相关系统或服务,而无需重复进行身份验证。这可以提高用户体验,减少因多次输入凭据而产生的错误,同时简化管理员的维护工作。
在基于JWT的SSO实现中,JWT被用作访问令牌。当用户首次登录时,服务器会生成一个JWT并返回给客户端。客户端可以在后续的请求中使用这个JWT来访问其他受保护的资源和服务,从而实现单点登录。
为了实现JWT单点登录,我们需要准备以下环境:
安装相关依赖:
npm install express body-parser jsonwebtoken bcryptjs express-session
身份验证接口:处理用户登录请求,生成并返回JWT。
const express = require('express'); const bodyParser = require('body-parser'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); const session = require('express-session'); const app = express(); const secret = 'yourSecretKey'; // 用于签名的密钥 app.use(bodyParser.json()); app.use(session({ secret: 'secret', resave: false, saveUninitialized: false })); // 模拟用户数据 const users = { 'john': 'password123' }; // 登录接口 app.post('/login', (req, res) => { const { username, password } = req.body; if (users[username] && bcrypt.compareSync(password, users[username])) { const token = jwt.sign({ username: username }, secret, { expiresIn: '1h' }); res.json({ token: token }); } else { res.status = 401; res.json({ message: 'Invalid credentials' }); } }); // 保护接口 app.get('/protected', (req, res) => { if (req.isAuthenticated()) { res.json({ message: 'You can access protected resources!' }); } else { res.status = 403; res.json({ message: 'Unauthorized' }); } }); // 检查JWT的有效性 const authenticateJWT = (req, res, next) => { const token = req.headers.authorization; if (!token) { return res.status(401).json({ message: 'No token provided.' }); } jwt.verify(token, secret, (err, user) => { if (err) { return res.status(403).json({ message: 'Failed to authenticate token.' }); } req.user = user; next(); }); }; // 设置中间件 app.use(authenticateJWT, (req, res, next) => { next(); }); // 启动服务器 app.listen(3000, () => { console.log('Server running on port 3000'); });
客户端代码:模拟用户登录和访问受保护资源的请求。
<!DOCTYPE html> <html> <head> <title>JWT SSO Example</title> <script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="https://code.jquery.com/jquery-3.6.0.min.js"></script> </head> <body> <h1>Login</h1> <form id="loginForm"> <label for="username">Username:</label> <input type="text" id="username" name="username"> <label for="password">Password:</label> <input type="password" id="password" name="password"> <button type="submit">Login</button> </form> <div id="response"></div> <button id="getResource">Get Protected Resource</button> <div id="protectedResource"></div> <script> $(document).ready(function() { $('#loginForm').submit(function(event) { event.preventDefault(); const username = $('#username').val(); const password = $('#password').val(); $.ajax({ url: 'http://localhost:3000/login', method: 'POST', data: { username: username, password: password }, success: function(response) { localStorage.setItem('token', response.token); $('#response').text('Login successful'); }, error: function(response) { $('#response').text('Login failed'); } }); }); $('#getResource').click(function() { const token = localStorage.getItem('token'); $.ajax({ url: 'http://localhost:3000/protected', method: 'GET', headers: { 'Authorization': 'Bearer ' + token }, success: function(response) { $('#protectedResource').text(response.message); }, error: function(response) { $('#protectedResource').text('Resource access denied'); } }); }); }); </script> </body> </html>
JWT默认包含一个过期时间字段(exp
),用于表示JWT的有效期。当JWT过期时,客户端需要重新登录或请求新的JWT。
客户端处理:在接收到过期JWT的响应时,客户端可以提示用户重新登录或刷新JWT。
if (response.status === 401 || response.status === 403) { if (response.responseJSON.message === 'No token provided.' || response.responseJSON.message === 'Failed to authenticate token.') { if (new Date().getTime() >= exp) { alert("Your token has expired. Please log in again."); // 清除存储的token localStorage.removeItem('token'); } } }
服务器端处理:服务器端可以检测JWT的过期时间,并在JWT过期时拒绝访问。
const authenticateJWT = (req, res, next) => { const token = req.headers.authorization; if (!token) { return res.status(401).json({ message: 'No token provided.' }); } jwt.verify(token, secret, (err, user) => { if (err) { return res.status(403).json({ message: 'Failed to authenticate token.' }); } if (Date.now() > user.exp) { return res.status(403).json({ message: 'Token has expired.' }); } req.user = user; next(); }); };
重放攻击是一种攻击方式,攻击者截取并重新发送合法用户的JWT,以获得未经授权的访问。为了防范重放攻击,可以使用时间戳和nonce等机制。
时间戳:添加一个时间戳字段到JWT中,服务器端检查JWT的时间戳是否在有效范围内。
{ "timestamp": 1600000000, "exp": 1600000001 }
const authenticateJWT = (req, res, next) => { const token = req.headers.authorization; if (!token) { return res.status(401).json({ message: 'No token provided.' }); } jwt.verify(token, secret, (err, user) => { if (err) { return res.status(403).json({ message: 'Failed to authenticate token.' }); } const now = Math.floor(Date.now() / 1000); if (now > user.exp || now < user.timestamp) { return res.status(403).json({ message: 'Token has expired or is not valid.' }); } req.user = user; next(); }); };
nonce:为每个请求生成一个随机的nonce,并将其包含在JWT中,服务器端检查nonce是否匹配。
{ "nonce": "randomNonceValue" }
const authenticateJWT = (req, res, next) => { const token = req.headers.authorization; if (!token) { return res.status(401).json({ message: 'No token provided.' }); } jwt.verify(token, secret, (err, user) => { if (err) { return res.status(403).json({ message: 'Failed to authenticate token.' }); } if (req.body.nonce !== user.nonce) { return res.status(403).json({ message: 'Nonce does not match.' }); } req.user = user; next(); }); };
跨域问题通常发生在不同域之间发送HTTP请求时。为了处理跨域问题,可以在服务器端设置适当的CORS(跨域资源共享)头。
安装cors
中间件:
npm install cors
配置CORS设置:
const cors = require('cors'); app.use(cors({ origin: 'http://localhost:3000', // 允许的源 credentials: true // 允许发送cookies等凭据 }));
通过本教程的学习,读者应该能够掌握JWT的原理以及如何使用JWT实现单点登录。实践部分提供了详细的代码示例,帮助读者更好地理解和应用这些概念。