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安全与认证的核心内容:

  1. Spring Security基础:框架概述、依赖配置、基础配置
  2. 用户认证:用户实体设计、UserDetailsService、认证控制器
  3. JWT Token管理:Token生成、验证、过滤器、异常处理
  4. 权限控制:方法级权限、自定义权限评估器、动态权限
  5. 密码安全:密码加密、强度验证、密码重置
  6. 会话管理:会话配置、并发控制、会话监听
  7. 安全配置:HTTPS配置、安全头、审计日志

这些知识点构成了Spring Boot应用安全体系的完整框架,掌握这些内容可以构建安全可靠的Web应用程序。