MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

如何实现基于JWT的身份验证

2021-09-297.8k 阅读

什么是 JWT

JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。这些信息可以被验证和信任,因为它们是数字签名的。JWT 通常由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。

JWT 的结构

  1. 头部(Header):通常由两部分组成:令牌的类型(即 JWT)和使用的哈希算法,如 HMAC SHA256 或 RSA。例如:
{
  "alg": "HS256",
  "typ": "JWT"
}

然后,这个 JSON 对象会使用 Base64Url 编码组成 JWT 的第一部分。

  1. 载荷(Payload):这部分包含声明(claims),也就是关于实体(通常是用户)和其他数据的陈述。有三种类型的声明:注册声明(如 iss、exp、sub 等)、公共声明和私有声明。例如:
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

同样,这个 JSON 对象会使用 Base64Url 编码组成 JWT 的第二部分。需要注意的是,由于载荷是 Base64Url 编码的,理论上任何人都可以解码它,所以不要在载荷中放置敏感信息,如密码等。

  1. 签名(Signature):要创建签名部分,需要使用编码后的头部、编码后的载荷、一个密钥(secret)和头部中指定的签名算法。例如,如果使用 HMAC SHA256 算法,签名将按如下方式创建:
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

签名用于验证消息在传输过程中没有被更改,并且在使用私钥签名的情况下,还可以验证 JWT 的发送者的身份。

JWT 在身份验证中的优势

  1. 无状态:服务器不需要在内存中存储任何关于用户会话的信息。每个请求都包含了足够的信息来验证用户身份,这使得应用程序更容易扩展,因为服务器可以独立处理每个请求,而不需要共享会话状态。
  2. 跨域友好:由于 JWT 通常通过 HTTP 头或 URL 参数传递,它可以很容易地在不同的域之间传递,这对于单页应用(SPA)和跨域资源共享(CORS)场景非常有用。
  3. 易于理解和解析:JWT 是基于 JSON 格式的,大多数编程语言都有库来处理 JSON,这使得开发人员可以很容易地在不同的后端技术栈中实现 JWT 的生成、验证和解析。

后端开发中实现基于 JWT 的身份验证流程

  1. 用户登录:用户在前端输入用户名和密码,然后将这些信息发送到后端服务器。
  2. 验证用户凭据:后端接收到用户的登录信息后,会查询数据库或其他身份验证源,验证用户名和密码是否匹配。
  3. 生成 JWT:如果用户凭据验证通过,后端会生成一个 JWT。生成 JWT 时,需要使用一个密钥(secret),这个密钥应该安全地存储在服务器端,不能泄露。
  4. 返回 JWT:后端将生成的 JWT 返回给前端。通常,JWT 会通过 HTTP 响应头(如 Authorization: Bearer <token>)或者在响应体中返回。
  5. 前端存储 JWT:前端接收到 JWT 后,会将其存储在本地,例如 localStorage、sessionStorage 或者 cookie 中。
  6. 后续请求:在后续的每个请求中,前端会将 JWT 包含在请求头中(如 Authorization: Bearer <token>)发送到后端。
  7. 验证 JWT:后端接收到请求后,会提取请求头中的 JWT,并使用相同的密钥(secret)来验证 JWT 的签名和有效性。如果 JWT 有效,后端可以从 JWT 的载荷中获取用户信息,并根据这些信息处理请求。

基于不同后端语言实现 JWT 身份验证的代码示例

使用 Node.js 和 Express 实现 JWT 身份验证

  1. 安装依赖:首先,确保你已经安装了 Node.js 和 npm。然后在项目目录下运行以下命令安装所需的依赖:
npm install express jsonwebtoken body-parser

express 是 Node.js 的 Web 应用框架,jsonwebtoken 用于生成和验证 JWT,body - parser 用于解析 HTTP 请求体。

  1. 创建 Express 应用:在项目根目录下创建一个 app.js 文件,内容如下:
const express = require('express');
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());

