1. 《理论知识》
首先是一些理论知识:
本文理论知识来自于以下两篇文章:
www.freecodecamp.org/chinese/new…
www.freecodecamp.org/news/how-to…
1. 什么是JWT?
在我们正式开始之前,让我们快速回顾一下JSON Web Token(JWT)到底是什么。
JSON Web Token(JWT)是一种在两方之间紧凑的、URL安全的传输数据的方式,用来管理user session的状态。
它由开放标准(RFC 7519)定义,并由三个部分组成:header(头部)、payload(负载)以及一个Signature(加密)部分。
JWT在生成时会被签名,相同的签名JWT在收到时会被验证,以确保它在传输过程中没有被修改。
--出自
2. JWT工作原理
3. JWT的组成部分
JWT由3部分组成,每一部分用一个.隔开。
JWT的Header(头部)部分
第一个部分是头部,如下:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9
头部是一个JSON对象,包含了一个签名算法和一个令牌类型。它是由base64Url编码而成。
解码后如下:
{
"alg": "RS256",
"typ": "JWT"
}
其中,alg代表此次加密所使用的算法(algorithm)。
typ用于表明这个token(令牌)的类型是什么。我们现在传给服务器的token是一个JSON web token(JWT)。
JWT的Payload(负载)部分
第二部分是负载:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0
这是一个包含数据声明的JSON对象,其中包含有关用户的信息和其他与身份验证相关的信息。
是JWT从一个实体传递到另一个实体的信息。它也是base64Url编码的。数据声明如下所示:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
因为JWT通常用来进行用户身份验证(User Identification),所以通常,它的Payload(负载)部分都是用户的身份信息等机密信息。
JWT的signature(加密/签名)部分
最后一部分是加密/签名部分。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
通常来说,它的组成部分是:你这次JWT的头部部分(header),负载部分(payload),以及一个“秘密”(secret)--这通常是用来加密的算法中的“密钥”内容。(原文:usually the contents of a key in a signing algorithm)。
用这“三部分”内容组合起来,进行一次加密,变成了JWT的签名部分(Signature)。
JWT被签名之后不能在传输的过程中被修改。一旦修改,则JWT验证会失败。
请注意:JWT的Signature(加密/签名)部分,虽然有过加密,但是这只是作为“身份验证(Validation)”来使用。它并不会对JWT的前两段(header,payload)进行任何加密,所以永远不要把“密码”等机密信息放入JWT并从服务器发回客户端。一个JWT的header和payload只应存一些公共内容。
4. JWT怎么进行验证
JWT具体的验证方法,会根据在头部(header)中写明的加密算法的不同,而略有不同。
但是通常来说,可以理解为:
服务器会保存一份JWT信息,然后当你传入一个JWT验证请求的时候,它会根据你传入的JWT的header和payload,再加上本算法的“密钥”,生成一个Signature(JWT的第三段)。
如果这个Signature,和服务器上保留的JWT的Signature一致,那就是正确的。反正验证失败。
2. 《代码修改》
1. 创建一个JWT工具类,用于处理token的生成和验证
package com.quickstore.security;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtTokenProvider {
@Value("${spring.security.jwt.secret}")
private String jwtSecret;
@Value("${spring.security.jwt.expiration}")
private long jwtExpiration;
private Key getSigningKey() {
byte[] keyBytes = jwtSecret.getBytes();
return Keys.hmacShaKeyFor(keyBytes);
}
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpiration);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(getSigningKey())
.compact();
}
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
}
2. 创建一个JWT认证过滤器
package com.quickstore.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtTokenProvider tokenProvider, UserDetailsService userDetailsService) {
this.tokenProvider = tokenProvider;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
String username = tokenProvider.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
3. 修改AuthController来返回JWT token
package com.quickstore.controller;
import com.quickstore.model.User;
import com.quickstore.security.JwtTokenProvider;
import com.quickstore.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
@RestController
@RequestMapping("/auth")
public class AuthController {
private static final Logger logger = LoggerFactory.getLogger(AuthController.class);
private final UserService userService;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider tokenProvider;
public AuthController(UserService userService, PasswordEncoder passwordEncoder, JwtTokenProvider tokenProvider) {
this.userService = userService;
this.passwordEncoder = passwordEncoder;
this.tokenProvider = tokenProvider;
}
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest loginRequest) {
logger.info("Attempting login for user: {}", loginRequest.getUsername());
User user = userService.findByUsername(loginRequest.getUsername());
if (user != null && passwordEncoder.matches(loginRequest.getPassword(), user.getPasswordHash())) {
logger.info("Login successful for user: {}", user.getUsername());
UserDetails userDetails = new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPasswordHash(),
Collections.singletonList(new org.springframework.security.core.authority.SimpleGrantedAuthority("ROLE_" + user.getRole().toUpperCase()))
);
String token = tokenProvider.generateToken(userDetails);
return ResponseEntity.ok(new LoginResponse(token, user.getUsername(), user.getRole()));
}
logger.warn("Login failed for user: {}", loginRequest.getUsername());
return ResponseEntity.badRequest().body(null);
}
}
class LoginRequest {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
class LoginResponse {
private String token;
private String username;
private String role;
public LoginResponse(String token, String username, String role) {
this.token = token;
this.username = username;
this.role = role;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}
4. 更新SecurityConfig,配置JWT过滤器
package com.quickstore.config;
import com.quickstore.security.JwtAuthenticationFilter;
import com.quickstore.security.JwtTokenProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtTokenProvider tokenProvider, UserDetailsService userDetailsService) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(new JwtAuthenticationFilter(tokenProvider, userDetailsService),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
5. 验证JWT功能是否完成
- 使用Postman发送登录请求:
- URL:http://localhost:8080/api/auth/login
- Method: POST
- Header:
Content-Type: application/json - Body(raw JSON):
{ "username": "admin", "password": "admin123" }
- 如果成功就会返回token(由服务器创建):
{
"token": "eyJhbGciOiJIUzI1NiJ9...",
"username": "admin",
"role": "ADMIN"
}
返回
200状态,并拿到token值。登录完成!
- 客户端发送
JWT token回服务器,请求校验:
在Header中添加我们拿到的token值:
Key: Authorization
Value: bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImlhdCI6MTc0NzY1NDEzOSwiZXhwIjoxNzQ3NzQwNTM5fQ.Gpjmr0GujDFpJn0NTSCb8_1E2KJVitaMNgi9edXF0Q0
返回
200状态,验证完成!
注:之所以value里面token前面还有一个bearer , 是因为我们在JwtAuthenticationFilter里面写了要以bearer 开头:
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
至此,为登录功能添加JWT验证功能。完成。
题外话:
至此,一个全栈项目的“数据库搭建”“用户登录功能”“JWT验证”就完成了。面对面试官的话,就可以围绕这几个方面展开,展示你“全栈工程师”的能力(偏后端)了。
比如:
- 使用SQL语句创建数据库
- 使用script(脚本)建立数据库表
- 完成用户登录功能
- 对用户密码进行加密
- 对访问链接进行过滤(application.yml里面规定了链接必须要以
api开头, SecurityConfig里面规定了/auth/**类型的链接才能被允许通行(permit)。两个合在一起,就是api/auth/**类型的链接才能通行,其他不符合要求的访问请求,会被Spring boot的Security(安全校验)部分拒绝。) - JWT验证是什么意思
- JWT token的三段式结构
- 为什么JWT token具有安全性
- JWT token是否可以伪造
- 你在你的项目里是怎么添加JWT并使用什么软件进行验证等
希望能以此找个好工作,Good Luck!
下一篇:新用户创建(使用Postman)