请添加图片描述

👋 欢迎阅读《Java面试200问》系列博客!

🚀大家好,我是Jinkxs,一名热爱Java、深耕技术一线的开发者。在准备和参与了数十场Java面试后,我深知面试不仅是对知识的考察,更是对理解深度与表达能力的综合检验。

✨本系列将带你系统梳理Java核心技术中的高频面试题,从源码原理到实际应用,从常见陷阱到大厂真题,每一篇文章都力求深入浅出、图文并茂,帮助你在求职路上少走弯路,稳拿Offer!

🔍今天我们要聊的是:《Spring Security 认证与授权机制详解》。准备好了吗?Let’s go!


Spring Security 认证与授权机制详解——从“保安大爷”到“智能门禁系统”的进化史


📚 目录结构

  1. 前言:为什么我们需要 Spring Security?
  2. Spring Security 是谁?
  3. 核心概念:认证 vs 授权
  4. 快速入门:Hello Security
  5. 用户详情服务:UserDetailsService
  6. 密码加密:PasswordEncoder
  7. 基于内存的用户存储
  8. 基于数据库的用户管理(JPA + MySQL)
  9. 基于 JWT 的无状态认证
  10. 方法级安全:@PreAuthorize、@Secured
  11. CSRF、CORS、Session 管理
  12. OAuth2 与 OpenID Connect 简介
  13. 自定义登录页面与登录逻辑
  14. 异常处理与登出配置
  15. 面试题环节:你真的懂安全吗?
  16. 总结:安全不是功能,而是责任

前言:为什么我们需要 Spring Security?

想象一下,你是一家五星级酒店的经理。

  • 没有安全系统:任何人都能自由进出总统套房,厨师可以偷偷修改客人的账单,清洁工能查看所有客人的隐私信息……
  • 有了安全系统:每个人必须刷卡进入,权限不同,能去的地方也不同。前台只能查入住信息,财务能看账单,但不能进客房。

在软件世界里,Spring Security 就是那个24小时值班、记忆力超群、从不打瞌睡的“智能门禁系统”。

它不只负责“你是谁”(认证),还管“你能干什么”(授权)。

没有它?你的应用可能就是个“开放的夜市”,谁都能进来“逛逛”。


Spring Security 是谁?

Spring Security 是 Spring 家族中负责**身份认证(Authentication)访问控制(Authorization)**的安全框架。它最初叫 Acegi Security,后来被 Spring 收编,成为 Spring 生态的“御用保安”。

它能:

  • 防止未授权访问
  • 提供登录、登出功能
  • 支持多种认证方式(表单、JWT、OAuth2)
  • 细粒度的权限控制
  • 防御常见攻击(CSRF、XSS、Session 固定)

一句话
“它让你的应用从‘随便进’变成‘凭票入场,对号入座’。”


核心概念:认证 vs 授权

这两个词经常被混用,但它们是兄弟,不是双胞胎。

概念 英文 通俗解释 例子
认证 Authentication “你是谁?” 你刷脸进公司,系统确认你是张三
授权 Authorization “你能干什么?” 张三只能访问研发部系统,不能进财务系统

🖼️ 流程图:一次完整的安全访问

用户请求 → [认证] → 是张三? → 是 → [授权] → 能访问订单页? → 是 → 返回数据
                                    ↓                        ↓
                                   否                       否
                                    ↓                        ↓
                               401 Unauthorized         403 Forbidden
  • 401:你没登录,先认证!
  • 403:你登录了,但没权限!

快速入门:Hello Security

1. 创建 Spring Boot 项目

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

2. 写个简单的 Controller

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello, Secure World!";
    }

    @GetMapping("/admin")
    public String admin() {
        return "Admin Page - Top Secret!";
    }
}

3. 启动应用

你会发现:

  • 访问 /hello → 自动跳转到 /login
  • 控制台输出:Using generated security password: 8a2d1c...
  • 默认用户名:user
  • 密码是控制台输出的那串随机字符

这就是 Spring Security 的“开箱即用”——默认保护所有接口


用户详情服务:UserDetailsService

