Skip to content

Spring Security 集成

本文档介绍如何在 LCGYL Framework 中集成 Spring Security,实现安全认证和授权。

快速开始

添加依赖

gradle
dependencies {
    implementation 'com.lcgyl:lcgyl-spring-security:${version}'
    implementation 'org.springframework.boot:spring-boot-starter-security'
}
xml
<dependencies>
    <dependency>
        <groupId>com.lcgyl</groupId>
        <artifactId>lcgyl-spring-security</artifactId>
        <version>${version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>

基本配置

yaml
spring:
  security:
    user:
      name: admin
      password: admin123

lcgyl:
  security:
    enabled: true
    jwt:
      enabled: true
      secret: your-256-bit-secret-key-here
      expiration: 3600

安全配置

配置类

java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
    
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthenticationFilter, 
                UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

CORS 配置

java
@Configuration
public class CorsConfig {
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

用户认证

UserDetailsService

java
@Service
public class CustomUserDetailsService implements UserDetailsService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
        
        return org.springframework.security.core.userdetails.User.builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .roles(user.getRoles().toArray(new String[0]))
            .build();
    }
}

自定义 UserDetails

java
public class CustomUserDetails implements UserDetails {
    
    private final User user;
    
    public CustomUserDetails(User user) {
        this.user = user;
    }
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return user.getRoles().stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
            .collect(Collectors.toList());
    }
    
    @Override
    public String getPassword() {
        return user.getPassword();
    }
    
    @Override
    public String getUsername() {
        return user.getUsername();
    }
    
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    
    @Override
    public boolean isAccountNonLocked() {
        return !user.isLocked();
    }
    
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    
    @Override
    public boolean isEnabled() {
        return user.isEnabled();
    }
    
    public User getUser() {
        return user;
    }
}

JWT 认证

JWT 工具类

java
@Component
public class JwtTokenProvider {
    
    @Value("${lcgyl.security.jwt.secret}")
    private String secret;
    
    @Value("${lcgyl.security.jwt.expiration}")
    private long expiration;
    
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("roles", userDetails.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.toList()));
        
        return Jwts.builder()
            .setClaims(claims)
            .setSubject(userDetails.getUsername())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
    }
    
    public String getUsernameFromToken(String token) {
        return Jwts.parser()
            .setSigningKey(secret)
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
    }
    
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
    
    public boolean isTokenExpired(String token) {
        Date expiration = Jwts.parser()
            .setSigningKey(secret)
            .parseClaimsJws(token)
            .getBody()
            .getExpiration();
        return expiration.before(new Date());
    }
}

JWT 过滤器

java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtTokenProvider tokenProvider;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
            HttpServletResponse response, FilterChain filterChain) 
            throws ServletException, IOException {
        
        String token = extractToken(request);
        
        if (token != null && tokenProvider.validateToken(token)) {
            String username = tokenProvider.getUsernameFromToken(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            
            UsernamePasswordAuthenticationToken authentication = 
                new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(
                new WebAuthenticationDetailsSource().buildDetails(request));
            
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String extractToken(HttpServletRequest request) {
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            return header.substring(7);
        }
        return null;
    }
}

认证控制器

java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private JwtTokenProvider tokenProvider;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @PostMapping("/login")
    public ResponseEntity<AuthResponse> login(@RequestBody LoginRequest request) {
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                request.getUsername(), 
                request.getPassword()
            )
        );
        
        SecurityContextHolder.getContext().setAuthentication(authentication);
        
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        String token = tokenProvider.generateToken(userDetails);
        
        return ResponseEntity.ok(new AuthResponse(token));
    }
    
    @PostMapping("/register")
    public ResponseEntity<User> register(@RequestBody RegisterRequest request) {
        // 注册逻辑
    }
    
    @PostMapping("/refresh")
    public ResponseEntity<AuthResponse> refresh(@RequestHeader("Authorization") String token) {
        // 刷新令牌逻辑
    }
}

方法级安全

启用方法安全

java
@Configuration
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig {
}

使用注解

java
@Service
public class UserService {
    
    // 需要 ADMIN 角色
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }
    
    // 需要特定权限
    @PreAuthorize("hasAuthority('user:write')")
    public User createUser(User user) {
        return userRepository.save(user);
    }
    
    // 只能访问自己的数据
    @PreAuthorize("#id == authentication.principal.id or hasRole('ADMIN')")
    public User getUser(Long id) {
        return userRepository.findById(id).orElse(null);
    }
    
    // 后置检查
    @PostAuthorize("returnObject.userId == authentication.principal.id")
    public Order getOrder(Long orderId) {
        return orderRepository.findById(orderId).orElse(null);
    }
    
    // 过滤集合
    @PostFilter("filterObject.userId == authentication.principal.id")
    public List<Order> getOrders() {
        return orderRepository.findAll();
    }
    
    // 使用 @Secured
    @Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
    public void adminOperation() {
    }
}

自定义权限评估器

