Skip to content

Spring Security 认证与授权流程

一、Spring Security 概述

核心功能

┌─────────────────────────────────────────────────────────────────┐
│                      Spring Security 核心功能                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Authentication(认证)                                          │
│  └─ 你是谁?验证用户身份                                         │
│     ┌──────────────┐                                            │
│     │  用户名/密码  │                                           │
│     │  JWT Token   │                                           │
│     │  OAuth2      │                                           │
│     │  LDAP        │                                           │
│     └──────────────┘                                            │
│                                                                  │
│  Authorization(授权)                                           │
│  └─ 你能做什么?验证用户权限                                      │
│     ┌──────────────┐                                            │
│     │  URL 权限    │                                            │
│     │  方法权限    │                                            │
│     │  字段权限    │                                            │
│     └──────────────┘                                            │
│                                                                  │
│  CSRF 防护                                                      │
│  └─ 跨站请求伪造防护                                             │
│                                                                  │
│  Session 管理                                                   │
│  └─ 并发控制、会话固定攻击防护                                   │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

二、快速开始

Maven 依赖

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

基本配置

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**", "/login").permitAll()  // 放行
                .anyRequest().authenticated()  // 其他需要认证
            )
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/home")
            )
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout")
            );
        
        return http.build();
    }
}

默认行为

引入 spring-boot-starter-security 后:
1. 所有端点都需要认证
2. 提供一个登录页 /login
3. 提供一个注销端点 /logout
4. 默认生成一个随机 session
5. 启用 CSRF 防护

三、认证流程

完整认证流程

┌─────────────────────────────────────────────────────────────────┐
│                      Spring Security 认证流程                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  用户请求 ──▶ UsernamePasswordAuthenticationFilter              │
│                    │                                            │
│                    ▼                                            │
│            提取用户名密码                                         │
│                    │                                            │
│                    ▼                                            │
│         AuthenticationManager.authenticate()                     │
│                    │                                            │
│                    ▼                                            │
│         ┌─────────────────────────┐                            │
│         │   AuthenticationProvider│                            │
│         │   ┌─────────────────┐ │                            │
│         │   │ UserDetailsService│ │                           │
│         │   └────────┬────────┘ │                            │
│         │            │           │                            │
│         │     查询用户信息        │                            │
│         │            ▼           │                            │
│         │     比较密码(BCrypt)  │                            │
│         │            │           │                            │
│         └────────────┼───────────┘                            │
│                      │                                         │
│                      ▼                                         │
│         认证成功 → Authentication (Principal + Authorities)     │
│         认证失败 → AuthenticationException                      │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

核心接口

java
// 认证信息
public interface Authentication extends Principal {
    Collection<? extends GrantedAuthority> getAuthorities();  // 权限
    Object getCredentials();    // 凭证(密码)
    Object getDetails();        // 详情
    Object getPrincipal();      // 主体(用户)
    boolean isAuthenticated();  // 是否已认证
    void setAuthenticated(boolean isAuthenticated);
}

// 用户信息服务
public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

// 用户详情
public interface UserDetails extends Authentication {
    boolean isAccountNonExpired();      // 账户未过期
    boolean isAccountNonLocked();      // 账户未锁定
    boolean isCredentialsNonExpired(); // 凭证未过期
    boolean isEnabled();               // 账户启用
}

四、认证实现

基于内存的用户

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        
        manager.createUser(User.withUsername("admin")
            .password(passwordEncoder().encode("123456"))
            .roles("ADMIN", "USER")
            .authorities("ROLE_ADMIN", "READ", "WRITE")
            .build());
        
        manager.createUser(User.withUsername("user")
            .password(passwordEncoder().encode("123456"))
            .roles("USER")
            .authorities("ROLE_USER", "READ")
            .build());
        
        return manager;
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

基于数据库的认证

java
@Service
public class JdbcUserDetailsService implements UserDetailsService {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String sql = "SELECT username, password, enabled FROM users WHERE username = ?";
        
        List<UserDetails> users = jdbcTemplate.query(sql, 
            (rs, rowNum) -> User.builder()
                .username(rs.getString("username"))
                .password(rs.getString("password"))
                .disabled(!rs.getBoolean("enabled"))
                .build(),
            username);
        
        if (users.isEmpty()) {
            throw new UsernameNotFoundException("用户不存在: " + username);
        }
        
