前言
在网络的世界中,数据安全是应用开发的头等大事。而Spring Boot与JWT的完美结合,就像是为你的应用添加了一把坚实的安全之锁。从今天开始,让我们一同踏上这场奇妙的旅程,揭开Spring Boot整合JWT的神秘面纱。
第一:项目整合
基本配置
package test.bo.springbootjwt.util; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTCreator; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; import java.util.Calendar; import java.util.Map; public class JwtUtil { private static final String sign="XS4B#2&1!*"; //生成token header payload sign public static String getToken(Map<String,String> map){ Calendar instance = Calendar.getInstance(); instance.add(Calendar.HOUR,24);//默认一天过期 //创建jwt builder JWTCreator.Builder builder = JWT.create(); //payload map.forEach((k,v)->{ builder.withClaim(k,v); }); String token = builder.withExpiresAt(instance.getTime())//指定令牌过期时间 .sign(Algorithm.HMAC256(sign)); return token; } //验证token public static DecodedJWT verify(String token){ return JWT.require(Algorithm.HMAC256(sign)).build().verify(token); } //获取token信息方法 public static DecodedJWT getTokerInfo(String token){ DecodedJWT verify = JWT.require(Algorithm.HMAC256(sign)).build().verify(token); return verify; } }
token验证配置
package test.bo.springbootjwt.interceptors; import com.auth0.jwt.exceptions.AlgorithmMismatchException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.exceptions.TokenExpiredException; import org.springframework.http.HttpMethod; import org.springframework.web.servlet.HandlerInterceptor; import test.bo.springbootjwt.entity.Result; import test.bo.springbootjwt.util.JwtUtil; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class JWTInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException, ServletException { if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) { System.out.println("OPTIONS请求,放行"); return true; } response.setContentType("application/json;charset=UTF-8"); Result result = new Result(); //获取请求头中令牌 String token = request.getHeader("token0001"); System.out.println(token); try { JwtUtil.verify(token);//验证令牌 return true; }catch (SignatureVerificationException e){ e.printStackTrace(); result.setMsg("无效签名"); }catch (TokenExpiredException e){ e.printStackTrace(); result.setMsg("token过期"); }catch (AlgorithmMismatchException e){ e.printStackTrace(); result.setMsg("token算法不一致"); }catch (Exception e){ e.printStackTrace(); result.setMsg("token无效"); } result.setState(false); /* String string = new ObjectMapper().writeValueAsString(result); response.setContentType("application/json;charset=utf8"); response.getWriter().println(string);*/ return false; } }
拦截器配置
package test.bo.springbootjwt.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import test.bo.springbootjwt.interceptors.JWTInterceptor; @Configuration @CrossOrigin public class InterceptorConfig implements WebMvcConfigurer { public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS") .maxAge(36000) .allowedHeaders("*") .allowCredentials(true); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JWTInterceptor()) .addPathPatterns("/**") //其他接口token验证 .excludePathPatterns("/user/login"); //所有用户都放行 } }
依赖的pom坐标
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.10.3</version> </dependency>
第二:Spring Security与JWT的完美协作
Spring Security和JWT的结合在现代Web应用中是一种常见的做法,它提供了一种安全的身份验证和授权机制。以下是配置Spring Security以使用JWT进行身份验证的一般步骤:
步骤1: 引入依赖
确保项目中引入了Spring Security和JWT的相关依赖。可以使用Maven或Gradle进行引入,例如:
Maven:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> <!-- 替换为最新版本 --> </dependency>
Gradle:
implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'io.jsonwebtoken:jjwt:0.9.1' // 替换为最新版本
步骤2: 配置Spring Security
在Spring Boot应用的配置类中配置Spring Security,指定哪些路径需要进行身份验证,哪些路径不需要。
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/public/**").permitAll() // 不需要身份验证的路径 .anyRequest().authenticated() .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 禁用Session } }
步骤3: 实现JWT工具类
实现一个用于生成和解析JWT的工具类。可以使用JJWT库来简化这个过程。
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.security.core.userdetails.UserDetails; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.function.Function; public class JwtUtil { private static final String SECRET_KEY = "your_secret_key"; public static String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); return createToken(claims, userDetails.getUsername()); } private static String createToken(Map<String, Object> claims, String subject) { return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // Token有效期10小时 .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static Boolean validateToken(String token, UserDetails userDetails) { final String username = extractUsername(token); return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } private static Boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } private static Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } private static <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { final Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); } private static Claims extractAllClaims(String token) { return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody(); } private static String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } }
步骤4: 集成JWT到身份验证过滤器
创建一个自定义的过滤器,用于在Spring Security的过滤链中验证JWT,并将用户信息添加到Spring Security上下文。
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class JwtRequestFilter extends UsernamePasswordAuthenticationFilter { private final UserDetailsService userDetailsService; private final JwtUtil jwtUtil; public JwtRequestFilter(UserDetailsService userDetailsService, JwtUtil jwtUtil) { this.userDetailsService = userDetailsService; this.jwtUtil = jwtUtil; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { final String authorizationHeader = request.getHeader("Authorization"); String username = null; String jwt = null; if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { jwt = authorizationHeader.substring(7); username = jwtUtil.extractUsername(jwt); } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtUtil.validateToken(jwt, userDetails)) { UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); usernamePasswordAuthenticationToken .setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); } } chain.doFilter(request, response); } }
步骤5: 配置Spring Security使用JWT过滤器
在配置类中将自定义的JwtRequestFilter
添加到Spring Security的过滤器链中。
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 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.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtUtil jwtUtil; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/authenticate").permitAll() // 不需要身份验证的路径 .anyRequest().authenticated() .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 禁用Session http.addFilterBefore(jwtRequestFilter(), UsernamePasswordAuthenticationFilter.class); } @Bean public JwtRequestFilter jwtRequestFilter() { return new JwtRequestFilter(userDetailsService, jwtUtil); } @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder password Encoder() { return new BCryptPasswordEncoder(); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
以上是一个简单的集成Spring Security和JWT的例子,实际项目中可能需要根据具体需求进行更复杂的配置和定制。这个例子涵盖了基本的认证、授权和JWT生成、解析的流程。
第三:实际应用场景
Spring Boot整合JWT在实际应用中有许多灵活的应用场景,特别适用于前后端分离和微服务架构。以下是一些实际应用场景的示例:
1. 前后端分离应用
在前后端分离的应用中,前端和后端是独立开发和部署的,通过API进行通信。JWT可以在这种情境下提供简便的身份验证和授权机制。
- 用户认证: 用户在前端登录,后端验证用户名和密码,如果通过,后端生成JWT并返回给前端。
- 后续请求: 前端在每个请求的头部携带JWT,后端通过JWT验证用户身份和权限。
2. 微服务架构
在微服务架构中,每个服务都可以独立验证和授权用户,而不需要在每个服务中保存用户的状态。JWT在这种场景下提供了轻量级的、无状态的身份验证方式。
- 用户认证: 一个服务处理用户登录请求,生成JWT,返回给客户端。
- 微服务间通信: 客户端在与其他微服务通信时,携带JWT进行身份验证,每个微服务独立验证JWT。
3. 单点登录(SSO
JWT可以用于实现单点登录,使用户只需在系统中进行一次登录,即可访问多个关联系统。
- 登录过程: 用户在一个系统中登录,获取JWT。
- 访问其他系统: 用户在其他系统中携带JWT,其他系统通过验证JWT即可知晓用户身份。
4. Token刷新
JWT的过期机制使得可以轻松实现Token刷新,提高了系统的安全性。
- 刷新流程: 在JWT即将过期时,客户端使用刷新令牌或重新提供用户凭证来获取新的JWT,而无需重新输入用户名和密码。
5. 动态权限调整
在某些场景下,用户的权限可能会在登录后发生变化。JWT中包含的信息可以灵活地调整用户的权限。
- 权限调整: 当用户的权限发生变化时,系统可以主动撤销旧的JWT,生成包含新权限信息的JWT。
6. 无状态服务
JWT是无状态的,服务端不需要存储用户的状态信息,这对于水平扩展和微服务的部署非常有利。
- 水平扩展: 可以轻松地将请求路由到不同的服务实例,而不需要共享用户的状态。
7. 跨域资源共享(CORS)
在前后端分离的应用中,处理跨域问题是常见的挑战。JWT可以在跨域环境中通过在响应头中包含Token来解决跨域问题。
- CORS处理: 后端在响应头中包含JWT,前端在每次请求中都携带JWT,以解决浏览器的同源策略限制。
8. 移动应用身份验证
JWT也可以用于移动应用的身份验证,提供安全的身份验证机制,同时减少对服务器的频繁请求。
- 移动应用: 移动应用通过安全的方式将用户凭证传递给服务器,获取JWT。
- JWT在请求中携带: 移动应用在后续请求中携带JWT,服务端验证并处理请求。
这些场景展示了Spring Boot整合JWT在不同应用场景下的灵活应用。通过使用JWT,可以实现简单、安全且可扩展的身份验证和授权机制。
第四:使用公钥/私钥对JWT进行签名
使用公钥/私钥对JWT进行签名可以提高系统的安全性,确保只有拥有私钥的服务端才能生成有效的签名,而其他服务端或客户端则可以通过公钥验证签名的合法性。以下是生成密钥对并在Spring Boot中应用的步骤:
1. 生成密钥对
首先,你需要生成公钥/私钥对。可以使用Java的keytool
工具或者使用一些开发库来生成。
使用keytool生成密钥对:
keytool -genkeypair -alias myjwtkey -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore myjwtkeystore.p12 -validity 3650
这将生成一个2048位的RSA密钥对,并存储在名为myjwtkeystore.p12
的文件中。
使用开发库生成密钥对:
在Java中,你也可以使用一些开发库(例如Bouncy Castle)生成密钥对。以下是一个使用Bouncy Castle库的示例:
import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.Security; public class KeyPairGeneratorExample { public static void main(String[] args) throws NoSuchProviderException, NoSuchAlgorithmException { Security.addProvider(new BouncyCastleProvider()); KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", "BC"); generator.initialize(2048); // key size KeyPair keyPair = generator.generateKeyPair(); // keyPair.getPrivate() 返回私钥 // keyPair.getPublic() 返回公钥 } }
2. 在Spring Boot中应用密钥对
在Spring Boot中,可以通过配置文件将密钥加载到应用中,以便在JWT的签名和验证过程中使用。
application.properties(或application.yml)中配置密钥:
# 密钥的存储类型(JKS或PKCS12等) jwt.key-store-type=PKCS12 # 密钥库文件的路径 jwt.key-store=classpath:myjwtkeystore.p12 # 密钥库的密码 jwt.key-store-password=mypassword # 别名 jwt.key-alias=myjwtkey # 密钥的密码 jwt.key-password=mypassword
配置类中加载密钥:
import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.Certificate; import java.security.KeyStore; @Configuration public class JwtConfig { @Value("${jwt.key-store-type}") private String keyStoreType; @Value("${jwt.key-store}") private String keyStore; @Value("${jwt.key-store-password}") private String keyStorePassword; @Value("${jwt.key-alias}") private String keyAlias; @Value("${jwt.key-password}") private String keyPassword; @Bean public KeyPair keyPair() throws Exception { KeyStore keystore = KeyStore.getInstance(keyStoreType); keystore.load(getClass().getClassLoader().getResourceAsStream(keyStore), keyStorePassword.toCharArray()); KeyPair keyPair = new KeyPair( keystore.getCertificate(keyAlias).getPublicKey(), (PrivateKey) keystore.getKey(keyAlias, keyPassword.toCharArray()) ); return keyPair; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
这个配置类中的keyPair()
方法通过加载密钥库,获取公钥和私钥的方式创建了一个KeyPair
对象,以便在JWT的签名和验证中使用。
3. 在JWT签名和验证中使用密钥对
使用Spring Security和JWT库,可以在签发和验证JWT时使用密钥对。
生成JWT并签名:
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import java.util.Date; @Component public class JwtUtil { @Autowired private KeyPair keyPair; public String generateToken(UserDetails userDetails) { return Jwts.builder() .setSubject(userDetails.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10 hours .signWith(SignatureAlgorithm.RS256, keyPair.getPrivate()) .compact(); } }
验证JWT签名:
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import java.util.Date; @Component public class JwtUtil { @Autowired private KeyPair keyPair; public Boolean validateToken(String token, UserDetails userDetails) { Claims claims = Jwts.parser() .setSigningKey(keyPair.getPublic()) .parseClaimsJws(token) .getBody(); return userDetails.getUsername().equals(claims.getSubject()) && !isTokenExpired(token); } private Boolean isTokenExpired(String token) { Date expirationDate = Jwts.parser() .setSigningKey(keyPair.getPublic()) .parseClaimsJws(token) .getBody() .getExpiration(); return expirationDate.before(new Date()); } }
在这个例子中,SignatureAlgorithm.RS256
指定了RSA算法,使用keyPair.getPrivate()
进行签名,keyPair.getPublic()
进行验证。
使用公钥/私钥对JWT进行签名和验证提供了更高的安全性,确保只有拥有私钥的服务端才能生成有效的签名,而其他服务端或客户端则可以通过公钥验证签名的合法性。