Spring Security 不关心你的用户存在哪,它只关心:“给我一个用户信息”。

这个“信息提供者”就是 UserDetailsService

自定义 UserDetailsService

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found: " + username);
        }
        
        return org.springframework.security.core.userdetails.User
                .withUsername(user.getUsername())
                .password(user.getPassword())
                .authorities(user.getRoles().split(",")) // 角色转 Authorities
                .accountExpired(false)
                .accountLocked(false)
                .credentialsExpired(false)
                .disabled(false)
                .build();
    }
}

注意UserDetails 是 Spring Security 的用户接口,你需要把你的实体类包装成它。


密码加密:PasswordEncoder

明文存密码?那是“自爆卡车”。

Spring Security 要求密码必须加密。推荐使用 BCryptPasswordEncoder

配置 PasswordEncoder

@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("zhangsan")
                .password(passwordEncoder().encode("123456"))
                .roles("USER")
                .build());
        manager.createUser(User.withUsername("lisi")
                .password(passwordEncoder().encode("123456"))
                .roles("ADMIN")
                .build());
        return manager;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/hello").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(withDefaults())
            .logout(withDefaults());
        return http.build();
    }
}

BCrypt 特点:每次加密结果都不同,但能正确验证。


基于内存的用户存储

适合测试,不适合生产。

@Bean
public UserDetailsService userDetailsService() {
    UserDetails user = User.withUsername("user")
            .password(passwordEncoder().encode("password"))
            .roles("USER")
            .build();

    UserDetails admin = User.withUsername("admin")
            .password(passwordEncoder().encode("admin"))
            .roles("ADMIN", "USER")
            .build();

    return new InMemoryUserDetailsManager(user, admin);
}

基于数据库的用户管理(JPA + MySQL)

1. 用户实体

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String username;

    private String password;

    private String roles; // 如 "USER,ADMIN"

    // getter/setter
}

2. Repository

public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username);
}

3. 自定义 UserDetailsService(见上文)

4. 注册用户时加密密码

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    public User register(String username, String password) {
        User user = new User();
        user.setUsername(username);
        user.setPassword(passwordEncoder.encode(password)); // 关键!
        user.setRoles("USER");
        return userRepository.save(user);
    }
}

基于 JWT 的无状态认证

传统 Session 有状态,不适合微服务。JWT(JSON Web Token)是无状态认证的王者。

JWT 结构

Header.Payload.Signature
  • Header:算法、类型
  • Payload:用户信息(如 userId, roles)
  • Signature:防篡改签名

实现步骤

1. 添加依赖
<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>
2. JWT 工具类
@Component
public class JwtUtil {

    private String SECRET_KEY = "your-256-bit-secret";

    public String generateToken(String username, Collection<? extends GrantedAuthority> authorities) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("roles", authorities.stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList()));
        
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10小时
                .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
                .compact();
    }

    public Boolean validateToken(String token, String username) {
        final String extractedUsername = extractUsername(token);
        return (extractedUsername.equals(username) && !isTokenExpired(token));
    }

    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
    }

    private Boolean isTokenExpired(String token) {
        return extractAllClaims(token).getExpiration().before(new Date());
    }
}
3. 自定义过滤器
@Component
@RequiredArgsConstructor
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private MyUserDetailsService userDetailsService;

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        final String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            username = jwtUtil.extractUsername(jwt);
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            if (jwtUtil.validateToken(jwt, userDetails.getUsername())) {
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = 
                    new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        chain.doFilter(request, response);
    }
}
4. 配置 Security
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/authenticate").permitAll()
                .anyRequest().authenticated()
            )
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 无状态
        
        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
5. 登录接口生成 Token
@RestController
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private MyUserDetailsService userDetailsService;

    @Autowired
    private JwtUtil jwtUtil;

    @PostMapping("/authenticate")
    public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) 
            throws Exception {

        try {
            authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), 
                                                      authenticationRequest.getPassword())
            );
        } catch (BadCredentialsException e) {
            throw new Exception("Incorrect username or password", e);
        }

        final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
        final String jwt = jwtUtil.generateToken(userDetails.getUsername(), userDetails.getAuthorities());

        return ResponseEntity.ok(new AuthenticationResponse(jwt));
    }
}

