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 执行流程?
- 请求到 SecurityFilterChain
- UsernamePasswordAuthenticationFilter 提取用户名密码
- AuthenticationManager 调用 AuthenticationProvider
- UserDetailsService 加载用户
- PasswordEncoder 比较密码
- 认证成功返回 Authentication
- 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 熔断限流