        // 查询权限
        String authSql = "SELECT role FROM user_roles WHERE username = ?";
        List<String> roles = jdbcTemplate.queryForList(authSql, String.class, username);
        
        UserDetails user = users.get(0);
        return User.builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .authorities(roles.toArray(new String[0]))
            .build();
    }
}

PasswordEncoder

java
@Configuration
public class PasswordEncoderConfig {
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 1. BCrypt(推荐)
        return new BCryptPasswordEncoder();
        
        // 2. Argon2(更好的安全性)
        return Argon2PasswordEncoder.getInstance();
        
        // 3. SCrypt
        return SCryptPasswordEncoder.getInstance();
        
        // 4. MD5(不推荐,已不安全)
        return new MessageDigestPasswordEncoder("MD5");
        
        // 5. 自定义
        return new CustomPasswordEncoder("sha256");
    }
}

五、授权配置

URL 授权

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                // 按顺序匹配,先匹配的生效
                .requestMatchers("/public/**", "/login", "/error").permitAll()  // 放行
                .requestMatchers("/admin/**").hasRole("ADMIN")  // 需要 ADMIN 角色
                .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")  // USER 或 ADMIN
                .requestMatchers("/api/**").hasAuthority("READ")  // 需要 READ 权限
                .anyRequest().authenticated()  // 其他需要认证
            );
        
        return http.build();
    }
}

方法级授权

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

// 使用注解
@Service
public class UserService {
    
    @PreAuthorize("hasRole('ADMIN')")  // 方法执行前检查
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }
    
    @PreAuthorize("hasAuthority('WRITE') or #user.id == authentication.principal.id")
    public void updateUser(User user) {
        // #user.id 引用参数
        // authentication.principal 当前用户
    }
    
    @PostAuthorize("returnObject.owner == authentication.principal.username")  // 方法执行后检查
    public Document getDocument(Long id) {
        return documentRepository.findById(id).orElseThrow();
    }
    
    @Secured({"ROLE_ADMIN", "ROLE_MANAGER"})  // 旧风格
    public void adminOnly() { }
    
    @RolesAllowed({"ADMIN", "MANAGER"})  // JSR-250
    public void managerOrAdmin() { }
}

Thymeleaf 权限控制

html
<div sec:authorize="isAuthenticated()">
    <p>已登录用户: <span sec:authentication="name">用户名</span></p>
</div>

<div sec:authorize="hasRole('ADMIN')">
    <a href="/admin">管理后台</a>
</div>

<button sec:authorize="hasAuthority('WRITE')">发布</button>

六、登录配置

表单登录

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .formLogin(form -> form
                .loginPage("/login")           // 自定义登录页
                .loginProcessingUrl("/doLogin") // 登录处理 URL
                .usernameParameter("username")   // 用户名参数名
                .passwordParameter("password") // 密码参数名
                .defaultSuccessUrl("/home", true) // 登录成功默认页
                .successHandler((request, response, authentication) -> {
                    // 自定义成功处理
                    response.setContentType("application/json");
                    response.getWriter().write("{\"code\":0,\"message\":\"登录成功\"}");
                })
                .failureHandler((request, response, exception) -> {
                    // 自定义失败处理
                    response.setContentType("application/json");
                    response.getWriter().write("{\"code\":401,\"message\":\"登录失败\"}");
                })
                .permitAll()  // 登录页无需认证
            );
        
        return http.build();
    }
}

JSON 登录

java
@Component
public class JsonLoginFilter extends UsernamePasswordAuthenticationFilter {
    
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, 
                                              HttpServletResponse response) 
            throws AuthenticationException {
        try {
            Map<String, String> map = new ObjectMapper().readValue(
                request.getInputStream(), Map.class);
            
            String username = map.get("username");
            String password = map.get("password");
            
            UsernamePasswordAuthenticationToken authRequest = 
                new UsernamePasswordAuthenticationToken(username, password);
            
            setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
            
        } catch (IOException e) {
            throw new AuthenticationServiceException("解析登录请求失败", e);
        }
    }
}

// 注册 Filter
@Configuration
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(jsonLoginFilter(), UsernamePasswordAuthenticationFilter.class)
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
        
        return http.build();
    }
    
    @Bean
    public JsonLoginFilter jsonLoginFilter() {
        JsonLoginFilter filter = new JsonLoginFilter();
        filter.setAuthenticationManager(authenticationManager());
        return filter;
    }
}