方法级安全:@PreAuthorize、@Secured

有时候 URL 级别控制不够细,比如:

  • 只有订单拥有者才能查看订单
  • 管理员才能删除用户

这时就要用到方法级安全

启用方法安全

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig {
    // ...
}

@PreAuthorize 使用

@Service
public class OrderService {

    @PreAuthorize("#orderId == authentication.principal.id")
    public Order getOrder(Long orderId) {
        // 只有当 orderId 等于当前登录用户 ID 时才能访问
        return orderRepository.findById(orderId);
    }

    @PreAuthorize("hasRole('ADMIN')")
    public void deleteUser(Long userId) {
        userRepository.deleteById(userId);
    }

    @PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
}

@Secured 使用

@Secured("ROLE_ADMIN")
public void adminOnlyMethod() {
    // 只有 ADMIN 角色能调用
}

区别

  • @Secured:只支持角色,不支持 SpEL
  • @PreAuthorize:支持 SpEL 表达式,更强大

CSRF、CORS、Session 管理

CSRF(跨站请求伪造)

Spring Security 默认开启 CSRF 防护,主要用于 Cookie-Session 认证。

http.csrf().disable(); // 如果是 JWT,建议关闭

CORS(跨域)

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOriginPatterns(Arrays.asList("*"));
    configuration.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE"));
    configuration.setAllowedHeaders(Arrays.asList("*"));
    configuration.setAllowCredentials(true);
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

// 在 configure 中
http.cors().configurationSource(corsConfigurationSource());

Session 管理

.sessionManagement()
    .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // 默认
    .invalidSessionUrl("/login?expired")
    .maximumSessions(1)
    .maxSessionsPreventsLogin(true); // 一个用户只能一个会话

OAuth2 与 OpenID Connect 简介

OAuth2 是什么?

不是认证协议,而是授权框架。它解决的是:“我允许 GitHub 访问我的微信头像吗?”

四种模式:

  1. 授权码模式(Authorization Code):最安全,用于 Web 应用
  2. 简化模式(Implicit):用于单页应用(SPA)
  3. 密码模式(Resource Owner Password):用户把密码给客户端(不推荐)
  4. 客户端模式(Client Credentials):服务间调用

OpenID Connect(OIDC)

在 OAuth2 上加了一层认证。ID Token 告诉你“你是谁”。

Spring Security 支持与 Google、GitHub、微信等 OAuth2 提供商集成。


自定义登录页面与登录逻辑

1. 自定义登录页

<!-- login.html -->
<form th:action="@{/login}" method="post">
    <input type="text" name="username" placeholder="Username"/>
    <input type="password" name="password" placeholder="Password"/>
    <button type="submit">Login</button>
</form>

2. 配置登录页

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authz -> authz
            .requestMatchers("/login", "/css/**").permitAll()
            .anyRequest().authenticated()
        )
        .formLogin(form -> form
            .loginPage("/login")
            .loginProcessingUrl("/perform_login")
            .defaultSuccessUrl("/hello", true)
            .failureUrl("/login?error=true")
        )
        .logout(logout -> logout
            .logoutUrl("/logout")
            .logoutSuccessUrl("/login")
        );
    return http.build();
}

异常处理与登出配置

自定义异常处理

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.getWriter().write("Access Denied: " + accessDeniedException.getMessage());
    }
}

// 在 configure 中
http.exceptionHandling()
    .accessDeniedHandler(new CustomAccessDeniedHandler())
    .authenticationEntryPoint(new CustomAuthenticationEntryPoint());

登出配置

.logout(logout -> logout
    .logoutUrl("/logout")
    .logoutSuccessUrl("/login")
    .invalidateHttpSession(true)
    .deleteCookies("JSESSIONID")
    .clearAuthentication(true)
)

面试题环节:你真的懂安全吗?

面试官:听说你用过 Spring Security?