java
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
    
    @Autowired
    private PermissionService permissionService;
    
    @Override
    public boolean hasPermission(Authentication authentication, 
            Object targetDomainObject, Object permission) {
        if (authentication == null || targetDomainObject == null) {
            return false;
        }
        
        String username = authentication.getName();
        String permissionStr = permission.toString();
        
        return permissionService.hasPermission(username, 
            targetDomainObject.getClass().getSimpleName(), permissionStr);
    }
    
    @Override
    public boolean hasPermission(Authentication authentication, 
            Serializable targetId, String targetType, Object permission) {
        if (authentication == null) {
            return false;
        }
        
        String username = authentication.getName();
        return permissionService.hasPermission(username, targetType, 
            targetId.toString(), permission.toString());
    }
}

// 使用
@PreAuthorize("hasPermission(#order, 'write')")
public void updateOrder(Order order) {
}

LCGYL 安全集成

集成 LCGYL 权限注解

java
@Aspect
@Component
public class LcgylSecurityAspect {
    
    @Before("@annotation(requiresPermission)")
    public void checkPermission(JoinPoint joinPoint, RequiresPermission requiresPermission) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        
        if (authentication == null || !authentication.isAuthenticated()) {
            throw new AccessDeniedException("未认证");
        }
        
        String permission = requiresPermission.value();
        boolean hasPermission = authentication.getAuthorities().stream()
            .anyMatch(a -> a.getAuthority().equals(permission));
        
        if (!hasPermission) {
            throw new AccessDeniedException("权限不足: " + permission);
        }
    }
    
    @Before("@annotation(requiresRole)")
    public void checkRole(JoinPoint joinPoint, RequiresRole requiresRole) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        
        if (authentication == null || !authentication.isAuthenticated()) {
            throw new AccessDeniedException("未认证");
        }
        
        String role = "ROLE_" + requiresRole.value();
        boolean hasRole = authentication.getAuthorities().stream()
            .anyMatch(a -> a.getAuthority().equals(role));
        
        if (!hasRole) {
            throw new AccessDeniedException("需要角色: " + requiresRole.value());
        }
    }
}

安全上下文桥接

java
@Component
public class SecurityContextBridge {
    
    // 将 Spring Security 上下文同步到 LCGYL
    @EventListener
    public void onAuthentication(AuthenticationSuccessEvent event) {
        Authentication auth = event.getAuthentication();
        if (auth.getPrincipal() instanceof CustomUserDetails userDetails) {
            com.lcgyl.framework.security.SecurityContext.setCurrentUser(
                userDetails.getUser()
            );
        }
    }
    
    // 获取当前用户
    public User getCurrentUser() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null && auth.getPrincipal() instanceof CustomUserDetails userDetails) {
            return userDetails.getUser();
        }
        return null;
    }
}

OAuth2 集成

OAuth2 配置

yaml
spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: your-client-id
            client-secret: your-client-secret
          google:
            client-id: your-client-id
            client-secret: your-client-secret

OAuth2 安全配置

java
@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/login/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard")
                .userInfoEndpoint(userInfo -> userInfo
                    .userService(customOAuth2UserService())
                )
            );
        
        return http.build();
    }
    
    @Bean
    public OAuth2UserService<OAuth2UserRequest, OAuth2User> customOAuth2UserService() {
        return new CustomOAuth2UserService();
    }
}

自定义 OAuth2 用户服务

java
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oauth2User = super.loadUser(userRequest);
        
        String provider = userRequest.getClientRegistration().getRegistrationId();
        String providerId = oauth2User.getAttribute("id").toString();
        String email = oauth2User.getAttribute("email");
        String name = oauth2User.getAttribute("name");
        
        // 查找或创建用户
        User user = userRepository.findByProviderAndProviderId(provider, providerId)
            .orElseGet(() -> {
                User newUser = new User();
                newUser.setProvider(provider);
                newUser.setProviderId(providerId);
                newUser.setEmail(email);
                newUser.setName(name);
                return userRepository.save(newUser);
            });
        
        return new CustomOAuth2User(oauth2User, user);
    }
}

异常处理

认证异常处理

java
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException {
        
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write(
            "{\"code\":401,\"message\":\"未认证: " + authException.getMessage() + "\"}"
        );
    }
}

授权异常处理

java
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException accessDeniedException) throws IOException {
        
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.getWriter().write(
            "{\"code\":403,\"message\":\"权限不足: " + accessDeniedException.getMessage() + "\"}"
        );
    }
}

最佳实践

1. 密码安全

java
// ✅ 推荐:使用 BCrypt
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12);
}

// ❌ 不推荐:使用弱加密
@Bean
public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
}

2. JWT 安全

java
// ✅ 推荐:使用足够长的密钥
lcgyl.security.jwt.secret=your-256-bit-secret-key-must-be-long-enough

// ✅ 推荐:设置合理的过期时间
lcgyl.security.jwt.expiration=3600  // 1小时

3. 最小权限原则

java
// ✅ 推荐:精细权限控制
@PreAuthorize("hasAuthority('order:delete')")
public void deleteOrder(Long id) { }

// ❌ 不推荐:粗粒度权限
@PreAuthorize("hasRole('USER')")
public void deleteOrder(Long id) { }

常见问题

Q: 如何处理跨域认证?

A: 配置 CORS 并在响应中包含认证头。

Q: JWT 过期后如何处理?

A: 使用 Refresh Token 机制刷新 Access Token。

Q: 如何实现记住我功能?

A: 使用 Spring Security 的 Remember-Me 功能或延长 JWT 有效期。

下一步

Released under the Apache License 2.0