七、注销配置

java
@Configuration
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .logout(logout -> logout
                .logoutUrl("/logout")           // 注销 URL
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "POST"))
                .logoutSuccessUrl("/login?logout")  // 注销成功页
                .logoutSuccessHandler((request, response, authentication) -> {
                    // 自定义成功处理
                    response.sendRedirect("/login?logout");
                })
                .addLogoutHandler((request, response, authentication) -> {
                    // 自定义清理
                    request.getSession().invalidate();
                })
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .deleteCookies("JSESSIONID")  // 清除 Cookie
            );
        
        return http.build();
    }
}

八、Session 管理

Session 配置

java
@Configuration
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .sessionManagement(session -> session
                // 无session则不创建(不生成 JSESSIONID)
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                
                // 会话 fixation 攻击防护
                // migrateSession: 匿名 session → 认证 session
                // newSession: 重新创建 session
                .sessionFixation().migrateSession()
                
                // 并发控制:同一用户最多几个 session
                .maximumSessions(1)
                .maxSessionsPreventsLogin(true)  // 达到上限阻止登录
                
                // 过期策略
                .expiredUrl("/login?expired")
            );
        
        return http.build();
    }
}

Session 并发控制

java
// 监听 session 过期
@Component
public class SessionExpirationListener implements ApplicationListener<SessionDestroyedEvent> {
    
    @Override
    public void onApplicationEvent(SessionDestroyedEvent event) {
        String sessionId = event.getId();
        // 清理相关资源
    }
}

九、CSRF 防护

CSRF 原理

正常流程:
┌────────┐                    ┌────────┐
│  用户   │ ──▶ 转账请求 ──▶ │  银行   │
│         │ ◀─── 转账成功 ─── │         │
└────────┘                    └────────┘

CSRF 攻击:
┌────────┐                    ┌────────┐
│  攻击者 │ ──▶ 伪造请求 ──▶ │  银行   │
│  网站   │                   │         │
└────────┘                    └────────┘
用户浏览器带着 cookie 发起请求,服务器无法区分是否是用户主动

启用/禁用 CSRF

java
@Configuration
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 禁用 CSRF(API 场景常用)
            .csrf(csrf -> csrf.disable())
            
            // 启用 CSRF(表单场景)
            .csrf(csrf -> csrf.enable())
            
            // 自定义 CSRF Token 存储
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            );
        
        return http.build();
    }
}

CSRF Token 使用

html
<!-- 方式1:表单隐藏字段 -->
<form method="post" action="/transfer">
    <input type="hidden" name="_csrf" value="${_csrf.token}" />
    <!-- 其他字段 -->
</form>

<!-- 方式2:请求头 -->
<!-- Spring Security 自动添加 X-CSRF-TOKEN 到请求头 -->

十、CORS 配置

java
@Configuration
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()));
        
        return http.build();
    }
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
        config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(Arrays.asList("*"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", config);
        return source;
    }
}

十一、面试高频问题

Q1: Spring Security 执行流程?

  1. 请求到 SecurityFilterChain
  2. UsernamePasswordAuthenticationFilter 提取用户名密码
  3. AuthenticationManager 调用 AuthenticationProvider
  4. UserDetailsService 加载用户
  5. PasswordEncoder 比较密码
  6. 认证成功返回 Authentication
  7. SecurityContext 保存认证信息

Q2: hasRole 和 hasAuthority 区别?

  • hasRole('ADMIN'):自动添加 ROLE_ 前缀,变成 hasAuthority('ROLE_ADMIN')
  • hasAuthority('READ'):直接匹配权限

Q3: @PreAuthorize 和 @Secured 区别?

  • @PreAuthorize:SpEL 表达式,灵活
  • @Secured:简单角色列表,不支持表达式

Q4: SessionCreationPolicy.IF_REQUIRED?

  • ALWAYS:总是创建 Session
  • NEVER:不创建,已有则使用
  • IF_REQUIRED:必要时创建(默认)
  • STATELESS:无状态,不创建 Session

十二、下一章预告

下一章我们将学习 Spring Cloud 微服务

  • 微服务概述与 Nacos 注册中心
  • Feign 远程调用
  • Gateway 网关
  • Sentinel 熔断限流

基于 MIT 许可发布