❓ Q1:Spring Security 的核心过滤器链是什么?

Spring Security 通过一个过滤器链(Filter Chain)来处理安全逻辑。关键过滤器包括:

  1. WebAsyncManagerIntegrationFilter:集成 WebAsync
  2. SecurityContextPersistenceFilter:加载/保存 SecurityContext
  3. UsernamePasswordAuthenticationFilter:处理表单登录
  4. FilterSecurityInterceptor:最终授权决策
  5. ExceptionTranslationFilter:处理认证/授权异常

金句
“它像一串安检门,每个门检查不同的东西。”


❓ Q2:Authentication 和 Authorization 的区别?

  • Authentication:验证用户身份(你是谁?)。如用户名密码登录、刷脸。
  • Authorization:决定用户能访问哪些资源(你能干什么?)。如角色、权限。

比喻
认证是“刷工卡进门”,授权是“你只能进研发部,不能进财务室”。


❓ Q3:JWT 和 Session 的区别?

对比项 Session JWT
存储 服务端(内存/Redis) 客户端(Header/Cookie)
状态 有状态 无状态
扩展性 需要 Session 共享 易扩展,适合微服务
注销 服务端删除 Session 需额外机制(如黑名单)
安全性 防 CSRF 防 XSS,注意存储位置

❓ Q4:如何实现“记住我”功能?

Spring Security 支持“Remember-Me”:

http.rememberMe()
    .tokenValiditySeconds(86400) // 24小时
    .key("uniqueAndSecret"); // 密钥

会生成一个加密的 remember-me Cookie,下次访问自动登录。


❓ Q5:CSRF 是什么?如何防御?

  • CSRF:攻击者诱导用户在已登录状态下访问恶意网站,执行非本意的操作(如转账)。
  • 防御:Spring Security 在表单中插入 _csrf 隐藏字段,服务端验证该 token。

注意:JWT 通常不需要 CSRF 防护,因为不依赖 Cookie。


❓ Q6:@PreAuthorize 和 @Secured 的区别?

特性 @PreAuthorize @Secured
SpEL 支持
角色检查 hasRole('ADMIN') @Secured("ROLE_ADMIN")
权限检查 hasAuthority('read')
灵活性 高(支持复杂表达式)

❓ Q7:如何自定义登录成功/失败逻辑?

实现 AuthenticationSuccessHandlerAuthenticationFailureHandler

.successHandler(new AuthenticationSuccessHandler() {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, 
                                       HttpServletResponse response, 
                                       Authentication authentication) {
        // 自定义逻辑,如记录日志、跳转不同页面
        response.sendRedirect("/dashboard");
    }
})

❓ Q8:Spring Security 如何与 Spring Boot Actuator 集成?

Actuator 的端点也需要保护。可以这样配置:

.authorizeHttpRequests(authz -> authz
    .requestMatchers("/actuator/health").permitAll()
    .requestMatchers("/actuator/**").hasRole("ADMIN")
    // ...
)

总结:安全不是功能,而是责任

Spring Security 不是一个“用了就安全”的黑盒。

它是一套严谨的哲学:最小权限原则、纵深防御、持续验证。

从“保安大爷”到“智能门禁系统”,Spring Security 让我们能构建出真正可信的应用。

最后提醒
“安全没有银弹,但 Spring Security 是你最可靠的战友。”


🎯 总结一下:

本文深入探讨了《Spring Security 认证与授权机制详解》,从原理到实践,解析了面试中常见的考察点和易错陷阱。掌握这些内容,不仅能应对面试官的连环追问,更能提升你在实际开发中的技术判断力。

🔗 下期预告:我们将继续深入Java面试核心,带你解锁《OAuth2.0 在 Spring Security 中的实现》 的关键知识点,记得关注不迷路!

💬 互动时间:你在面试中遇到过类似问题吗?或者对本文内容有疑问?欢迎在评论区留言交流,我会一一回复!

如果你觉得这篇文章对你有帮助,别忘了 点赞 + 收藏 + 转发,让更多小伙伴一起进步!我们下一篇见 👋

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