本文介绍了JWT的构成和用途,解释了单点登录的基本概念,并详细阐述了JWT单点登录的优势和实现步骤。文章还提供了跨域认证、跨应用认证和跨平台认证的实际应用场景,以及相关的常见问题和解决方案。
什么是JWT单点登录JWT简介
JSON Web Token(JWT)是一种开放标准(RFC 7519),用于在双方之间安全地传输信息。JWT可用来构建安全的无状态认证机制,它通常用于身份验证和授权。JWT的核心特性包括:无状态性、加密签名和自包含。无状态性意味着服务端不需要存储和追踪会话状态,加密签名确保了数据的完整性和不可篡改性,自包含则意味着所有必要的信息都包含在单个令牌中。
JWT由三部分组成,分别用.
分割:
- 头部(Header):包含令牌的类型(即JWT)和所使用的签名算法(如HMAC SHA256或RSA)。
- 载荷(Payload):包含声明(claims),是JWT的主体部分,通常用于传输用户身份信息、权限信息等。
- 签名(Signature):通过联合头部、载荷和一个密钥(只有服务端知道),用HMAC算法、SHA256算法或其他算法生成一个签名,用于验证消息的完整性。
单点登录的基本概念
单点登录(Single Sign-On,简称 SSO)是指用户只需一次登录,即可访问多个系统或应用。这种机制提高了用户的便利性,减少了输入密码的次数,同时也简化了身份验证过程。传统的单点登录通常依赖于服务端会话机制,而JWT单点登录则采用无状态的JWT令牌来实现。
JWT单点登录的意义
JWT单点登录的意义在于:
- 无状态认证:省去了服务端存储会话状态的需求,简化了系统架构。
- 高可扩展性:由于JWT是无状态的,服务端不需要维护用户的登录状态,因此可以轻松地扩展到多个应用或服务。
- 安全性:JWT通过加密签名确保了数据的安全性和完整性,同时令牌在客户端本地存储,减少了网络传输中的安全风险。
- 跨域访问:支持跨域认证,适合微服务架构和分布式系统。
搭建JWT环境
在开始使用JWT之前,首先需要搭建JWT环境。主要工具包括:
- Node.js(后端)
- Express(框架)
- jsonwebtoken(JWT处理库)
安装所需依赖:
npm install express jsonwebtoken
生成JWT令牌的过程
生成JWT令牌需要以下几个步骤:
- 创建JWT头部,指定类型和算法。
- 构建JWT载荷,包含必要的声明。
- 使用密钥和头部中的算法生成签名。
- 合并头部、载荷和签名,形成完整的JWT令牌。
示例代码:
const jwt = require('jsonwebtoken');
const secret = 'your256bitsecret'; // 秘钥
function generateToken(user) {
const payload = {
id: user.id,
username: user.username,
email: user.email
};
const token = jwt.sign(payload, secret, {
expiresIn: '1h'
});
return token;
}
验证JWT令牌的流程
验证JWT令牌需要以下几个步骤:
- 从请求中获取JWT令牌。
- 使用相同的秘钥和算法解码JWT。
- 检查令牌的有效性和签名。
- 验证令牌的过期时间。
示例代码:
function verifyToken(req, res, next) {
const token = req.headers.authorization && req.headers.authorization.split(' ')[1];
if (token) {
jwt.verify(token, secret, (err, decoded) => {
if (err) {
return res.status(403).send({ message: '无效的Token' });
} else {
req.userId = decoded.id;
next();
}
});
} else {
return res.status(403).send({ message: '未提供Token' });
}
}
JWT单点登录的实现步骤
前端与后端的交互过程
前端向后端发起认证请求,通过验证后,后端生成JWT令牌并返回给前端,前端则保存这个令牌,并在后续请求中携带这个令牌进行身份验证。
前端代码示例(使用fetch
):
fetch('/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'user1',
password: 'pass1'
})
})
.then(response => response.json())
.then(data => {
console.log('Token:', data.token);
localStorage.setItem('token', data.token);
});
后端实现登录接口:
app.post('/auth/login', (req, res) => {
const user = {
id: 1,
username: 'user1',
email: '[email protected]'
};
const token = generateToken(user);
res.send({ token });
});
实现登录接口
登录接口负责验证用户身份,并生成JWT令牌。通常包括以下几个步骤:
- 验证用户身份(例如,通过用户名和密码验证)。
- 创建JWT令牌。
- 将JWT令牌返回给客户端。
示例代码:
app.post('/auth/login', (req, res) => {
const user = {
id: 1,
username: 'user1',
email: '[email protected]'
};
const token = generateToken(user);
res.json({ token });
});
实现认证接口
认证接口用于验证JWT令牌的有效性,并可能附加一些额外的验证步骤(例如验证用户角色、权限等)。
示例代码:
app.get('/profile', verifyToken, (req, res) => {
const user = {
id: req.userId,
username: 'user1',
email: '[email protected]'
};
res.json(user);
});
JWT单点登录的应用场景
跨域认证
JWT非常适合在微服务架构下进行跨域认证,因为它不需要依赖会话状态,只要JWT签名有效且未过期,就可以通过身份验证。
跨应用认证
JWT可以用于多个应用之间共享同一个登录状态。例如,用户在一个网站上登录后,其他关联的网站或应用也能识别该用户的身份。
跨平台认证
JWT可以用于Web应用、移动应用和桌面应用之间的认证。由于JWT的数据格式是JSON,并且包含所有必要的身份信息,因此可以很容易地在不同平台之间传输。
JWT单点登录的常见问题及解决方案令牌过期与刷新
JWT令牌通常有一个有限的有效期。过期后,客户端需要重新认证或刷新令牌。刷新机制通常通过客户端发送旧令牌来获取一个新的有效令牌。
示例代码(刷新令牌):
app.post('/auth/refresh', (req, res) => {
const oldToken = req.headers.authorization && req.headers.authorization.split(' ')[1];
jwt.verify(oldToken, secret, (err, decoded) => {
if (err) {
return res.status(403).send({ message: '无效的旧Token' });
}
const user = {
id: decoded.id,
username: decoded.username,
email: decoded.email
};
const newToken = generateToken(user);
res.json({ token: newToken });
});
});
令牌安全存储
为了防止令牌被截获或窃取,客户端必须安全地存储JWT令牌。常见的做法是在浏览器的localStorage
中存储令牌,或者使用HttpOnly
的cookie
存储。
令牌被恶意攻击的防范
- 使用推荐的加密算法:如RSA、HMAC SHA256。
- 定期更新密钥:定期更换JWT的密钥,以增加攻击者的破解难度。
- 防止CSRF攻击:可以在JWT载荷中添加额外的声明来防止CSRF攻击。
- 令牌长度和复杂度:确保令牌长度足够长且包含足够的随机性,以增加破解难度。
案例一:基于Node.js的JWT单点登录实现
本案例将展示如何使用Node.js和Express构建一个简单的JWT单点登录系统。
后端代码
安装依赖:
npm install express jsonwebtoken
创建server.js
:
const express = require('express');
const jwt = require('jsonwebtoken');
const secret = 'your256bitsecret'; // 秘钥
const app = express();
function generateToken(user) {
const payload = {
id: user.id,
username: user.username,
email: user.email
};
const token = jwt.sign(payload, secret, {
expiresIn: '1h'
});
return token;
}
function verifyToken(req, res, next) {
const token = req.headers.authorization && req.headers.authorization.split(' ')[1];
if (token) {
jwt.verify(token, secret, (err, decoded) => {
if (err) {
return res.status(403).send({ message: '无效的Token' });
} else {
req.userId = decoded.id;
next();
}
});
} else {
return res.status(403).send({ message: '未提供Token' });
}
}
app.post('/auth/login', (req, res) => {
const user = {
id: 1,
username: 'user1',
email: '[email protected]'
};
const token = generateToken(user);
res.json({ token });
});
app.get('/profile', verifyToken, (req, res) => {
const user = {
id: req.userId,
username: 'user1',
email: '[email protected]'
};
res.json(user);
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
前端代码
创建index.html
:
<!DOCTYPE html>
<html>
<head>
<title>JWT SSO Example</title>
<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
fetch('/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'user1',
password: 'pass1'
})
})
.then(response => response.json())
.then(data => {
localStorage.setItem('token', data.token);
console.log('Token:', data.token);
fetch('/profile', {
headers: {
'Authorization': `Bearer ${data.token}`
}
})
.then(response => response.json())
.then(profile => {
console.log('Profile:', profile);
});
});
});
</script>
</head>
<body>
<h1>JWT SSO Example</h1>
</body>
</html>
案例二:基于Spring Boot的JWT单点登录实现
本案例将展示如何使用Spring Boot构建一个简单的JWT单点登录系统。
后端代码
创建pom.xml
:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>jwt-sso</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
``
创建`JwtUtil.java`:
```java
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class JwtUtil {
private String secret = "your256bitsecret";
public String generateToken(User user) {
return Jwts.builder()
.setSubject(user.getUsername())
.claim("id", user.getId())
.claim("email", user.getEmail())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
public Claims getClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
}
创建User.java
:
public class User {
private int id;
private String username;
private String email;
// getters and setters
}
创建JwtAuthenticationFilter.java
:
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (token != null) {
JwtUtil util = new JwtUtil();
if (util.validateToken(token)) {
Claims claims = util.getClaimsFromToken(token);
User user = new User();
user.setId((Integer) claims.get("id"));
user.setUsername((String) claims.getSubject());
user.setEmail((String) claims.get("email"));
SecurityContextHolder.getContext().setAuthentication(new UserAuthenticationToken(user));
}
}
filterChain.doFilter(request, response);
}
}
创建UserAuthenticationToken.java
:
import org.springframework.security.core.Authentication;
public class UserAuthenticationToken implements Authentication {
private User user;
public UserAuthenticationToken(User user) {
this.user = user;
}
@Override
public Object getCredentials() {
return user.getPassword();
}
@Override
public Object getPrincipal() {
return user;
}
@Override
public Object getDetails() {
return null;
}
@Override
public Object getAuthorities() {
return null;
}
@Override
public boolean isAuthenticated() {
return true;
}
@Override
public void setAuthenticated(boolean b) {
this.user.setPassword(null);
}
@Override
public String getName() {
return user.getUsername();
}
}
创建UserController.java
:
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/auth")
public class UserController {
@PostMapping("/login")
public Map<String, String> login(@RequestBody User user) {
JwtUtil util = new JwtUtil();
String token = util.generateToken(user);
return Map.of("token", token);
}
@GetMapping("/profile")
public User profile() {
return (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
}
前端代码
创建index.html
:
<!DOCTYPE html>
<html>
<head>
<title>JWT SSO Example</title>
<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
axios.post('/auth/login', {
username: 'user1',
password: 'pass1'
})
.then(response => {
localStorage.setItem('token', response.data.token);
console.log('Token:', response.data.token);
return axios.get('/auth/profile', {
headers: {
'Authorization': `Bearer ${response.data.token}`
}
});
})
.then(response => {
console.log('Profile:', response.data);
})
.catch(error => {
console.error('Error:', error);
});
});
</script>
</head>
<body>
<h1>JWT SSO Example</h1>
</body>
</html>
共同學習,寫下你的評論
評論加載中...
作者其他優質文章