// 模拟用户数据
const users = [
  { username: 'admin', password: 'password123' }
];

// 登录路由
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const user = users.find(u => u.username === username && u.password === password);
  if (user) {
    const token = jwt.sign({ username }, 'your-secret-key', { expiresIn: '1h' });
    res.json({ token });
  } else {
    res.status(401).json({ message: 'Invalid credentials' });
  }
});

// 受保护的路由
app.get('/protected', authenticateToken, (req, res) => {
  res.json({ message: 'This is a protected route', user: req.user });
});

function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  if (token === null) return res.sendStatus(401);
  jwt.verify(token, 'your-secret-key', (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
}

const port = 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

在上述代码中:

  • /login 路由处理用户登录请求,验证用户凭据并生成 JWT 返回。
  • /protected 路由是一个受保护的路由,只有在 JWT 验证通过后才能访问。authenticateToken 中间件负责验证 JWT。

使用 Python 和 Flask 实现 JWT 身份验证

  1. 安装依赖:确保你已经安装了 Python 和 pip。然后在项目目录下运行以下命令安装所需的依赖:
pip install flask flask - jwt - extended

flask 是 Python 的 Web 应用框架,flask - jwt - extended 用于在 Flask 中处理 JWT。

  1. 创建 Flask 应用:在项目根目录下创建一个 app.py 文件,内容如下:
from flask import Flask, jsonify, request
from flask_jwt_extended import JWTManager, jwt_required, create_access_token

app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'your-secret-key'
jwt = JWTManager(app)

# 模拟用户数据
users = [
  {'username': 'admin', 'password': 'password123'}
]

# 登录路由
@app.route('/login', methods=['POST'])
def login():
  data = request.get_json()
  username = data.get('username')
  password = data.get('password')
  user = next((u for u in users if u['username'] == username and u['password'] == password), None)
  if user:
    access_token = create_access_token(identity=username)
    return jsonify(access_token=access_token)
  else:
    return jsonify({'message': 'Invalid credentials'}), 401

# 受保护的路由
@app.route('/protected', methods=['GET'])
@jwt_required()
def protected():
  current_user = get_jwt_identity()
  return jsonify({'message': 'This is a protected route', 'user': current_user})

if __name__ == '__main__':
  app.run(debug=True)

在上述代码中:

  • /login 路由处理用户登录请求,验证用户凭据并生成 JWT 返回。
  • /protected 路由是一个受保护的路由,@jwt_required() 装饰器负责验证 JWT。

使用 Java 和 Spring Boot 实现 JWT 身份验证

  1. 添加依赖:在 pom.xml 文件中添加以下依赖:
<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.11.2</version>
  </dependency>
  <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
  </dependency>
</dependencies>
  1. 创建 JWT 工具类:创建一个 JwtUtil 类来生成和验证 JWT:
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;

@Component
public class JwtUtil {
  private static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
  private static final long EXPIRATION_TIME = 10 * 60 * 1000; // 10 minutes

  public String generateToken(String username) {
    Date now = new Date();
    Date expiration = new Date(now.getTime() + EXPIRATION_TIME);

    Claims claims = Jwts.claims().setSubject(username);
    claims.put("iat", now);
    claims.put("exp", expiration);

    return Jwts.builder()
      .setClaims(claims)
      .signWith(key, SignatureAlgorithm.HS256)
      .compact();
  }

  public boolean validateToken(String token) {
    try {
      Claims claims = Jwts.parserBuilder()
        .setSigningKey(key)
        .build()
        .parseClaimsJws(token)
        .getBody();

      Date expiration = claims.getExpiration();
      return!expiration.before(new Date());
    } catch (Exception e) {
      return false;
    }
  }

  public String getUsernameFromToken(String token) {
    Claims claims = Jwts.parserBuilder()
      .setSigningKey(key)
      .build()
      .parseClaimsJws(token)
      .getBody();

    return claims.getSubject();
  }
}
  1. 创建用户服务和控制器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/api")
public class UserController {
  private final List<User> users = new ArrayList<>();

  @Autowired
  private JwtUtil jwtUtil;

  public UserController() {
    users.add(new User("admin", "password123"));
  }

  @PostMapping("/login")
  public ResponseEntity<String> login(@RequestBody User user) {
    if (users.stream().anyMatch(u -> u.getUsername().equals(user.getUsername()) && u.getPassword().equals(user.getPassword()))) {
      String token = jwtUtil.generateToken(user.getUsername());
      return new ResponseEntity<>(token, HttpStatus.OK);
    } else {
      return new ResponseEntity<>("Invalid credentials", HttpStatus.UNAUTHORIZED);
    }
  }

  @GetMapping("/protected")
  public ResponseEntity<String> protectedResource(@RequestHeader("Authorization") String token) {
    if (token != null && token.startsWith("Bearer ")) {
      String jwtToken = token.substring(7);
      if (jwtUtil.validateToken(jwtToken)) {
        String username = jwtUtil.getUsernameFromToken(jwtToken);
        return new ResponseEntity<>("This is a protected route, user: " + username, HttpStatus.OK);
      } else {
        return new ResponseEntity<>("Invalid token", HttpStatus.UNAUTHORIZED);
      }
    } else {
      return new ResponseEntity<>("Token is missing", HttpStatus.UNAUTHORIZED);
    }
  }
}

class User {
  private String username;
  private String password;

  public User(String username, String password) {
    this.username = username;
    this.password = password;
  }

  public String getUsername() {
    return username;
  }

  public String getPassword() {
    return password;
  }
}

在上述 Java 代码中:

  • JwtUtil 类负责生成和验证 JWT。
  • UserController 中的 /login 路由处理用户登录并生成 JWT,/protected 路由验证请求头中的 JWT 并处理受保护资源的请求。

JWT 的安全性考虑

  1. 密钥管理:密钥(secret)是 JWT 安全的关键。密钥应该足够长且随机,并且要安全地存储在服务器端。避免在代码中硬编码密钥,最好将其存储在环境变量或密钥管理服务(KMS)中。
  2. 过期时间设置:合理设置 JWT 的过期时间。如果过期时间过长,一旦 JWT 泄露,攻击者就有更多时间利用它进行非法操作;如果过期时间过短,用户可能需要频繁重新登录,影响用户体验。根据应用场景,如用户的操作频率、数据的敏感性等,来确定合适的过期时间。
  3. 防止 JWT 泄露:前端在存储 JWT 时,应避免使用 localStorage,因为它容易受到 XSS 攻击。推荐使用 HttpOnly cookie 来存储 JWT,如果必须使用 localStorage,可以结合其他安全机制,如在每次请求前对 JWT 进行加密处理。在网络传输过程中,确保使用 HTTPS 协议,防止中间人攻击截取 JWT。
  4. 验证签名和声明:后端在验证 JWT 时,不仅要验证签名的有效性,还要检查载荷中的声明,如过期时间、签发者等。确保所有的声明都符合预期,防止攻击者篡改声明。

总结 JWT 应用场景

  1. 用户身份验证:这是 JWT 最常见的应用场景。在用户登录后,后端生成 JWT 返回给前端,前端在后续请求中携带 JWT 进行身份验证,后端验证通过后处理请求。
  2. 单点登录(SSO):在多个应用系统组成的环境中,用户只需登录一次,就可以访问所有相互信任的应用系统。JWT 可以在不同的应用系统之间传递用户身份信息,实现单点登录。
  3. API 授权:对于一些需要授权才能访问的 API,JWT 可以作为授权凭证。客户端在请求 API 时携带 JWT,服务器验证 JWT 的有效性后决定是否允许访问 API。

通过以上内容,我们详细介绍了如何在后端开发中实现基于 JWT 的身份验证,包括 JWT 的原理、优势、实现流程以及不同后端语言的代码示例,同时也强调了安全性方面的考虑和应用场景。希望这些内容能帮助你在实际项目中有效地应用 JWT 进行身份验证。