Skip to content

Spring 安全框架

1. Spring Security 概述

1.1 什么是 Spring Security

Spring Security 是一个强大且高度可定制的身份验证和访问控制框架,是保护基于 Spring 应用程序的事实标准。

1.2 核心功能

功能说明
认证(Authentication)验证用户身份
授权(Authorization)控制用户访问权限
攻击防护CSRF、Session Fixation、点击劫持等
集成支持OAuth2、JWT、LDAP 等

1.3 核心组件

请求 → SecurityFilterChain → Filter 链 → 认证/授权 → 资源访问

核心组件:

  • SecurityContext:安全上下文,存储认证信息
  • Authentication:认证对象,包含用户信息和权限
  • UserDetails:用户详情接口
  • UserDetailsService:用户详情服务
  • PasswordEncoder:密码编码器

2. 快速入门

2.1 添加依赖

xml
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>6.3.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>6.3.0</version>
</dependency>

2.2 最简配置

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
            )
            .logout(logout -> logout
                .permitAll()
            );
        
        return http.build();
    }
    
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.builder()
            .username("user")
            .password(passwordEncoder().encode("password"))
            .roles("USER")
            .build();
        
        UserDetails admin = User.builder()
            .username("admin")
            .password(passwordEncoder().encode("admin"))
            .roles("ADMIN")
            .build();
        
        return new InMemoryUserDetailsManager(user, admin);
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

2.3 初始化配置

java
public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer {
    // 自动注册 DelegatingFilterProxy
}

3. 认证配置

3.1 自定义 UserDetailsService

java
@Service
public class CustomUserDetailsService implements UserDetailsService {
    
    private final UserRepository userRepository;
    
    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
        
        return new CustomUserDetails(user);
    }
}

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.getName()))
            .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 true;
    }
    
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    
    @Override
    public boolean isEnabled() {
        return user.isEnabled();
    }
    
    public User getUser() {
        return user;
    }
}

3.2 密码编码器

java
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

// 使用示例
String rawPassword = "123456";
String encodedPassword = passwordEncoder.encode(rawPassword);

boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);

3.3 自定义登录

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
                .requestMatchers("/register", "/login").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .loginProcessingUrl("/login")
                .defaultSuccessUrl("/dashboard", true)
                .failureUrl("/login?error=true")
                .usernameParameter("username")
                .passwordParameter("password")
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout=true")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
            )
            .rememberMe(remember -> remember
                .key("uniqueAndSecret")
                .tokenValiditySeconds(86400)
                .userDetailsService(userDetailsService)
            );
        
        return http.build();
    }
}

4. 授权配置

4.1 URL 授权

java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth
        .requestMatchers("/public/**").permitAll()
        .requestMatchers("/admin/**").hasRole("ADMIN")
        .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
        .requestMatchers("/api/**").authenticated()
        .anyRequest().denyAll()
    );
    
    return http.build();
}

4.2 方法授权

java
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
}

@Service
public class UserService {
    
    @PreAuthorize("hasRole('ADMIN')")
    public List<User> findAllUsers() {
        return userRepository.findAll();
    }
    
    @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
    public User findById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
    
    @PreAuthorize("hasAuthority('user:write')")
    public void updateUser(User user) {
        userRepository.save(user);
    }
    
    @PostAuthorize("returnObject.username == authentication.name")
    public User findByUsername(String username) {
        return userRepository.findByUsername(username).orElse(null);
    }
    
    @PostFilter("filterObject.owner == authentication.name")
    public List<Document> findUserDocuments() {
        return documentRepository.findAll();
    }
    
    @PreFilter("filterObject.owner == authentication.name")
    public void saveDocuments(List<Document> documents) {
        documentRepository.saveAll(documents);
    }
}

4.3 权限表达式

表达式说明
hasRole('ADMIN')拥有 ADMIN 角色
hasAnyRole('ADMIN', 'USER')拥有任意一个角色
hasAuthority('user:read')拥有 user:read 权限
hasAnyAuthority('user:read', 'user:write')拥有任意一个权限
permitAll允许所有用户
denyAll拒绝所有用户
isAuthenticated()已认证用户
isAnonymous()匿名用户
isRememberMe()记住我登录
isFullyAuthenticated()完整认证(非记住我)
principal当前用户主体
authentication当前认证对象

5. CSRF 防护

5.1 启用 CSRF

java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
        );
    
    return http.build();
}

5.2 表单中使用 CSRF

html
<form th:action="@{/login}" method="post">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
    <input type="text" name="username"/>
    <input type="password" name="password"/>
    <button type="submit">登录</button>
