首页 / 资讯中心 / 文章详情

30. 技术专题-‌应用框架组件-安全网关-Spring Security

30. 技术专题-‌应用框架组件-安全网关-Spring Security ★ FEATURED ARTICLE
文章目录前言一、先搞明白为什么系统需要一套门禁系统1.1 认证与授权门卫管的两件大事1.2 为什么不用自己写拦截器二、核心世界观一切皆过滤器2.1 过滤器链小区门口的一排检查岗2.2 三个核心对象身份、认证器、安全上下文2.3 快速上手三行代码配置一套门禁三、认证机制让门卫认出你3.1 认证的核心流程3.2 UserDetailsService门卫的花名册3.3 PasswordEncoder密码不能明文存四、实战一基于数据库的用户认证4.1 建表与实体4.2 完整的安全配置4.3 注册接口写入加密密码五、授权门卫放行还要看你的通行证5.1 角色与权限RBAC 模型5.2 方法级安全PreAuthorize5.3 常用 SpEL 表达式速查六、实战二无状态 JWT 认证6.1 JWT 长什么样6.2 生成与校验 JWT6.3 用过滤器翻译 JWT 为登录身份七、安全防护进阶别让攻击者钻空子7.1 CSRF防借刀杀人7.2 CORS跨域不是你想开就开7.3 登录防爆破别让人无限试密码7.4 安全防护小结八、原理总结与高频面试题8.1 面试高频问题速览8.2 学到这里下一步看什么前言每个开发者都见识过自家小区的门禁 —— 刷卡进大门、指纹进单元楼、人脸识别进电梯你要是没登记连大门口都进不去。这套门禁系统看着简单背后却藏着整个安保体系门卫验身份、系统查权限、摄像头留证据。Java 世界里也有一位门禁总管——Spring Security它负责决定谁能进系统、进来之后能干什么、干坏事怎么拦。这套门禁系统到底是怎么认人、怎么放行、又是怎么把坏人拦在门外的接下来让我们一起看看它的实现原理及如何使用最后再总结一下它的高频面试题。一、先搞明白为什么系统需要一套门禁系统1.1 认证与授权门卫管的两件大事在谈 Spring Security 之前先分清两个天天被混淆的概念——认证Authentication和授权Authorization。回到小区场景保安让你刷卡是验证你是不是小区的人——这叫认证进了大门后能不能进 3 栋、能不能用健身房门禁按你的权限放行——这叫授权。概念英文回答的问题小区类比认证Authentication你是谁门卫验身份刷卡确认你是业主授权Authorization你能干什么门禁权限你能不能进某栋楼凭证Credential拿什么证明你是谁门禁卡、指纹角色Role你的身份层级业主、租户、物业管理员Note认证一定发生在授权之前。你没刷开门谈能不能进健身房没有意义。但认证通过不代表授权通行——你是业主也进不了别人的家。1.2 为什么不用自己写拦截器你可能会想不就是登录后放行没登录就跳转吗我自己写个拦截器不就行了可以但你还会发现密码不能明文存要用 BCrypt、登录接口不能被无限爆破、请求要防 CSRF、Session 要被管理、权限要细到这个按钮只有管理员能看、还要记安全日志……从零写这套门禁系统会越做越像一个小型框架。Spring Security 的定位就是把认证和授权这件脏活累活标准化、插件化。它基于 Servlet 过滤器前身是 2003 年的 Acegi Security如今已是 Spring 生态的安全事实标准。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency记忆点只需这一个依赖全站接口立刻默认全部要求登录并弹出一个默认登录页——门禁系统一秒装好。二、核心世界观一切皆过滤器理解了定位正式进入 Spring Security 的世界。它和很多框架不一样核心不是某个类而是一条过滤器链Filter Chain。2.1 过滤器链小区门口的一排检查岗想象小区门口不是只有一名保安而是一字排开的五个检查岗每个岗查一件事任何 HTTP 请求都会先经过这排检查岗过滤器全部通过才到达你的 Controller。任何一个岗不通过请求直接打回根本到不了业务层。关键认知Spring Security 本质上就是往 Servlet 的过滤器链里插入了一串过滤器。这条链也叫SecurityFilterChain它的顺序是固定的其中几个关键岗位过滤器职责类比UsernamePasswordAuthenticationFilter处理表单登录请求提取用户名密码门卫查证件BasicAuthenticationFilter处理 HTTP Basic 认证门卫查访客单AuthorizationFilter判断当前用户是否有权访问楼栋门禁检查CsrfFilter校验 CSRF Token防陌生人冒充业主进门AnonymousAuthenticationFilter给未登录用户一个匿名身份给临时访客发临时牌ExceptionTranslationFilter捕获认证/授权异常做统一处理安保中心协调处理Warning过滤器顺序至关重要。认证过滤器在前、授权过滤器在后——保安必须先验明身份才谈得上放你去哪栋楼。2.2 三个核心对象身份、认证器、安全上下文要读懂这条链先认识三个主角① Authentication身份的票据代表一个认证请求或已认证身份包含三样主体principal通常是用户、凭证credentials通常是密码、权限列表authorities。② AuthenticationManager认证总管所有认证请求都交给它。它自己不干活把任务分派给一个个AuthenticationProvider。③ SecurityContextHolder安全上下文认证通过后Authentication对象被放进这里像一张贴在胸口的访客证后续代码包括 Service 层随时能读出来。认证通过后在业务代码里这样取当前用户GetMapping(/me) public Object me() { Authentication authentication SecurityContextHolder.getContext().getAuthentication(); String username authentication.getName(); return 当前登录用户 username; }2.3 快速上手三行代码配置一套门禁有了认知基础我们先跑一个最小可用的配置。Spring Security 5.7 推荐用SecurityFilterChainBean 的方式旧的WebSecurityConfigurerAdapter已被废弃别再用它import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http // 1. 授权规则这些路径放行其余全部要求登录 .authorizeHttpRequests(auth - auth .requestMatchers(/, /public/**, /login).permitAll() .anyRequest().authenticated() ) // 2. 开启表单登录 .formLogin(Customizer.withDefaults()) // 3. 开启 HTTP Basic 认证 .httpBasic(Customizer.withDefaults()) // 4. 登出放行 .logout(logout - logout.permitAll()); return http.build(); } }这段配置翻译成人话就是配置小区类比permitAll()大门、大厅这些公共区域谁都能进authenticated()其他区域必须刷卡登录后才能进.formLogin()没登录就弹出一个登录页.logout()允许退卡离场现在访问/hello你会发现它被重定向到默认登录页输入用户user和启动时控制台打印的随机密码就能进去——这就是最基础的门禁已经生效了。三、认证机制让门卫认出你上一章的配置用的是 Spring Security 的默认账号。真实系统当然不能这样这一章我们拆开认证的内核看看认人的完整流程。3.1 认证的核心流程以最常见的用户名密码表单登录为例一次认证请求走完是这样的注意几个关键细节①认证成功后Spring Security 会返回标记为已认证的 token实际新建对象并把其中的凭证清空后存入 SecurityContext②AuthenticationManager只管分派不亲自干活。它手下可能有多个AuthenticationProvider用户名密码、OAuth2、手机号验证码等按类型找到合适的那个。③ 三个可插拔接口。大部分时候你只需要实现/替换这三个接口职责你要做的UserDetailsService根据用户名查用户信息实现它去数据库查人UserDetails用户信息载体一般直接用框架默认实现PasswordEncoder密码加密/比对用BCryptPasswordEncoder3.2 UserDetailsService门卫的花名册UserDetailsService只有一个方法loadUserByUsername(String username)。它就像门卫手里的花名册——按名字查人查到返回这个人的完整档案密码、角色、权限查不到抛UsernameNotFoundException。import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; Service public class UserDetailsServiceImpl implements UserDetailsService { private final UserMapper userMapper; public UserDetailsServiceImpl(UserMapper userMapper) { this.userMapper userMapper; } Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 1. 去数据库查用户你项目里的 Mapper/Repository SysUser sysUser userMapper.selectByUsername(username); if (sysUser null) { throw new UsernameNotFoundException(用户不存在 username); } // 2. 包装成 Spring Security 认识的 UserDetails return User.builder() .username(sysUser.getUsername()) .password(sysUser.getPassword()) // 必须是加密后的密码 .roles(sysUser.getRole()) // 如 ADMIN / USER .disabled(sysUser.getStatus() 0) // 账号是否禁用 .build(); } }3.3 PasswordEncoder密码不能明文存数据库里绝不能存明文密码否则数据库一泄露全体用户账号裸奔。Spring Security 的PasswordEncoder负责两件事加密和比对。import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; Configuration public class PasswordConfig { Bean public PasswordEncoder passwordEncoder() { // BCrypt 自动加盐同一个密码每次加密结果都不同 return new BCryptPasswordEncoder(); } }注册用户时加密存储登录时让框架自动比对Service public class RegisterService { private final PasswordEncoder passwordEncoder; private final UserMapper userMapper; public RegisterService(PasswordEncoder passwordEncoder, UserMapper userMapper) { this.passwordEncoder passwordEncoder; this.userMapper userMapper; } public void register(String username, String rawPassword, String role) { SysUser user new SysUser(); user.setUsername(username); // 只存加密后的密文 user.setPassword(passwordEncoder.encode(rawPassword)); user.setRole(role); userMapper.insert(user); } }NoteBCryptPasswordEncoder每次加密生成不同的密文内部有随机盐但matches(明文, 密文)总能正确比对。如果把两个相同明文密码加密后比较equals结果是false——这是正常现象别怀疑自己写错了。PasswordEncoder家族有多个成员选型时要看清实现特点适用场景BCryptPasswordEncoder加盐、抗暴力破解、社区公认首选生产环境默认推荐Argon2PasswordEncoder更抗 GPU 暴力破解高安全敏感场景DelegatingPasswordEncoder密码前带{bcrypt}前缀支持多算法并存老项目升级过渡NoOpPasswordEncoder不加密明文存储仅测试环境禁止用于生产四、实战一基于数据库的用户认证理论讲了不少这一章我们把花名册 门禁完整落地写一个真实的认证链路。4.1 建表与实体假设一张最简用户表CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, -- BCrypt 密文 role VARCHAR(20) NOT NULL, -- ADMIN / USER status TINYINT NOT NULL DEFAULT 1 -- 1 正常0 禁用 );对应的实体类public class SysUser { private Long id; private String username; private String password; private String role; private Integer status; // getter / setter 省略 }4.2 完整的安全配置把前面学的串起来写一个能跑通的配置类Configuration EnableWebSecurity public class SecurityConfig { private final UserDetailsService userDetailsService; private final PasswordEncoder passwordEncoder; public SecurityConfig(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) { this.userDetailsService userDetailsService; this.passwordEncoder passwordEncoder; } Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http // 禁用 CSRF下面第七章会讲什么时候该禁 .csrf(csrf - csrf.disable()) // 授权规则 .authorizeHttpRequests(auth - auth .requestMatchers(/, /public/**, /register).permitAll() .requestMatchers(/admin/**).hasRole(ADMIN) .anyRequest().authenticated() ) // 表单登录配置登录页、成功/失败处理器 .formLogin(form - form .loginPage(/login) .loginProcessingUrl(/login) .successHandler((request, response, authentication) - { response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:0,\msg\:\登录成功\}); }) .failureHandler((request, response, exception) - { response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:1,\msg\:\用户名或密码错误\}); }) .permitAll() ) // 认证用户信息从哪里来 .userDetailsService(userDetailsService); return http.build(); } }关键点解释•loginPage(/login)指定自定义登录页一个返回登录页 HTML 的 Controller。默认登录页是框架自动生成的丑但能用。•successHandler/failureHandler登录成功/失败的自定义处理。前后端分离项目里这是返回 JSON的关键别再让框架跳转页面了。•.userDetailsService(...)告诉认证器查花名册用我这个实现。Tip登录失败默认响应是 302 跳转到/login?error前端很难优雅处理。前后端分离项目建议用successHandler/failureHandler直接返回 JSON让前端根据code字段做交互。4.3 注册接口写入加密密码注册接口本身要放行/register在 permitAll 里入库前加密RestController public class AuthController { private final RegisterService registerService; public AuthController(RegisterService registerService) { this.registerService registerService; } PostMapping(/register) public MapString, Object register(RequestBody RegisterRequest request) { registerService.register(request.getUsername(), request.getPassword(), USER); return Map.of(code, 0, msg, 注册成功); } }测试完这条链路你会发现访问/hello会被拦下来要求登录用数据库里注册的账号能登录成功而/admin/**只有ADMIN角色能进——门禁系统正式上岗了。五、授权门卫放行还要看你的通行证认证解决了你是谁这一章解决你能干什么。授权是安全系统里最容易做糊的部分先建立一个正确的模型。5.1 角色与权限RBAC 模型最经典的权限模型是RBACRole-Based Access Control基于角色的访问控制核心思想权限不直接挂给用户而是挂给角色用户再关联角色。改权限不用逐个改用户改角色就行——就像物业管理员天然拥有开单元门的权限不用给每个管理员单独授权。Spring Security 里角色的完整写法是ROLE_ADMIN前缀ROLE_加上角色名但用hasRole(ADMIN)时不需要写前缀框架自动补Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth - auth .requestMatchers(/admin/**).hasRole(ADMIN) // 需要 ADMIN 角色 .requestMatchers(/user/**).hasAnyRole(ADMIN, USER) // ADMIN 或 USER 都行 .requestMatchers(/public/**).permitAll() // 完全放行 .anyRequest().authenticated() // 其余登录即可 ); return http.build(); }5.2 方法级安全PreAuthorizeURL 级别控制适合整页权限但业务里经常要控制到这个操作只有管理员能做。这时用方法级安全更灵活。先开启方法级安全import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; Configuration EnableMethodSecurity public class MethodSecurityConfig { }然后在方法上加注解RestController public class UserController { // 登录用户都能访问自己信息 GetMapping(/user/info) public Object info() { return 用户信息; } // 只有管理员能删除用户 DeleteMapping(/user/{id}) PreAuthorize(hasRole(ADMIN)) public Object delete(PathVariable Long id) { return 删除用户 id 成功; } // 判断是不是操作自己——SpEL 表达式大显身手 PutMapping(/user/{username}) PreAuthorize(#username authentication.name) public Object updateSelf(PathVariable String username) { return 修改用户 username; } }NoteEnableMethodSecurity开启后PreAuthorize、PostAuthorize、Secured、RolesAllowed都能用。PreAuthorize在方法执行前校验PostAuthorize在方法执行后校验常用于返回的数据是否属于当前用户这类场景。5.3 常用 SpEL 表达式速查方法级安全注解里用的是 Spring 表达式语言SpEL把这些常用表达式背下来日常开发够用表达式含义hasRole(ADMIN)拥有 ADMIN 角色hasAnyRole(ADMIN,USER)拥有任一角色hasAuthority(user:delete)拥有某个具体权限hasAnyAuthority(a,b)拥有任一权限isAuthenticated()已登录非匿名isAnonymous()未登录#id authentication.principal.id参数 id 等于当前用户 id防越权beanName.method()调用自定义 Bean 方法做复杂判断面试小贴士越权防护是面试高频考点。比如用户 A 能否修改用户 B 的资料这类横向越权问题用#username authentication.name这类表达式在方法入口就把权限卡死。六、实战二无状态 JWT 认证前面用的都是 Session 方案登录后服务端记住你给你一个 SessionId 存 Cookie。但在前后端分离、微服务、多端登录下Session 有痛点服务端要存状态、集群要共享 Session、移动端用 Cookie 不方便。于是诞生了 JWT 方案认证信息加密进一个 Token服务端不存任何状态。6.1 JWT 长什么样JWTJSON Web Token由三部分用.拼接Header头部 Payload载荷 Signature签名。eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJBRE1JTiJ9.5g9jQpRq... ├── Header {alg:HS256,typ:JWT} 算法声明 ├── Payload {sub:admin,role:ADMIN,exp:...} 用户信息过期时间 └── Signature HMACSHA256(base64(Header).base64(Payload), 密钥) 防篡改签名Payload 里的内容任何人都能解出来只是 Base64 编码不是加密但改不了——因为一旦改动签名就对不上服务端会直接拒绝。所以 JWT 里绝不能放密码等敏感信息。6.2 生成与校验 JWT用流行的jjwt库演示引入io.jsonwebtoken:jjwt-api及相关实现依赖import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.util.Date; Component public class JwtUtil { // 生产环境放配置文件/密钥管理服务密钥至少 32 字节 private final SecretKey key Keys.hmacShaKeyFor( my-secret-key-must-be-at-least-32-bytes-long!.getBytes(StandardCharsets.UTF_8)); private final long expire 1000L * 60 * 60 * 2; // 2 小时 // 登录成功后签发 Token public String generateToken(String username, String role) { Date now new Date(); return Jwts.builder() .subject(username) .claim(role, role) .issuedAt(now) .expiration(new Date(now.getTime() expire)) .signWith(key) .compact(); } // 解析 Token返回用户名Token 无效或过期会抛异常 public String parseUsername(String token) { return Jwts.parser() .verifyWith(key) .build() .parseSignedClaims(token) .getPayload() .getSubject(); } }6.3 用过滤器翻译 JWT 为登录身份JWT 方案下每次请求在Authorization请求头带Bearer token。需要一个过滤器把 Token 解析成Authentication放进 SecurityContext——相当于门卫扫一眼通行证认了放你进去import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final UserDetailsService userDetailsService; public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) { this.jwtUtil jwtUtil; this.userDetailsService userDetailsService; } Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String header request.getHeader(Authorization); // 1. 请求头里有没有 Bearer Token if (header ! null header.startsWith(Bearer )) { String token header.substring(7); try { // 2. 解析出用户名 String username jwtUtil.parseUsername(token); // 3. 当前线程还没登录身份时加载用户并放进去 if (username ! null SecurityContextHolder.getContext().getAuthentication() null) { UserDetails userDetails userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { // Token 无效或过期不设置身份走匿名/未授权逻辑 } } // 4. 无论认没认出来都要放行到后续过滤器 filterChain.doFilter(request, response); } }最后把这个过滤器插到认证过滤器之前Bean public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception { http .csrf(csrf - csrf.disable()) // 无状态 JWT 不用 Session天然免疫 CSRF .sessionManagement(session - session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 关键不创建 Session .authorizeHttpRequests(auth - auth .requestMatchers(/auth/login, /public/**).permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); }完整的 JWT 登录时序Warning设置STATELESS后每次请求都要重新验证 Token服务端不再保留任何 Session 状态——这是无状态的含义。代价是无法主动踢人下线Token 要等自然过期。需要强制下线如改密码、封号时要么缩短过期时间要么引入 Token 黑名单。七、安全防护进阶别让攻击者钻空子认证和授权搭好了但门禁系统还有一个重要职责——防坏人。这一章讲几个 Spring Security 内置的防护面试常问生产必配。7.1 CSRF防借刀杀人CSRFCross-Site Request Forgery跨站请求伪造的攻击套路你登录了银行网站Session 还活着这时打开一个恶意网站里面藏了张图片src指向银行转账接口。浏览器会带着你的 Cookie 自动发起请求——银行以为是你在转账。防护原理在表单里埋一个只有服务端知道的随机 TokenCSRF Token请求带不上就拒绝。攻击者的恶意请求从别的域名发起拿不到 Token自然无法伪造。Spring Security 默认开启 CSRF 防护。但注意两个场景•前后端分离 无状态 JWT不用 Cookie 和 Session攻击者无从借你的凭证可以安全地关闭 CSRF上文配置里我们就是这么做的。•传统服务端渲染 Session 登录必须开启。表单里要带上 Token用 Thymeleaf 可自动渲染。7.2 CORS跨域不是你想开就开前后端分离时前端localhost:8080要请求后端localhost:9000这就涉及跨域。但跨域策略如果配置得过宽比如允许所有来源等于把门禁大门拆了。正确做法是白名单化Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config new CorsConfiguration(); // 只允许这几个前端来源 config.setAllowedOrigins(List.of(http://localhost:8080, https://myapp.example.com)); config.setAllowedMethods(List.of(GET, POST, PUT, DELETE, OPTIONS)); config.setAllowedHeaders(List.of(*)); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return source; }WarningallowCredentials(true)允许携带 Cookie和setAllowedOrigins(*)允许所有来源不能同时用——这是安全底线。允许携带凭证还放开所有来源等于把每个用户的门禁卡都复制了一份贴在大门口。7.3 登录防爆破别让人无限试密码门卫不可能让陌生人无限次试密码。WarningSpring Security 的默认表单登录没有内置限流需要自己结合第三方实现如 Bucket4j 或 Redis 计数器。一个朴素的思路用 Redis 记录用户名IP的错误次数超过阈值就锁定一段时间。// 伪代码思路 public boolean tryLogin(String username, String ip) { String key login:fail: username : ip; Long count redisTemplate.opsForValue().increment(key); if (count 1) { redisTemplate.expire(key, Duration.ofMinutes(5)); // 5 分钟内累计 } if (count 5) { throw new LockedException(尝试次数过多请 5 分钟后再试); } return true; }配合登录失败处理器连续失败 5 次就锁定账号 5 分钟——从根上把暴力破解的通道堵死。7.4 安全防护小结攻击手段危害Spring Security 对策CSRF伪造用户请求转账/改资料CSRF Token默认开启暴力破解无限试密码登录限流 账号锁定Session 固定攻击者诱导用户用他的 Session登录成功后重新生成 Session明文密码泄露数据库泄露全员裸奔BCrypt 加盐加密存储NoteSession 固定攻击的防护是 Spring Security 默认行为——登录成功后自动失效旧 Session 并创建新 Session无需额外配置但面试要知道这回事。八、原理总结与高频面试题学到这里Spring Security 的主干已经清晰了。最后用一张图把全篇串起来一句话总结Spring Security 通过一条过滤器链完成认证你是谁和授权你能干什么认证结果放入 SecurityContextHolder 供业务代码读取并通过 PasswordEncoder 保护密码、CSRF/CORS 等机制抵御常见攻击。8.1 面试高频问题速览Q1Spring Security 的认证流程是怎样的答客户端提交凭证 →AuthenticationManager分派给对应AuthenticationProvider→DaoAuthenticationProvider调UserDetailsService.loadUserByUsername()加载用户 →PasswordEncoder.matches()比对密码 → 认证成功后生成已认证的Authentication存入SecurityContextHolder。Q2认证和授权的区别答认证Authentication确认你是谁回答身份问题授权Authorization决定你能干什么回答权限问题。认证在前授权在后。Q3为什么密码要用 BCrypt 加密答BCrypt 自动加盐随机盐同样的密码每次加密结果不同无法直接比对密文破解且加密过程有意设计得很慢大幅提高暴力破解成本。对比 MD5/SHA 这类哈希它们速度快、无盐非常容易被彩虹表或暴力破解。Q4Session 方案和 JWT 方案怎么选答Session 方案有状态服务端可控性强能主动踢人、实时注销适合单体、服务端渲染JWT 无状态天然适合前后端分离、微服务、多端但无法主动失效安全性依赖密钥保护与合理过期时间。Q5越权水平越权/垂直越权如何防护答垂直越权普通用户访问管理员功能用 URL 授权hasRole(ADMIN) 方法级PreAuthorize分层控制水平越权用户 A 操作用户 B 的资源要用#id authentication.principal.id这类表达式在方法入口校验操作对象是否属于当前用户。8.2 学到这里下一步看什么Spring Security 的世界不止于此。学完这篇基础还可以继续探索OAuth2 与 OIDC 授权登录第三方登录、Authorization Server、多因素认证MFA/短信验证码、与 OAuth2 资源服务器整合等。安全无小事愿你写的每一行代码都像那道门禁一样——认人、守门、防坏人。[相关文章] 多系统认证授权利器OAuth 2究竟该如何使用本文的引用仅限自我学习如有侵权请联系作者删除。参考知识系统安全守护利器 · Spring Security 应用实战及高频面试题精讲
阅读完成 · 觉得有帮助?
咨询建站