Java面试-Spring Security 认证与授权机制详解
《Java面试200问》系列聚焦Spring Security的认证与授权机制,通过五星级酒店门禁系统类比,生动解释其核心价值。文章从基础概念(认证vs授权)切入,提供快速入门示例,详细剖析UserDetailsService、PasswordEncoder等核心组件。内容涵盖多种用户存储方案(内存、数据库、JWT)、方法级安全控制、安全防护(CSRF/CORS)以及OAuth2集成,配合面试题强
👋 欢迎阅读《Java面试200问》系列博客!
🚀大家好,我是Jinkxs,一名热爱Java、深耕技术一线的开发者。在准备和参与了数十场Java面试后,我深知面试不仅是对知识的考察,更是对理解深度与表达能力的综合检验。
✨本系列将带你系统梳理Java核心技术中的高频面试题,从源码原理到实际应用,从常见陷阱到大厂真题,每一篇文章都力求深入浅出、图文并茂,帮助你在求职路上少走弯路,稳拿Offer!
🔍今天我们要聊的是:《Spring Security 认证与授权机制详解》。准备好了吗?Let’s go!
Spring Security 认证与授权机制详解——从“保安大爷”到“智能门禁系统”的进化史
📚 目录结构
- 前言:为什么我们需要 Spring Security?
- Spring Security 是谁?
- 核心概念:认证 vs 授权
- 快速入门:Hello Security
- 用户详情服务:UserDetailsService
- 密码加密:PasswordEncoder
- 基于内存的用户存储
- 基于数据库的用户管理(JPA + MySQL)
- 基于 JWT 的无状态认证
- 方法级安全:@PreAuthorize、@Secured
- CSRF、CORS、Session 管理
- OAuth2 与 OpenID Connect 简介
- 自定义登录页面与登录逻辑
- 异常处理与登出配置
- 面试题环节:你真的懂安全吗?
- 总结:安全不是功能,而是责任
前言:为什么我们需要 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 访问我的微信头像吗?”
四种模式:
- 授权码模式(Authorization Code):最安全,用于 Web 应用
- 简化模式(Implicit):用于单页应用(SPA)
- 密码模式(Resource Owner Password):用户把密码给客户端(不推荐)
- 客户端模式(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)来处理安全逻辑。关键过滤器包括:
WebAsyncManagerIntegrationFilter
:集成 WebAsyncSecurityContextPersistenceFilter
:加载/保存 SecurityContextUsernamePasswordAuthenticationFilter
:处理表单登录FilterSecurityInterceptor
:最终授权决策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:如何自定义登录成功/失败逻辑?
答:
实现 AuthenticationSuccessHandler
和 AuthenticationFailureHandler
:
.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 中的实现》 的关键知识点,记得关注不迷路!
💬 互动时间:你在面试中遇到过类似问题吗?或者对本文内容有疑问?欢迎在评论区留言交流,我会一一回复!
如果你觉得这篇文章对你有帮助,别忘了 点赞 + 收藏 + 转发,让更多小伙伴一起进步!我们下一篇见 👋
更多推荐
所有评论(0)