</form>

5.3 禁用 CSRF

java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.csrf(csrf -> csrf.disable());
    return http.build();
}

6. Session 管理

6.1 Session 配置

java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
            .maximumSessions(1)
            .maxSessionsPreventsLogin(true)
            .sessionRegistry(sessionRegistry())
        );
    
    return http.build();
}

@Bean
public SessionRegistry sessionRegistry() {
    return new SessionRegistryImpl();
}

6.2 Session 固定防护

java
http.sessionManagement(session -> session
    .sessionFixation(fixation -> fixation
        .migrateSession()
    )
);

6.3 无状态 Session

java
http.sessionManagement(session -> session
    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);

7. OAuth2 登录

7.1 添加依赖

xml
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-client</artifactId>
    <version>6.3.0</version>
</dependency>

7.2 配置 OAuth2

yaml
spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: your-github-client-id
            client-secret: your-github-client-secret
          google:
            client-id: your-google-client-id
            client-secret: your-google-client-secret
        provider:
          github:
            authorization-uri: https://github.com/login/oauth/authorize
            token-uri: https://github.com/login/oauth/access_token
            user-info-uri: https://api.github.com/user

7.3 安全配置

java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/", "/login/**", "/oauth2/**").permitAll()
            .anyRequest().authenticated()
        )
        .oauth2Login(oauth2 -> oauth2
            .loginPage("/login")
            .defaultSuccessUrl("/dashboard", true)
            .failureUrl("/login?error=true")
        );
    
    return http.build();
}

7.4 自定义 OAuth2 用户服务

java
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
    
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oauth2User = super.loadUser(userRequest);
        
        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        Map<String, Object> attributes = oauth2User.getAttributes();
        
        OAuth2UserInfo userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(registrationId, attributes);
        
        User user = userRepository.findByEmail(userInfo.getEmail())
            .orElseGet(() -> createOAuth2User(userInfo, registrationId));
        
        return new CustomOAuth2User(oauth2User, user);
    }
    
    private User createOAuth2User(OAuth2UserInfo userInfo, String registrationId) {
        User user = new User();
        user.setEmail(userInfo.getEmail());
        user.setUsername(userInfo.getName());
        user.setProvider(AuthProvider.valueOf(registrationId.toUpperCase()));
        user.setProviderId(userInfo.getId());
        user.setEnabled(true);
        return userRepository.save(user);
    }
}

8. 异常处理

8.1 自定义认证入口点

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);
        
        Map<String, Object> data = new HashMap<>();
        data.put("code", 401);
        data.put("message", "未授权访问");
        data.put("path", request.getRequestURI());
        
        response.getWriter().write(new ObjectMapper().writeValueAsString(data));
    }
}

8.2 自定义访问拒绝处理器

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);
        
        Map<String, Object> data = new HashMap<>();
        data.put("code", 403);
        data.put("message", "权限不足");
        data.put("path", request.getRequestURI());
        
        response.getWriter().write(new ObjectMapper().writeValueAsString(data));
    }
}

8.3 配置异常处理

java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .exceptionHandling(exception -> exception
            .authenticationEntryPoint(authenticationEntryPoint)
            .accessDeniedHandler(accessDeniedHandler)
        );
    
    return http.build();
}

9. CORS 配置

9.1 Spring Security CORS

java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .cors(cors -> cors.configurationSource(corsConfigurationSource()));
    
    return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(List.of("http://localhost:3000"));
    configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    configuration.setAllowedHeaders(List.of("*"));
    configuration.setAllowCredentials(true);
    configuration.setMaxAge(3600L);
    
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    
    return source;
}

10. 安全过滤器

10.1 自定义过滤器

java
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    private final JwtTokenProvider tokenProvider;
    
    public JwtAuthenticationFilter(JwtTokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                   HttpServletResponse response,
                                   FilterChain filterChain) throws ServletException, IOException {
        
        String token = getTokenFromRequest(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 getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

10.2 注册过滤器

java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    
    return http.build();
}

11. 小结

本章学习了 Spring Security 的核心功能:

内容要点
认证UserDetailsService、UserDetails、PasswordEncoder
授权URL 授权、方法授权、权限表达式
CSRFToken 生成、表单集成
SessionSession 管理、Session 固定防护
OAuth2第三方登录、自定义用户服务
异常处理AuthenticationEntryPoint、AccessDeniedHandler
CORS跨域配置
过滤器自定义过滤器、过滤器链

下一章将学习 JWT 认证实战。