引言
在现代Web应用开发中,安全认证和授权是至关重要的组成部分。随着微服务架构的普及和前后端分离的流行,传统的Session认证机制已经无法满足现代应用的需求。JWT(JSON Web Token)作为一种开放标准(RFC 7519),为构建安全可靠的认证授权体系提供了理想的解决方案。
Spring Security作为Spring生态系统中最重要的安全框架,提供了强大的认证和授权功能。结合JWT技术,我们可以构建一个既安全又灵活的认证授权系统。本文将详细介绍如何使用Spring Security结合JWT技术构建企业级的认证授权体系,涵盖用户认证、权限控制、Token刷新机制、跨域处理等核心功能。
JWT技术原理
什么是JWT
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。JWT由三部分组成,用点(.)分隔:
- Header(头部):包含令牌类型和签名算法信息
- Payload(载荷):包含声明信息,如用户身份、权限等
- Signature(签名):用于验证令牌的完整性
JWT的优势
- 无状态:服务器不需要存储会话信息
- 跨域支持:可以在不同域名间共享
- 移动友好:适合移动端应用
- 可扩展性:可以包含自定义声明
- 性能优越:减少数据库查询次数
系统架构设计
整体架构
基于Spring Security的JWT认证授权系统采用分层架构设计,主要包括以下几个核心组件:
- 认证服务层:处理用户登录、Token生成等
- 授权服务层:处理权限验证、角色控制等
- 安全过滤器层:拦截请求,验证Token
- 业务逻辑层:具体的业务实现
- 数据访问层:用户信息存储和查询
核心流程
用户登录 → 验证凭据 → 生成JWT → 返回Token → 后续请求携带Token → 验证Token → 执行业务逻辑
技术栈选型
核心依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
配置文件
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/security_db?useSSL=false&serverTimezone=UTC
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
jwt:
secret: mySecretKeyForSecurity
expiration: 86400000 # 24小时
refresh-expiration: 604800000 # 7天
用户实体设计
用户实体类
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(unique = true, nullable = false)
private String email;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "role")
private List<String> roles;
private Boolean enabled = true;
// 构造函数、getter、setter
public User() {}
public User(String username, String password, String email, List<String> roles) {
this.username = username;
this.password = password;
this.email = email;
this.roles = roles;
}
// getter和setter方法
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
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; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public List<String> getRoles() { return roles; }
public void setRoles(List<String> roles) { this.roles = roles; }
public Boolean getEnabled() { return enabled; }
public void setEnabled(Boolean enabled) { this.enabled = enabled; }
}
角色枚举
public enum Role {
ROLE_USER,
ROLE_ADMIN,
ROLE_MANAGER
}
JWT工具类实现
JWT工具类
@Component
public class JwtTokenUtil {
private String secret;
private int jwtExpiration;
private int refreshExpiration;
@Value("${jwt.secret}")
public void setSecret(String secret) {
this.secret = secret;
}
@Value("${jwt.expiration}")
public void setJwtExpiration(int jwtExpiration) {
this.jwtExpiration = jwtExpiration;
}
@Value("${jwt.refresh-expiration}")
public void setRefreshExpiration(int refreshExpiration) {
this.refreshExpiration = refreshExpiration;
}
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
public String generateRefreshToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createRefreshToken(claims, userDetails.getUsername());
}
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() + jwtExpiration))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
private String createRefreshToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + refreshExpiration))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
public String refreshToken(String token) {
final Date createdDate = new Date();
final Date expirationDate = new Date(createdDate.getTime() + jwtExpiration);
final String username = getUsernameFromToken(token);
return Jwts.builder()
.setSubject(username)
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
}
Spring Security配置
安全配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig)
throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/manager/**").hasAnyRole("ADMIN", "MANAGER")
.requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN", "MANAGER")
.anyRequest().authenticated()
)
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
认证入口点
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
JWT过滤器实现
JWT请求过滤器
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
System.out.println("Unable to get JWT Token");
} catch (Exception e) {
System.out.println("JWT Token has expired");
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
认证服务实现
用户服务接口
public interface UserService {
User findByUsername(String username);
User findByEmail(String email);
User save(User user);
List<User> findAll();
void delete(Long id);
}
用户服务实现
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public User findByUsername(String username) {
return userRepository.findByUsername(username);
}
@Override
public User findByEmail(String email) {
return userRepository.findByEmail(email);
}
@Override
public User save(User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
return userRepository.save(user);
}
@Override
public List<User> findAll() {
return userRepository.findAll();
}
@Override
public void delete(Long id) {
userRepository.deleteById(id);
}
}
认证控制器
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
try {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String token = jwtTokenUtil.generateToken(userDetails);
String refreshToken = jwtTokenUtil.generateRefreshToken(userDetails);
return ResponseEntity.ok(new JwtResponse(token, refreshToken, userDetails.getUsername(),
userDetails.getAuthorities()));
} catch (AuthenticationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("Invalid credentials");
}
}
@PostMapping("/refresh")
public ResponseEntity<?> refresh(@RequestBody RefreshTokenRequest refreshTokenRequest) {
try {
String refreshToken = refreshTokenRequest.getRefreshToken();
String username = jwtTokenUtil.getUsernameFromToken(refreshToken);
UserDetails userDetails = userService.findByUsername(username);
if (jwtTokenUtil.validateToken(refreshToken, userDetails)) {
String newToken = jwtTokenUtil.refreshToken(refreshToken);
return ResponseEntity.ok(new JwtResponse(newToken, refreshToken, username,
userDetails.getAuthorities()));
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("Invalid refresh token");
}
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("Invalid refresh token");
}
}
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterRequest registerRequest) {
if (userService.findByUsername(registerRequest.getUsername()) != null) {
return ResponseEntity.badRequest().body("Username already exists");
}
if (userService.findByEmail(registerRequest.getEmail()) != null) {
return ResponseEntity.badRequest().body("Email already exists");
}
User user = new User(
registerRequest.getUsername(),
registerRequest.getPassword(),
registerRequest.getEmail(),
Arrays.asList("ROLE_USER")
);
userService.save(user);
return ResponseEntity.ok("User registered successfully");
}
}
请求响应对象
public class LoginRequest {
private String username;
private String password;
// 构造函数、getter、setter
public LoginRequest() {}
public LoginRequest(String username, String password) {
this.username = username;
this.password = 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; }
}
public class RefreshTokenRequest {
private String refreshToken;
// 构造函数、getter、setter
public RefreshTokenRequest() {}
public RefreshTokenRequest(String refreshToken) {
this.refreshToken = refreshToken;
}
public String getRefreshToken() { return refreshToken; }
public void setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; }
}
public class JwtResponse {
private String token;
private String refreshToken;
private String username;
private Collection<? extends GrantedAuthority> authorities;
public JwtResponse(String token, String refreshToken, String username,
Collection<? extends GrantedAuthority> authorities) {
this.token = token;
this.refreshToken = refreshToken;
this.username = username;
this.authorities = authorities;
}
// getter和setter方法
public String getToken() { return token; }
public void setToken(String token) { this.token = token; }
public String getRefreshToken() { return refreshToken; }
public void setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; }
public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = authorities;
}
}
权限控制实现
基于注解的权限控制
@RestController
@RequestMapping("/api/admin")
@PreAuthorize("hasRole('ADMIN')")
public class AdminController {
@GetMapping("/users")
public ResponseEntity<List<User>> getAllUsers() {
// 只有ADMIN角色可以访问
return ResponseEntity.ok(userService.findAll());
}
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
// 只有ADMIN角色可以创建用户
return ResponseEntity.ok(userService.save(user));
}
}
基于URL的权限控制
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/manager/**").hasAnyRole("ADMIN", "MANAGER")
.requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN", "MANAGER")
.anyRequest().authenticated()
)
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
Token刷新机制
刷新Token实现
@PostMapping("/refresh")
public ResponseEntity<?> refresh(@RequestBody RefreshTokenRequest refreshTokenRequest) {
try {
String refreshToken = refreshTokenRequest.getRefreshToken();
String username = jwtTokenUtil.getUsernameFromToken(refreshToken);
UserDetails userDetails = userService.findByUsername(username);
if (jwtTokenUtil.validateToken(refreshToken, userDetails)) {
String newToken = jwtTokenUtil.refreshToken(refreshToken);
return ResponseEntity.ok(new JwtResponse(newToken, refreshToken, username,
userDetails.getAuthorities()));
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("Invalid refresh token");
}
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("Invalid refresh token");
}
}
刷新Token的安全考虑
@Component
public class RefreshTokenUtil {
private static final int MAX_REFRESH_TOKENS = 10;
public boolean isRefreshTokenValid(String refreshToken, String username) {
// 检查刷新Token是否过期
// 检查是否被撤销
// 检查是否超过最大使用次数
return true;
}
public void revokeRefreshToken(String refreshToken) {
// 将刷新Token标记为已撤销
// 可以使用Redis存储撤销列表
}
}
跨域处理
跨域配置
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
configuration.setExposedHeaders(Arrays.asList("Authorization", "Content-Type"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
前端调用示例
// 前端JavaScript调用示例
const login = async (username, password) => {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok) {
localStorage.setItem('token', data.token);
localStorage.setItem('refreshToken', data.refreshToken);
return data;
} else {
throw new Error(data);
}
} catch (error) {
console.error('Login failed:', error);
throw error;
}
};
const apiCall = async (url, method = 'GET', data = null) => {
const token = localStorage.getItem('token');
const config = {
method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
}
};
if (data) {
config.body = JSON.stringify(data);
}
const response = await fetch(url, config);
if (response.status === 401) {
// Token过期,尝试刷新
const refreshToken = localStorage.getItem('refreshToken');
const refreshResponse = await fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ refreshToken })
});
if (refreshResponse.ok) {
const refreshData = await refreshResponse.json();
localStorage.setItem('token', refreshData.token);
// 重新执行原始请求
return apiCall(url, method, data);
} else {
// 刷新失败,跳转到登录页
window.location.href = '/login';
}
}
return response.json();
};
安全最佳实践
输入验证和过滤
@PostMapping("/register")
public ResponseEntity<?> register(@Valid @RequestBody RegisterRequest registerRequest,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResponseEntity.badRequest().body("Invalid input data");
}
// 验证用户名和邮箱的唯一性
if (userService.findByUsername(registerRequest.getUsername()) != null) {
return ResponseEntity.badRequest().body("Username already exists");
}
if (userService.findByEmail(registerRequest.getEmail()) != null) {
return ResponseEntity.badRequest().body("Email already exists");
}
// 密码强度验证
if (!isValidPassword(registerRequest.getPassword())) {
return ResponseEntity.badRequest().body("Password does not meet requirements");
}
User user = new User(
registerRequest.getUsername(),
registerRequest.getPassword(),
registerRequest.getEmail(),
Arrays.asList("ROLE_USER")
);
userService.save(user);
return ResponseEntity.ok("User registered successfully");
}
private boolean isValidPassword(String password) {
// 密码至少8位,包含大小写字母、数字和特殊字符
return password != null &&
password.length() >= 8 &&
password.matches(".*[a-z].*") &&
password.matches(".*[A-Z].*") &&
password.matches(".*\\d.*") &&
password.matches(".*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?].*");
}
安全头配置
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests(authz -> authz
.anyRequest().authenticated()
)
.headers(headers -> headers
.frameOptions().deny()
.contentTypeOptions().and()
.httpStrictTransportSecurity().maxAgeInSeconds(31536000).includeSubdomains(true).preload(true)
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
性能优化
缓存机制
@Service
public class CachedUserService {
@Autowired
private UserService userService;
@Cacheable(value = "users", key = "#username")
public User findByUsername(String username) {
return userService.findByUsername(username);
}
@CacheEvict(value = "users", key = "#user.username")
public User save(User user) {
return userService.save(user);
}
@CacheEvict(value = "users", key = "#id")
public void delete(Long id) {
userService.delete(id);
}
}
Redis集成
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
return template;
}
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(connectionFactory)
.withCacheConfiguration("users", config)
.build();
}
}
测试策略
单元测试
@SpringBootTest
class JwtTokenUtilTest {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Test
void testGenerateToken() {
UserDetails userDetails = User.builder()
.username("testuser")
.password("password")
.authorities("ROLE_USER")
.build();
String token = jwtTokenUtil.generateToken(userDetails);
assertNotNull(token);
assertFalse(token.isEmpty());
}
@Test
void testValidateToken() {
UserDetails userDetails = User.builder()
.username("testuser")
.password("password")
.authorities("ROLE_USER")
.build();
String token = jwtTokenUtil.generateToken(userDetails);
boolean isValid = jwtTokenUtil.validateToken(token, userDetails);
assertTrue(isValid);
}
}
集成测试
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class AuthIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void testUserLogin() {
LoginRequest loginRequest = new LoginRequest("testuser", "password");
ResponseEntity<JwtResponse> response = restTemplate.postForEntity(
"/api/auth/login", loginRequest, Jwt
评论 (0)