Spring Security跨服务认证的JWT共享机制实现方案
大家好,今天我们来聊聊在微服务架构下,如何利用JWT (JSON Web Token) 实现跨服务的认证与授权。在单体应用中,Spring Security配合Session机制可以很好地完成认证授权,但在微服务架构中,每个服务都是独立部署和扩展的,Session共享变得复杂且难以维护。JWT由于其无状态性,成为了微服务认证授权的首选方案。
1. 认证授权的挑战与JWT的优势
在微服务架构中,用户认证和授权面临以下挑战:
- 服务间认证: 各个服务如何确认请求来自已认证的用户?
- Session共享问题: 如何在多个服务间共享Session信息?传统Session共享方案(如Session复制、共享Session存储)复杂且存在性能瓶颈。
- 单点登录 (SSO): 如何实现用户在一个地方登录,所有服务都自动认证?
JWT的优势在于:
- 无状态性: JWT包含用户身份信息,服务无需保存Session,每次请求都携带JWT进行验证。
- 可扩展性: 无需共享Session,服务可以独立扩展。
- 跨域支持: JWT可以方便地用于跨域认证。
- 安全性: JWT可以使用签名算法保证其完整性和不可篡改性。
2. JWT的基本原理
JWT是一个字符串,由三个部分组成,分别是:
- Header (头部): 描述JWT的元数据,通常包含签名算法和Token类型。例如:
{
"alg": "HS256",
"typ": "JWT"
}
- Payload (载荷): 包含用户的声明信息,例如用户ID、用户名、权限等。Payload可以包含注册声明 (Registered Claims)、公共声明 (Public Claims) 和私有声明 (Private Claims)。例如:
{
"sub": "user123",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
- Signature (签名): 通过将Header和Payload进行Base64编码后,使用Header中指定的签名算法(如HMAC SHA256)和一个密钥进行签名,用于验证Token的完整性。例如:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
这三部分用.连接起来,就形成了完整的JWT。
3. Spring Security集成JWT的实现方案
下面我们通过一个简单的例子,演示如何在Spring Security中集成JWT,实现跨服务的认证授权。假设我们有两个服务:Auth Service (认证服务) 和 Resource Service (资源服务)。
3.1 Auth Service (认证服务)
Auth Service负责用户认证,成功后生成JWT并返回给客户端。
- 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
- 实体类 (User):
import lombok.Data;
@Data
public class User {
private String username;
private String password;
private String role; // 用户的角色,例如 "ADMIN", "USER"
}
- JWT工具类 (JwtUtil):
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private long expiration;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public String generateToken(String username, String role) {
Map<String, Object> claims = new HashMap<>();
claims.put("role", role);
return createToken(claims, username);
}
private 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() + expiration * 1000))
.signWith(SignatureAlgorithm.HS256, secret).compact();
}
public Boolean validateToken(String token, String username) {
final String usernameFromToken = extractUsername(token);
return (usernameFromToken.equals(username) && !isTokenExpired(token));
}
}
- 配置application.properties:
jwt.secret=yourSecretKey
jwt.expiration=3600 # Token过期时间,单位秒
- 认证Controller:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AuthController {
@Autowired
private JwtUtil jwtUtil;
// 模拟用户认证
private boolean authenticate(String username, String password) {
// 在实际场景中,你需要从数据库或其他地方验证用户凭据
return "admin".equals(username) && "password".equals(password);
}
@PostMapping("/authenticate")
public ResponseEntity<?> authenticate(@RequestBody User user) {
if (authenticate(user.getUsername(), user.getPassword())) {
// 假设用户角色为 "ADMIN"
String token = jwtUtil.generateToken(user.getUsername(), "ADMIN");
return ResponseEntity.ok(new AuthenticationResponse(token));
} else {
return ResponseEntity.status(401).body("Invalid credentials");
}
}
}
// 简单的响应类
@Data
class AuthenticationResponse {
private final String token;
}
- Spring Security配置 (SecurityConfig):
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/authenticate").permitAll() // 允许访问 /authenticate 接口
.anyRequest().authenticated(); // 其他请求需要认证
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3.2 Resource Service (资源服务)
Resource Service需要验证请求头中的JWT,并根据JWT中的信息进行授权。
- 依赖: (与Auth Service相同)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
- JWT工具类 (JwtUtil): (与Auth Service相同)
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private long expiration;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public String generateToken(String username, String role) {
Map<String, Object> claims = new HashMap<>();
claims.put("role", role);
return createToken(claims, username);
}
private 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() + expiration * 1000))
.signWith(SignatureAlgorithm.HS256, secret).compact();
}
public Boolean validateToken(String token, String username) {
final String usernameFromToken = extractUsername(token);
return (usernameFromToken.equals(username) && !isTokenExpired(token));
}
}
- 配置application.properties: (注意:
jwt.secret必须与Auth Service一致)
jwt.secret=yourSecretKey
jwt.expiration=3600 # Token过期时间,单位秒
- JWT过滤器 (JwtRequestFilter):
import org.springframework.beans.factory.annotation.Autowired;
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.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;
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService; // 模拟UserDetailsService,实际从数据库获取用户信息
@Autowired
private 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);
try {
username = jwtUtil.extractUsername(jwt);
} catch (Exception e) {
// Token解析失败,记录日志
System.err.println("Token解析失败: " + e.getMessage());
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); // 模拟从数据库获取用户信息,实际从数据库获取
if (jwtUtil.validateToken(jwt, username)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
- UserDetailsService (模拟):
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 模拟从数据库获取用户信息
if ("admin".equals(username)) {
return new User("admin", "password", new ArrayList<>()); // 密码不重要,因为JWT已经验证过了
} else {
throw new UsernameNotFoundException("User not found: " + username);
}
}
}
- 资源Controller:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ResourceController {
@GetMapping("/resource")
public String getResource() {
return "This is a protected resource!";
}
@GetMapping("/admin/resource")
public String getAdminResource() {
return "This is an admin protected resource!";
}
}
- Spring Security配置 (SecurityConfig):
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.configuration.WebSecurityConfigurerAdapter;
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.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService myUserDetailsService;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/authenticate").permitAll() // 允许访问 /authenticate 接口
.antMatchers("/admin/**").hasAuthority("ADMIN") // 需要ADMIN权限
.anyRequest().authenticated() // 其他请求需要认证
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 无状态Session
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3.3 流程说明
- 客户端向Auth Service的
/authenticate接口发送用户名和密码。 - Auth Service验证用户凭据,如果验证成功,则生成JWT。JWT的Payload中包含用户名和角色信息。
- Auth Service将JWT返回给客户端。
- 客户端将JWT存储在本地 (例如,LocalStorage或Cookie)。
- 客户端在访问Resource Service时,将JWT放在
Authorization请求头中,格式为Bearer <JWT>。 - Resource Service的
JwtRequestFilter拦截请求,从Authorization头中提取JWT。 JwtRequestFilter使用JwtUtil验证JWT的签名和过期时间。- 如果JWT有效,
JwtRequestFilter从JWT中提取用户名和角色信息,并创建一个UsernamePasswordAuthenticationToken。 JwtRequestFilter将UsernamePasswordAuthenticationToken设置到SecurityContextHolder中。- Resource Service根据
SecurityContextHolder中的认证信息进行授权,判断用户是否具有访问资源的权限。
4. JWT共享的关键点
- 统一的密钥 (secret): Auth Service和Resource Service必须使用相同的密钥来签名和验证JWT。密钥应该安全地存储,并定期轮换。
- 统一的JWT工具类 (JwtUtil): Auth Service和Resource Service可以使用相同的
JwtUtil类,或者各自实现,但必须保证签名算法和密钥一致。 - 清晰的权限设计: Payload中的权限信息需要清晰定义,方便Resource Service进行授权判断。可以使用角色 (Role-Based Access Control, RBAC) 或权限 (Permission-Based Access Control) 模型。
5. 高级话题
- 刷新Token (Refresh Token): 为了避免频繁重新登录,可以使用刷新Token机制。当JWT过期时,客户端可以使用刷新Token向Auth Service请求新的JWT。
- JWT存储: 客户端可以安全地存储JWT,例如使用HttpOnly Cookie或加密的LocalStorage。
- OAuth 2.0集成: 可以将JWT与OAuth 2.0协议结合使用,实现更完善的认证授权流程。
- 服务发现与配置中心: 在微服务架构中,可以使用服务发现 (例如Eureka, Consul) 和配置中心 (例如Spring Cloud Config, Apollo) 来动态管理Auth Service和Resource Service的配置信息,包括JWT密钥。
- OIDC (OpenID Connect): OIDC 是一个基于 OAuth 2.0 的身份验证协议。它允许客户端验证用户的身份,并获取用户的基本信息,例如姓名、电子邮件地址等。OIDC 可以简化认证流程,并提供更高的安全性。
6. 代码示例:刷新Token的实现
为了支持刷新Token,我们需要在Auth Service中增加以下功能:
- 数据库存储RefreshToken: 创建一个RefreshToken表,用于存储RefreshToken以及与之关联的用户信息。
- 生成RefreshToken的接口: 在用户登录成功后,除了生成JWT,还需要生成一个RefreshToken并将其存储到数据库中。
- 刷新Token的接口: 提供一个接口,接收RefreshToken,验证其有效性,如果有效则生成新的JWT和RefreshToken,并更新数据库中的RefreshToken。
以下是示例代码:
- RefreshToken实体类:
import lombok.Data;
import javax.persistence.*;
import java.time.Instant;
@Entity
@Data
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String token;
private String username;
private Instant expiryDate;
}
- RefreshTokenRepository接口:
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
Optional<RefreshToken> findByToken(String token);
void deleteByUsername(String username);
}
- RefreshTokenService类:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.UUID;
@Service
public class RefreshTokenService {
@Value("${jwt.refresh.expiration}")
private Long refreshTokenDurationMs;
@Autowired
private RefreshTokenRepository refreshTokenRepository;
public RefreshToken createRefreshToken(String username) {
RefreshToken refreshToken = new RefreshToken();
refreshToken.setUsername(username);
refreshToken.setToken(UUID.randomUUID().toString());
refreshToken.setExpiryDate(Instant.now().plusMillis(refreshTokenDurationMs));
refreshToken = refreshTokenRepository.save(refreshToken);
return refreshToken;
}
public RefreshToken verifyExpiration(RefreshToken token) {
if (token.getExpiryDate().compareTo(Instant.now()) < 0) {
refreshTokenRepository.delete(token);
throw new RuntimeException(token.getToken() + " Refresh token was expired. Please make a new signin request");
}
return token;
}
public void deleteByUsername(String username) {
refreshTokenRepository.deleteByUsername(username);
}
}
- AuthController更新:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AuthController {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private RefreshTokenService refreshTokenService;
// 模拟用户认证
private boolean authenticate(String username, String password) {
// 在实际场景中,你需要从数据库或其他地方验证用户凭据
return "admin".equals(username) && "password".equals(password);
}
@PostMapping("/authenticate")
public ResponseEntity<?> authenticate(@RequestBody User user) {
if (authenticate(user.getUsername(), user.getPassword())) {
// 假设用户角色为 "ADMIN"
String token = jwtUtil.generateToken(user.getUsername(), "ADMIN");
RefreshToken refreshToken = refreshTokenService.createRefreshToken(user.getUsername());
return ResponseEntity.ok(new AuthenticationResponse(token, refreshToken.getToken()));
} else {
return ResponseEntity.status(401).body("Invalid credentials");
}
}
}
// 更新响应类
@Data
class AuthenticationResponse {
private final String token;
private final String refreshToken;
}
- 新增刷新token接口:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RefreshTokenController {
@Autowired
private RefreshTokenService refreshTokenService;
@Autowired
private RefreshTokenRepository refreshTokenRepository;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/refreshtoken")
public ResponseEntity<?> refreshtoken(@RequestBody RefreshTokenRequest request) {
String requestRefreshToken = request.getRefreshToken();
return refreshTokenRepository.findByToken(requestRefreshToken)
.map(refreshTokenService::verifyExpiration)
.map(RefreshToken::getUsername)
.map(username -> {
String token = jwtUtil.generateToken(username, "ADMIN"); // 假设刷新后角色不变
return ResponseEntity.ok(new AuthenticationResponse(token, requestRefreshToken));
})
.orElseThrow(() -> new RuntimeException("Refresh token is not in database!"));
}
}
@Data
class RefreshTokenRequest {
private String refreshToken;
}
- application.properties添加refreshToken过期时间:
jwt.refresh.expiration=86400000 # Refresh Token过期时间,单位毫秒,这里设置为24小时
这个代码示例展示了如何在Auth Service中实现RefreshToken的生成、存储和验证。 在客户端,需要修改逻辑为:如果JWT过期,则使用RefreshToken向/refreshtoken接口请求新的JWT和RefreshToken。
7. 总结一下
通过以上步骤,我们实现了一个基于JWT的跨服务认证授权方案。该方案具有无状态、可扩展、跨域支持等优点,适用于微服务架构。需要注意的是,密钥管理、权限设计和刷新Token机制是保障安全性的关键。此外,还可以考虑与OAuth 2.0和OIDC等协议集成,构建更完善的认证授权体系。