5.1 Spring Security概述
Spring Security简介
Spring Security是Spring生态系统中的安全框架,提供了全面的安全服务:
- 认证(Authentication):验证用户身份
- 授权(Authorization):控制用户访问权限
- 攻击防护:CSRF、XSS、会话固定等
- 密码加密:多种加密算法支持
- 会话管理:会话控制和并发控制
依赖配置
<!-- pom.xml -->
<dependencies>
<!-- Spring Security Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT支持 -->
<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>
</dependencies>
基础配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@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(HttpMethod.GET, "/api/posts/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
5.2 用户认证
用户实体设计
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
@Enumerated(EnumType.STRING)
private UserStatus status = UserStatus.ACTIVE;
@Column(name = "account_non_expired")
private boolean accountNonExpired = true;
@Column(name = "account_non_locked")
private boolean accountNonLocked = true;
@Column(name = "credentials_non_expired")
private boolean credentialsNonExpired = true;
@Column(name = "enabled")
private boolean enabled = true;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
}
@Entity
@Table(name = "roles")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
@Column(unique = true, nullable = false)
private RoleName name;
private String description;
@ManyToMany(mappedBy = "roles")
private Set<User> users = new HashSet<>();
}
public enum RoleName {
ROLE_USER, ROLE_ADMIN, ROLE_MODERATOR
}
public enum UserStatus {
ACTIVE, INACTIVE, SUSPENDED, DELETED
}
UserDetailsService实现
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return UserPrincipal.create(user);
}
@Transactional(readOnly = true)
public UserDetails loadUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UsernameNotFoundException("User not found with id: " + id));
return UserPrincipal.create(user);
}
}
// 自定义UserDetails实现
@Data
@AllArgsConstructor
public class UserPrincipal implements UserDetails {
private Long id;
private String username;
private String email;
private String password;
private Collection<? extends GrantedAuthority> authorities;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
public static UserPrincipal create(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName().name()))
.collect(Collectors.toList());
return new UserPrincipal(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getPassword(),
authorities,
user.isAccountNonExpired(),
user.isAccountNonLocked(),
user.isCredentialsNonExpired(),
user.isEnabled()
);
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
认证控制器
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private PasswordEncoder passwordEncoder;
// 用户注册
@PostMapping("/register")
public ResponseEntity<ApiResponse<UserResponse>> register(@Valid @RequestBody RegisterRequest request) {
// 检查用户名是否已存在
if (userService.existsByUsername(request.getUsername())) {
return ResponseEntity.badRequest()
.body(ApiResponse.error(400, "Username is already taken!"));
}
// 检查邮箱是否已存在
if (userService.existsByEmail(request.getEmail())) {
return ResponseEntity.badRequest()
.body(ApiResponse.error(400, "Email is already in use!"));
}
// 创建新用户
User user = User.builder()
.username(request.getUsername())
.email(request.getEmail())
.password(passwordEncoder.encode(request.getPassword()))
.firstName(request.getFirstName())
.lastName(request.getLastName())
.build();
User savedUser = userService.createUser(user);
UserResponse userResponse = UserResponse.fromUser(savedUser);
return ResponseEntity.ok(ApiResponse.success(userResponse));
}
// 用户登录
@PostMapping("/login")
public ResponseEntity<ApiResponse<JwtResponse>> login(@Valid @RequestBody LoginRequest request) {
try {
// 认证用户
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
// 生成JWT token
String jwt = jwtTokenUtil.generateToken(userPrincipal);
JwtResponse jwtResponse = new JwtResponse(
jwt,
userPrincipal.getId(),
userPrincipal.getUsername(),
userPrincipal.getEmail(),
userPrincipal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList())
);
return ResponseEntity.ok(ApiResponse.success(jwtResponse));
} catch (BadCredentialsException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponse.error(401, "Invalid username or password"));
}
}
// 刷新token
@PostMapping("/refresh")
public ResponseEntity<ApiResponse<JwtResponse>> refreshToken(
@Valid @RequestBody RefreshTokenRequest request) {
String refreshToken = request.getRefreshToken();
if (jwtTokenUtil.isTokenValid(refreshToken)) {
String username = jwtTokenUtil.getUsernameFromToken(refreshToken);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String newToken = jwtTokenUtil.generateToken(userDetails);
JwtResponse jwtResponse = new JwtResponse(
newToken,
((UserPrincipal) userDetails).getId(),
userDetails.getUsername(),
((UserPrincipal) userDetails).getEmail(),
userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList())
);
return ResponseEntity.ok(ApiResponse.success(jwtResponse));
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponse.error(401, "Invalid refresh token"));
}
}
// 登出
@PostMapping("/logout")
public ResponseEntity<ApiResponse<String>> logout(HttpServletRequest request) {
String token = jwtTokenUtil.getTokenFromRequest(request);
if (token != null) {
// 将token加入黑名单(可选实现)
jwtTokenUtil.invalidateToken(token);
}
return ResponseEntity.ok(ApiResponse.success("Logged out successfully"));
}
}
// 请求和响应DTO
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RegisterRequest {
@NotBlank(message = "Username cannot be blank")
@Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters")
private String username;
@NotBlank(message = "Email cannot be blank")
@Email(message = "Email should be valid")
private String email;
@NotBlank(message = "Password cannot be blank")
@Size(min = 6, max = 20, message = "Password must be between 6 and 20 characters")
private String password;
private String firstName;
private String lastName;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginRequest {
@NotBlank(message = "Username cannot be blank")
private String username;
@NotBlank(message = "Password cannot be blank")
private String password;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class JwtResponse {
private String token;
private String type = "Bearer";
private Long id;
private String username;
private String email;
private List<String> roles;
public JwtResponse(String token, Long id, String username, String email, List<String> roles) {
this.token = token;
this.id = id;
this.username = username;
this.email = email;
this.roles = roles;
}
}
5.3 JWT Token管理
JWT工具类
@Component
public class JwtTokenUtil {
private static final Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);
@Value("${app.jwtSecret}")
private String jwtSecret;
@Value("${app.jwtExpirationInMs}")
private int jwtExpirationInMs;
@Autowired
private RedisTemplate<String, String> redisTemplate;
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
return Keys.hmacShaKeyFor(keyBytes);
}
// 生成JWT token
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
if (userDetails instanceof UserPrincipal) {
UserPrincipal userPrincipal = (UserPrincipal) userDetails;
claims.put("id", userPrincipal.getId());
claims.put("email", userPrincipal.getEmail());
}
claims.put("authorities", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(getSigningKey(), SignatureAlgorithm.HS512)
.compact();
}
// 从token获取用户名
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
// 从token获取用户ID
public Long getUserIdFromToken(String token) {
Claims claims = getAllClaimsFromToken(token);
return claims.get("id", Long.class);
}
// 从token获取过期时间
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.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
// 检查token是否过期
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
// 验证token
public Boolean isTokenValid(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername())
&& !isTokenExpired(token)
&& !isTokenBlacklisted(token));
}
public Boolean isTokenValid(String token) {
try {
return !isTokenExpired(token) && !isTokenBlacklisted(token);
} catch (Exception e) {
return false;
}
}
// 从请求中获取token
public String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
// Token黑名单管理
public void invalidateToken(String token) {
try {
Date expiration = getExpirationDateFromToken(token);
long ttl = expiration.getTime() - System.currentTimeMillis();
if (ttl > 0) {
redisTemplate.opsForValue().set(
"blacklist:" + token,
"true",
Duration.ofMilliseconds(ttl)
);
}
} catch (Exception e) {
logger.error("Error invalidating token", e);
}
}
private boolean isTokenBlacklisted(String token) {
try {
return Boolean.TRUE.equals(redisTemplate.hasKey("blacklist:" + token));
} catch (Exception e) {
logger.error("Error checking token blacklist", e);
return false;
}
}
}
JWT过滤器
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(JwtRequestFilter.class);
@Autowired
private CustomUserDetailsService 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;
// JWT Token格式: "Bearer token"
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
logger.error("Unable to get JWT Token", e);
} catch (ExpiredJwtException e) {
logger.error("JWT Token has expired", e);
} catch (Exception e) {
logger.error("JWT Token validation error", e);
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
}
// 验证token并设置认证信息
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.isTokenValid(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
chain.doFilter(request, response);
}
}
JWT异常处理
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
logger.error("Unauthorized error: {}", authException.getMessage());
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ApiResponse<Void> apiResponse = ApiResponse.error(401, "Unauthorized: " + authException.getMessage());
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), apiResponse);
}
}
5.4 权限控制
方法级权限控制
@RestController
@RequestMapping("/api/admin")
@PreAuthorize("hasRole('ADMIN')")
public class AdminController {
@Autowired
private UserService userService;
// 只有ADMIN角色可以访问
@GetMapping("/users")
public ResponseEntity<Page<User>> getAllUsers(Pageable pageable) {
Page<User> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
// 只有ADMIN角色可以删除用户
@DeleteMapping("/users/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
// 只有ADMIN角色可以更改用户状态
@PutMapping("/users/{id}/status")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<User> updateUserStatus(
@PathVariable Long id,
@RequestBody UserStatusUpdateRequest request) {
User user = userService.updateUserStatus(id, request.getStatus());
return ResponseEntity.ok(user);
}
}
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private UserService userService;
// 用户只能访问自己的信息
@GetMapping("/profile")
@PreAuthorize("authentication.name == #username or hasRole('ADMIN')")
public ResponseEntity<User> getUserProfile(@RequestParam String username) {
User user = userService.findByUsername(username);
return ResponseEntity.ok(user);
}
// 用户只能更新自己的信息
@PutMapping("/profile/{id}")
@PreAuthorize("#id == authentication.principal.id or hasRole('ADMIN')")
public ResponseEntity<User> updateProfile(
@PathVariable Long id,
@RequestBody UserUpdateRequest request) {
User user = userService.updateUser(id, request);
return ResponseEntity.ok(user);
}
// 复杂权限表达式
@PostMapping("/posts")
@PreAuthorize("hasRole('USER') and #post.author.id == authentication.principal.id")
public ResponseEntity<Post> createPost(@RequestBody Post post) {
Post savedPost = postService.createPost(post);
return ResponseEntity.ok(savedPost);
}
}
自定义权限评估器
@Component("customPermissionEvaluator")
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Autowired
private UserService userService;
@Autowired
private PostService postService;
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
if (authentication == null || targetDomainObject == null || !(permission instanceof String)) {
return false;
}
String targetType = targetDomainObject.getClass().getSimpleName().toUpperCase();
return hasPrivilege(authentication, targetType, permission.toString(), targetDomainObject);
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
if (authentication == null || targetType == null || !(permission instanceof String)) {
return false;
}
return hasPrivilege(authentication, targetType.toUpperCase(), permission.toString(), targetId);
}
private boolean hasPrivilege(Authentication authentication, String targetType, String permission, Object target) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
// 管理员拥有所有权限
if (hasRole(authentication, "ROLE_ADMIN")) {
return true;
}
switch (targetType) {
case "POST":
return hasPostPermission(userPrincipal, permission, target);
case "USER":
return hasUserPermission(userPrincipal, permission, target);
default:
return false;
}
}
private boolean hasPostPermission(UserPrincipal userPrincipal, String permission, Object target) {
switch (permission) {
case "READ":
return true; // 所有人都可以读取文章
case "write":
case "delete":
if (target instanceof Post) {
Post post = (Post) target;
return post.getAuthor().getId().equals(userPrincipal.getId());
} else if (target instanceof Serializable) {
Post post = postService.findById((Long) target);
return post != null && post.getAuthor().getId().equals(userPrincipal.getId());
}
return false;
default:
return false;
}
}
private boolean hasUserPermission(UserPrincipal userPrincipal, String permission, Object target) {
switch (permission) {
case "read":
return true; // 基本信息所有人都可以读取
case "write":
case "delete":
if (target instanceof User) {
User user = (User) target;
return user.getId().equals(userPrincipal.getId());
} else if (target instanceof Serializable) {
return userPrincipal.getId().equals(target);
}
return false;
default:
return false;
}
}
private boolean hasRole(Authentication authentication, String role) {
return authentication.getAuthorities().stream()
.anyMatch(authority -> authority.getAuthority().equals(role));
}
}
// 使用自定义权限评估器
@RestController
public class PostController {
@GetMapping("/posts/{id}")
@PreAuthorize("hasPermission(#id, 'Post', 'read')")
public ResponseEntity<Post> getPost(@PathVariable Long id) {
Post post = postService.findById(id);
return ResponseEntity.ok(post);
}
@PutMapping("/posts/{id}")
@PreAuthorize("hasPermission(#id, 'Post', 'write')")
public ResponseEntity<Post> updatePost(@PathVariable Long id, @RequestBody Post post) {
Post updatedPost = postService.updatePost(id, post);
return ResponseEntity.ok(updatedPost);
}
@DeleteMapping("/posts/{id}")
@PreAuthorize("hasPermission(#id, 'Post', 'delete')")
public ResponseEntity<Void> deletePost(@PathVariable Long id) {
postService.deletePost(id);
return ResponseEntity.noContent().build();
}
}
动态权限控制
@Service
public class DynamicPermissionService {
@Autowired
private UserPermissionRepository userPermissionRepository;
@Autowired
private RolePermissionRepository rolePermissionRepository;
public boolean hasPermission(Long userId, String resource, String action) {
// 检查用户直接权限
boolean hasUserPermission = userPermissionRepository
.existsByUserIdAndResourceAndAction(userId, resource, action);
if (hasUserPermission) {
return true;
}
// 检查角色权限
List<String> userRoles = getUserRoles(userId);
return rolePermissionRepository
.existsByRoleNameInAndResourceAndAction(userRoles, resource, action);
}
public List<String> getUserPermissions(Long userId) {
List<String> permissions = new ArrayList<>();
// 获取用户直接权限
List<UserPermission> userPermissions = userPermissionRepository.findByUserId(userId);
userPermissions.forEach(permission ->
permissions.add(permission.getResource() + ":" + permission.getAction()));
// 获取角色权限
List<String> userRoles = getUserRoles(userId);
List<RolePermission> rolePermissions = rolePermissionRepository.findByRoleNameIn(userRoles);
rolePermissions.forEach(permission ->
permissions.add(permission.getResource() + ":" + permission.getAction()));
return permissions.stream().distinct().collect(Collectors.toList());
}
private List<String> getUserRoles(Long userId) {
User user = userService.findById(userId);
return user.getRoles().stream()
.map(role -> role.getName().name())
.collect(Collectors.toList());
}
}
// 权限实体
@Entity
@Table(name = "user_permissions")
@Data
public class UserPermission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id")
private Long userId;
private String resource;
private String action;
@CreationTimestamp
private LocalDateTime createdAt;
}
@Entity
@Table(name = "role_permissions")
@Data
public class RolePermission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "role_name")
private String roleName;
private String resource;
private String action;
@CreationTimestamp
private LocalDateTime createdAt;
}
5.5 密码安全
密码加密
@Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 强度12
}
// 自定义密码编码器(支持多种算法)
@Bean
public PasswordEncoder delegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(encodingId, encoders);
passwordEncoder.setDefaultPasswordEncoderForMatches(new BCryptPasswordEncoder());
return passwordEncoder;
}
}
@Service
public class PasswordService {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private PasswordHistoryRepository passwordHistoryRepository;
// 密码强度验证
public boolean isPasswordStrong(String password) {
if (password == null || password.length() < 8) {
return false;
}
boolean hasUpper = password.chars().anyMatch(Character::isUpperCase);
boolean hasLower = password.chars().anyMatch(Character::isLowerCase);
boolean hasDigit = password.chars().anyMatch(Character::isDigit);
boolean hasSpecial = password.chars().anyMatch(ch -> "!@#$%^&*()_+-=[]{}|;:,.<>?".indexOf(ch) >= 0);
return hasUpper && hasLower && hasDigit && hasSpecial;
}
// 检查密码历史
public boolean isPasswordReused(Long userId, String newPassword) {
List<PasswordHistory> history = passwordHistoryRepository
.findTop5ByUserIdOrderByCreatedAtDesc(userId);
return history.stream()
.anyMatch(h -> passwordEncoder.matches(newPassword, h.getPasswordHash()));
}
// 更改密码
@Transactional
public void changePassword(Long userId, String oldPassword, String newPassword) {
User user = userService.findById(userId);
// 验证旧密码
if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
throw new BadCredentialsException("Current password is incorrect");
}
// 验证新密码强度
if (!isPasswordStrong(newPassword)) {
throw new IllegalArgumentException("Password does not meet strength requirements");
}
// 检查密码重用
if (isPasswordReused(userId, newPassword)) {
throw new IllegalArgumentException("Cannot reuse recent passwords");
}
// 保存密码历史
PasswordHistory history = new PasswordHistory();
history.setUserId(userId);
history.setPasswordHash(user.getPassword());
passwordHistoryRepository.save(history);
// 更新密码
user.setPassword(passwordEncoder.encode(newPassword));
userService.updateUser(user);
}
}
@Entity
@Table(name = "password_history")
@Data
public class PasswordHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id")
private Long userId;
@Column(name = "password_hash")
private String passwordHash;
@CreationTimestamp
private LocalDateTime createdAt;
}
密码重置
@Service
public class PasswordResetService {
@Autowired
private UserService userService;
@Autowired
private PasswordResetTokenRepository tokenRepository;
@Autowired
private EmailService emailService;
@Autowired
private PasswordEncoder passwordEncoder;
// 发送密码重置邮件
public void sendPasswordResetEmail(String email) {
User user = userService.findByEmail(email)
.orElseThrow(() -> new ResourceNotFoundException("User not found with email: " + email));
// 生成重置token
String token = UUID.randomUUID().toString();
PasswordResetToken resetToken = new PasswordResetToken();
resetToken.setToken(token);
resetToken.setUserId(user.getId());
resetToken.setExpiryDate(LocalDateTime.now().plusHours(24)); // 24小时有效
tokenRepository.save(resetToken);
// 发送邮件
String resetUrl = "http://localhost:8080/reset-password?token=" + token;
emailService.sendPasswordResetEmail(user.getEmail(), resetUrl);
}
// 验证重置token
public boolean validatePasswordResetToken(String token) {
PasswordResetToken resetToken = tokenRepository.findByToken(token)
.orElse(null);
return resetToken != null &&
!resetToken.isUsed() &&
resetToken.getExpiryDate().isAfter(LocalDateTime.now());
}
// 重置密码
@Transactional
public void resetPassword(String token, String newPassword) {
PasswordResetToken resetToken = tokenRepository.findByToken(token)
.orElseThrow(() -> new IllegalArgumentException("Invalid token"));
if (resetToken.isUsed()) {
throw new IllegalArgumentException("Token has already been used");
}
if (resetToken.getExpiryDate().isBefore(LocalDateTime.now())) {
throw new IllegalArgumentException("Token has expired");
}
// 验证新密码强度
if (!passwordService.isPasswordStrong(newPassword)) {
throw new IllegalArgumentException("Password does not meet strength requirements");
}
// 更新密码
User user = userService.findById(resetToken.getUserId());
user.setPassword(passwordEncoder.encode(newPassword));
userService.updateUser(user);
// 标记token为已使用
resetToken.setUsed(true);
tokenRepository.save(resetToken);
}
}
@Entity
@Table(name = "password_reset_tokens")
@Data
public class PasswordResetToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String token;
@Column(name = "user_id")
private Long userId;
@Column(name = "expiry_date")
private LocalDateTime expiryDate;
@Column(name = "is_used")
private boolean used = false;
@CreationTimestamp
private LocalDateTime createdAt;
}
5.6 会话管理
会话配置
@Configuration
public class SessionConfig {
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
// 会话并发控制
@Bean
public SessionAuthenticationStrategy sessionAuthenticationStrategy() {
ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy =
new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
concurrentSessionControlStrategy.setMaximumSessions(1); // 最大并发会话数
concurrentSessionControlStrategy.setExceptionIfMaximumExceeded(false); // 不抛异常,踢掉旧会话
CompositeSessionAuthenticationStrategy sessionAuthenticationStrategy =
new CompositeSessionAuthenticationStrategy(Arrays.asList(
concurrentSessionControlStrategy,
new SessionFixationProtectionStrategy(),
new RegisterSessionAuthenticationStrategy(sessionRegistry())
));
return sessionAuthenticationStrategy;
}
}
// 在SecurityConfig中配置会话管理
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1)
.maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry())
.and()
.sessionFixation().migrateSession()
.invalidSessionUrl("/login?expired")
);
return http.build();
}
}
会话监听器
@Component
public class SessionEventListener implements ApplicationListener<SessionDestroyedEvent> {
private static final Logger logger = LoggerFactory.getLogger(SessionEventListener.class);
@Autowired
private UserSessionService userSessionService;
@Override
public void onApplicationEvent(SessionDestroyedEvent event) {
List<SecurityContext> securityContexts = event.getSecurityContexts();
for (SecurityContext securityContext : securityContexts) {
Authentication authentication = securityContext.getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof UserPrincipal) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
logger.info("Session destroyed for user: {}", userPrincipal.getUsername());
// 清理用户会话信息
userSessionService.removeUserSession(userPrincipal.getId());
}
}
}
}
@Service
public class UserSessionService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String SESSION_PREFIX = "user:session:";
// 记录用户会话
public void recordUserSession(Long userId, String sessionId) {
String key = SESSION_PREFIX + userId;
UserSession session = new UserSession();
session.setUserId(userId);
session.setSessionId(sessionId);
session.setLoginTime(LocalDateTime.now());
session.setLastAccessTime(LocalDateTime.now());
redisTemplate.opsForValue().set(key, session, Duration.ofHours(24));
}
// 更新最后访问时间
public void updateLastAccessTime(Long userId) {
String key = SESSION_PREFIX + userId;
UserSession session = (UserSession) redisTemplate.opsForValue().get(key);
if (session != null) {
session.setLastAccessTime(LocalDateTime.now());
redisTemplate.opsForValue().set(key, session, Duration.ofHours(24));
}
}
// 移除用户会话
public void removeUserSession(Long userId) {
String key = SESSION_PREFIX + userId;
redisTemplate.delete(key);
}
// 获取在线用户列表
public List<UserSession> getOnlineUsers() {
Set<String> keys = redisTemplate.keys(SESSION_PREFIX + "*");
List<UserSession> onlineUsers = new ArrayList<>();
if (keys != null) {
for (String key : keys) {
UserSession session = (UserSession) redisTemplate.opsForValue().get(key);
if (session != null) {
onlineUsers.add(session);
}
}
}
return onlineUsers;
}
}
@Data
public class UserSession {
private Long userId;
private String sessionId;
private LocalDateTime loginTime;
private LocalDateTime lastAccessTime;
private String ipAddress;
private String userAgent;
}
5.7 安全配置
HTTPS配置
# application.properties
# HTTPS配置
server.port=8443
server.ssl.enabled=true
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=password
server.ssl.key-store-type=PKCS12
server.ssl.key-alias=tomcat
# 强制HTTPS
security.require-ssl=true
@Configuration
public class HttpsConfig {
@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
@Override
protected void postProcessContext(Context context) {
SecurityConstraint securityConstraint = new SecurityConstraint();
securityConstraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
securityConstraint.addCollection(collection);
context.addConstraint(securityConstraint);
}
};
tomcat.addAdditionalTomcatConnectors(redirectConnector());
return tomcat;
}
private Connector redirectConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
connector.setPort(8080);
connector.setSecure(false);
connector.setRedirectPort(8443);
return connector;
}
}
安全头配置
@Configuration
public class SecurityHeadersConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
.frameOptions().deny()
.contentTypeOptions().and()
.xssProtection().and()
.referrerPolicy(ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
.httpStrictTransportSecurity(hstsConfig -> hstsConfig
.maxAgeInSeconds(31536000)
.includeSubdomains(true)
.preload(true)
)
.contentSecurityPolicy("default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'")
);
return http.build();
}
}
审计日志
@Component
public class SecurityAuditListener {
private static final Logger auditLogger = LoggerFactory.getLogger("SECURITY_AUDIT");
@EventListener
public void handleAuthenticationSuccess(AuthenticationSuccessEvent event) {
UserPrincipal userPrincipal = (UserPrincipal) event.getAuthentication().getPrincipal();
auditLogger.info("Authentication successful for user: {}", userPrincipal.getUsername());
}
@EventListener
public void handleAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
auditLogger.warn("Authentication failed: {}", event.getException().getMessage());
}
@EventListener
public void handleAuthorizationFailure(AuthorizationDeniedEvent event) {
auditLogger.warn("Authorization denied for user: {} on resource: {}",
event.getAuthentication().getName(),
event.getAuthorizationDecision());
}
}
总结
本章详细介绍了Spring Boot安全与认证的核心内容:
- Spring Security基础:框架概述、依赖配置、基础配置
- 用户认证:用户实体设计、UserDetailsService、认证控制器
- JWT Token管理:Token生成、验证、过滤器、异常处理
- 权限控制:方法级权限、自定义权限评估器、动态权限
- 密码安全:密码加密、强度验证、密码重置
- 会话管理:会话配置、并发控制、会话监听
- 安全配置:HTTPS配置、安全头、审计日志
这些知识点构成了Spring Boot应用安全体系的完整框架,掌握这些内容可以构建安全可靠的Web应用程序。