一、JWT详解
1.1 什么是JWT
1.1.1 JWT的定义
JWT(JSON Web Token)是一种基于JSON格式的开放标准(RFC 7519),主要用于在各个系统之间安全地传输信息。它通过使用数字签名可以验证数据的完整性,从而实现了在不同系统之间的安全通信。它的标准结构为:“aaa.bbb.ccc”,其中第一部分是Header,第二部分是Payload,第三部分是Signature。
1.1.2 JWT的优势
与传统的Session认证方式相比,JWT有以下优点:
- 无状态:JWT通过将状态信息储存在token中,降低了服务端的状态存储压力,使服务端变得无状态化。
- 分布式系统:JWT可以在不同的应用系统中传递,可以方便地用于分布式系统中的身份验证和授权。
- 可定制性:JWT内置了Header、Payload和Signature三个部分,开发人员可以根据自己的需求,在Payload中添加自定义信息。
1.2 JWT的组成
1.2.1 Header
Header部分通常由两部分组成:token类型和算法类型,它们使用base64Url编码后组成一个字符串。例如:
{ "alg": "HS256", "typ": "JWT" }
其中,alg表示加密算法,typ表示token类型。JWT支持多种加密算法,如HS256、HS512、RS256等。
1.2.2 Payload
Payload是JWT的第二部分,它存放着需要传输的数据信息。Payload通常也由一些标准字段组成,比如iss、exp、sub、aud等。例如:
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
- sub:Subject,标识JWT的主题,即用户的唯一标识。
- name:用户名。
- iat: Issued At时间,即JWT的签发时间。它的值为时间戳,用于标识JWT的唯一性。
除了标准字段外,开发人员也可以在Payload中添加自定义的字段,以实现更多的功能。
1.2.3 Signature
Signature是JWT的第三部分,它使用Header中指定的算法对Header和Payload进行签名,从而保证JWT的完整性和真实性。Signature的值通常由Header、Payload、密钥和加密算法组成。例如:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
其中,secret为加密密钥,用于对Header和Payload进行签名。在验证JWT时,服务端会使用同样的算法和密钥来验证Signature的正确性。
二、JWT的使用
2.1 使用JWT
使用JWT的流程如下:
- 用户提供账号和密码进行登录认证;
- 服务端验证账号和密码的正确性,如果正确则生成JWT并返回给客户端;
- 客户端收到JWT后,将其储存在本地;
- 客户端在后续请求中携带JWT;
- 服务端使用相同的密钥对JWT进行验证,并根据其中的Payload信息进行相应的处理。
2.2 JWT的生成与验证
使用Java可以使用第三方库进行JWT的生成和验证。常见的库有jjwt和java-jwt。以下是使用jjwt库进行JWT的生成和验证的示例代码。
2.2.1 安装jjwt库
使用Maven配置依赖:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
或者下载jar包手动引入。
2.2.2 生成JWT
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; public class JwtUtil { public static String createJWT(String subject, String audience, String issuer, long ttlMillis, String secret) { // 签发时间 long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); // 设置签发时间和过期时间 Date exp = new Date(nowMillis + ttlMillis); // 构建JWT String jwt = Jwts.builder() .setSubject(subject) .setAudience(audience) .setIssuer(issuer) .setIssuedAt(now) .setExpiration(exp) .signWith(SignatureAlgorithm.HS256, secret) .compact(); return jwt; } }
其中,subject为JWT的主题,可以是用户ID;audience为JWT的受众,可以是应用名称;issuer为JWT的签发者,可以是服务器名称;ttlMillis为JWT的有效期,单位为毫秒;secret为JWT的密钥,用于签名。
2.2.3 验证JWT
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureException; public class JwtUtil { public static boolean validateJWT(String jwt, String secret) { try { // 验证JWT的签名 Claims claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(jwt).getBody(); // 验证JWT的过期时间 long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); Date exp = claims.getExpiration(); if (exp.before(now)) { return false; } return true; } catch (SignatureException e) { // 验证不通过 return false; } } }
其中,jwt为待验证的JWT字符串,secret为JWT的密钥。该方法会返回一个boolean值,表示JWT验证是否通过。
2.3 JWT的使用示例
以下是一个使用JWT进行身份验证的示例。假设有一个AdminController,该类的所有方法都需要进行身份验证。在每个请求中,客户端需要在Authorization Header中携带JWT。服务端在验证JWT通过后才会继续处理请求。
import io.jsonwebtoken.*; @RestController @RequestMapping("/admin") public class AdminController { @Autowired private AdminService adminService; @Value("${jwt.secret}") private String secret; @PostMapping("/login") public ResponseEntity login(@RequestBody Admin admin) { // 验证账号和密码 boolean authenticated = adminService.authenticate(admin); if (!authenticated) { return ResponseEntity.badRequest().build(); } // 生成JWT String jwt = JwtUtil.createJWT(admin.getId(), "admin", "MyApp", 3600000L, secret); return ResponseEntity.ok(jwt); } @GetMapping("/users") public ResponseEntity getUsers(@RequestHeader("Authorization") String auth) { // 验证JWT String jwt = auth.substring(7); boolean validated = JwtUtil.validateJWT(jwt, secret); if (!validated) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } // 处理请求 List<User> userList = adminService.getUsers(); return ResponseEntity.ok(userList); } }
在登录时,AdminController会验证账号和密码,如果成功,则生成JWT并返回给客户端。在后续请求中,客户端需要在Authorization Header中携带JWT。服务端在验证JWT通过后才会继续处理请求。
三、JWT的实践
3.1 Spring Boot 集成 JWT
在 Spring Boot 中集成 JWT,可以使用 JJwt 库,该库已经提供了 JWT 的创建、验证以及解析功能。在使用该库之前,需要在 pom.xml 文件中添加以下依赖:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.10.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.10.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.10.5</version> </dependency>
其中,jjwt-api
是 JWT 的 API 接口,jjwt-impl
是 JWT 的实现,jjwt-jackson
是为了支持 JWT 对象的序列化。
在 Spring Boot 中使用 JWT 的步骤如下:
3.1.1. 定义 JWT 的生成和解析接口
public interface JwtTokenProvider { String generateToken(Authentication authentication); boolean validateToken(String token); String getUsernameFromToken(String token); }
在接口中,定义了生成 Token、验证 Token 和从 Token 中获取用户名的方法。
3.1.2. 实现 JWT 的生成和解析接口
@Service public class JwtTokenProviderImpl implements JwtTokenProvider { private static final Logger logger = LoggerFactory.getLogger(JwtTokenProviderImpl.class); @Value("${app.jwtSecret}") private String jwtSecret; @Value("${app.jwtExpirationInMs}") private int jwtExpirationInMs; @Override public String generateToken(Authentication authentication) { UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); Date now = new Date(); Date expiryDate = new Date(now.getTime() + jwtExpirationInMs); return Jwts.builder() .setSubject(Long.toString(userPrincipal.getId())) .setIssuedAt(new Date()) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, jwtSecret) .compact(); } @Override public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token); return true; } catch (SignatureException ex) { logger.error("Invalid JWT signature"); } catch (MalformedJwtException ex) { logger.error("Invalid JWT token"); } catch (ExpiredJwtException ex) { logger.error("Expired JWT token"); } catch (UnsupportedJwtException ex) { logger.error("Unsupported JWT token"); } catch (IllegalArgumentException ex) { logger.error("JWT claims string is empty."); } return false; } @Override public String getUsernameFromToken(String token) { Claims claims = Jwts.parser() .setSigningKey(jwtSecret) .parseClaimsJws(token) .getBody(); return claims.getSubject(); } }
在实现类中,使用 Jwts 对象生成 Token,使用 setSigningKey 方法设置签名密钥,使用 parseClaimsJws 方法解析 Token,使用getBody 方法获取 Token 中存储的所有信息。
3.1.3. 在 Spring Security 中使用 JWT
在 Spring Security 中使用 JWT,需要进行以下配置:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenProvider jwtTokenProvider; @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(jwtTokenProvider, userDetailsService); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable() .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() .antMatchers("/api/auth/**").permitAll() .antMatchers("/api/user/checkUsernameAvailability", "/api/user/checkEmailAvailability").permitAll() .anyRequest().authenticated(); http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } }
其中,JwtAuthenticationFilter 是自定义的过滤器,用于拦截请求并验证 Token。需要注意,使用 JWT 的时候,需要设置 Spring Security 的 sessionCreationPolicy 为 STATELESS。
3.2 使用 JWT 进行认证和授权
使用 JWT 进行认证和授权的过程如下:
- 用户向服务器发送请求。
- 服务器收到请求,生成 JWT。
- 服务器将 JWT 发送给客户端。
- 客户端将 JWT 存储在本地。
- 客户端向服务器发送请求,使用存储的 JWT 进行认证和授权。
- 服务器验证 JWT,验证通过后返回请求的内容。
- 客户端使用返回的内容进行操作。
在使用 JWT 进行认证和授权的时候,需要注意以下几点:
- JWT 中需要包含用户的信息,例如用户 ID、用户名等。
- JWT 的签名密钥需要保密,不能泄露给外部。
- JWT 需要设置过期时间。
- JWT 不应该包含敏感信息,例如密码等。
考虑到这些问题,使用 JWT 进行认证和授权的时候需要根据实际情况进行选择,并进行相应的安全设置。
四、JWT的安全
JWT在实现认证和授权的同时,也需要考虑安全性。本章将介绍JWT的安全问题及相关解决方案。
4.1. JWT的漏洞
在使用JWT时,需要注意以下几个方面的安全问题:
- 私钥保护问题:JWT的签发是由服务端进行的,所以服务端需要妥善保管自己的私钥。如果私钥泄露,攻击者可以伪造JWT,篡改数据,危害系统安全。
- Token劫持问题:JWT在传输过程中可能被第三方窃取,攻击者可以利用JWT进行伪造请求,篡改数据等恶意行为。因此,JWT需要采用HTTPS协议进行传输,同时服务端也应在JWT中加入一些防篡改的信息,比如过期时间、签名等。
- 敏感信息问题:JWT中的Payload部分是明文存储的,如果将敏感信息存入Payload中,在JWT被反解出来后会泄露敏感信息。因此,JWT中只应存放一些基本信息,敏感信息应该存放在服务端中。
4.2. JWT的安全解决方案
为解决JWT存在的安全问题,有以下几种方案:
- 选择合适的算法:JWT中的签名算法影响着JWT的安全性,应根据系统的实际情况选择合适的算法。建议采用RSA算法或HMAC-SHA256算法,不建议使用HMAC-SHA1算法。
- 签名时加盐:为防止黑客破解签名,应在签名时加盐,使攻击者更难猜测签名的值,进而增强JWT的安全性。
- 使用HTTPS协议传输:在传输JWT时,应使用HTTPS协议进行传输,避免Token被第三方窃取。
- 设置Token的过期时间:为了防止Token被长期滥用,应在Token中设置过期时间。
- JWT中不存储敏感信息:为了保护敏感信息,JWT中只应存放一些基本信息,敏感信息应该存放在服务端中。
除了以上方案,还可以使用一些第三方的JWT安全解决方案,比如Auth0提供的JWT黑名单、JWT白名单,可以对Token进行有效性判断。
五、总结
本文介绍了JSON Web Token(JWT)的基础知识、原理、应用场景、Spring Boot集成以及安全问题及解决方案。JWT作为一种轻量级的认证和授权机制,具有简单、可扩展性好、支持跨域等优点,在Web开发中得到了广泛的应用。
5.1. 优点
- 轻量级:JWT是一种轻量级的认证和授权机制,Token的体积很小,传输速度快。
- 自包含:JWT是一种自包含的Token,里面包含了用户的身份信息,无需再向数据库查询,减少了服务器的压力。
- 可扩展性好:JWT中的Payload可以存放任何数据,具有很好的扩展性。
- 支持跨域:JWT可以跨域传输,适用于多种场景。
5.2. 缺点
- 安全性:JWT存在私钥保护、Token劫持、敏感信息泄露等安全问题。
- 无法撤回:一旦JWT被签发,就无法撤回,除非Token过期。
- 需要存储Payload:JWT中的Payload部分是明文存储的,如果存储敏感信息,会存在信息泄露的风险。
5.3. 应用场景
- 单点登录(SSO):JWT可以在多个应用之间共享用户信息,实现单点登录。
- 前后端分离:前后端分离的应用中,JWT作为一种轻量级的认证和授权机制,可以方便的实现用户登录。
- 无状态认证:JWT的自包含特性可以实现无状态认证,减少服务器压力。
5.4. 未来
随着互联网的发展,移动端应用和Web应用的用户认证和授权需求越来越多,JWT作为一种轻量级的认证和授权机制,具有应用价值。未来,在不断的技术迭代中,JWT也会愈加成熟、完善,可能出现更多的安全性强、使用更加方便的JWT实现方案。