Spring 安全框架
1. Spring Security 概述
1.1 什么是 Spring Security
Spring Security 是一个强大且高度可定制的身份验证和访问控制框架,是保护基于 Spring 应用程序的事实标准。
1.2 核心功能
| 功能 | 说明 |
|---|---|
| 认证(Authentication) | 验证用户身份 |
| 授权(Authorization) | 控制用户访问权限 |
| 攻击防护 | CSRF、Session Fixation、点击劫持等 |
| 集成支持 | OAuth2、JWT、LDAP 等 |
1.3 核心组件
请求 → SecurityFilterChain → Filter 链 → 认证/授权 → 资源访问核心组件:
- SecurityContext:安全上下文,存储认证信息
- Authentication:认证对象,包含用户信息和权限
- UserDetails:用户详情接口
- UserDetailsService:用户详情服务
- PasswordEncoder:密码编码器
2. 快速入门
2.1 添加依赖
xml
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>6.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>6.3.0</version>
</dependency>2.2 最简配置
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.permitAll()
);
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder().encode("password"))
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder().encode("admin"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}2.3 初始化配置
java
public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer {
// 自动注册 DelegatingFilterProxy
}3. 认证配置
3.1 自定义 UserDetailsService
java
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
return new CustomUserDetails(user);
}
}
public class CustomUserDetails implements UserDetails {
private final User user;
public CustomUserDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return user.isEnabled();
}
public User getUser() {
return user;
}
}3.2 密码编码器
java
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 使用示例
String rawPassword = "123456";
String encodedPassword = passwordEncoder.encode(rawPassword);
boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);3.3 自定义登录
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
.requestMatchers("/register", "/login").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/dashboard", true)
.failureUrl("/login?error=true")
.usernameParameter("username")
.passwordParameter("password")
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
)
.rememberMe(remember -> remember
.key("uniqueAndSecret")
.tokenValiditySeconds(86400)
.userDetailsService(userDetailsService)
);
return http.build();
}
}4. 授权配置
4.1 URL 授权
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/api/**").authenticated()
.anyRequest().denyAll()
);
return http.build();
}4.2 方法授权
java
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
}
@Service
public class UserService {
@PreAuthorize("hasRole('ADMIN')")
public List<User> findAllUsers() {
return userRepository.findAll();
}
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
@PreAuthorize("hasAuthority('user:write')")
public void updateUser(User user) {
userRepository.save(user);
}
@PostAuthorize("returnObject.username == authentication.name")
public User findByUsername(String username) {
return userRepository.findByUsername(username).orElse(null);
}
@PostFilter("filterObject.owner == authentication.name")
public List<Document> findUserDocuments() {
return documentRepository.findAll();
}
@PreFilter("filterObject.owner == authentication.name")
public void saveDocuments(List<Document> documents) {
documentRepository.saveAll(documents);
}
}4.3 权限表达式
| 表达式 | 说明 |
|---|---|
| hasRole('ADMIN') | 拥有 ADMIN 角色 |
| hasAnyRole('ADMIN', 'USER') | 拥有任意一个角色 |
| hasAuthority('user:read') | 拥有 user:read 权限 |
| hasAnyAuthority('user:read', 'user:write') | 拥有任意一个权限 |
| permitAll | 允许所有用户 |
| denyAll | 拒绝所有用户 |
| isAuthenticated() | 已认证用户 |
| isAnonymous() | 匿名用户 |
| isRememberMe() | 记住我登录 |
| isFullyAuthenticated() | 完整认证(非记住我) |
| principal | 当前用户主体 |
| authentication | 当前认证对象 |
5. CSRF 防护
5.1 启用 CSRF
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
return http.build();
}5.2 表单中使用 CSRF
html
<form th:action="@{/login}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<input type="text" name="username"/>
<input type="password" name="password"/>
<button type="submit">登录</button>
</form>5.3 禁用 CSRF
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable());
return http.build();
}6. Session 管理
6.1 Session 配置
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.sessionRegistry(sessionRegistry())
);
return http.build();
}
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}6.2 Session 固定防护
java
http.sessionManagement(session -> session
.sessionFixation(fixation -> fixation
.migrateSession()
)
);6.3 无状态 Session
java
http.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);7. OAuth2 登录
7.1 添加依赖
xml
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
<version>6.3.0</version>
</dependency>7.2 配置 OAuth2
yaml
spring:
security:
oauth2:
client:
registration:
github:
client-id: your-github-client-id
client-secret: your-github-client-secret
google:
client-id: your-google-client-id
client-secret: your-google-client-secret
provider:
github:
authorization-uri: https://github.com/login/oauth/authorize
token-uri: https://github.com/login/oauth/access_token
user-info-uri: https://api.github.com/user7.3 安全配置
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login/**", "/oauth2/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/dashboard", true)
.failureUrl("/login?error=true")
);
return http.build();
}7.4 自定义 OAuth2 用户服务
java
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = super.loadUser(userRequest);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
Map<String, Object> attributes = oauth2User.getAttributes();
OAuth2UserInfo userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(registrationId, attributes);
User user = userRepository.findByEmail(userInfo.getEmail())
.orElseGet(() -> createOAuth2User(userInfo, registrationId));
return new CustomOAuth2User(oauth2User, user);
}
private User createOAuth2User(OAuth2UserInfo userInfo, String registrationId) {
User user = new User();
user.setEmail(userInfo.getEmail());
user.setUsername(userInfo.getName());
user.setProvider(AuthProvider.valueOf(registrationId.toUpperCase()));
user.setProviderId(userInfo.getId());
user.setEnabled(true);
return userRepository.save(user);
}
}8. 异常处理
8.1 自定义认证入口点
java
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
Map<String, Object> data = new HashMap<>();
data.put("code", 401);
data.put("message", "未授权访问");
data.put("path", request.getRequestURI());
response.getWriter().write(new ObjectMapper().writeValueAsString(data));
}
}8.2 自定义访问拒绝处理器
java
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
Map<String, Object> data = new HashMap<>();
data.put("code", 403);
data.put("message", "权限不足");
data.put("path", request.getRequestURI());
response.getWriter().write(new ObjectMapper().writeValueAsString(data));
}
}8.3 配置异常处理
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.exceptionHandling(exception -> exception
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
);
return http.build();
}9. CORS 配置
9.1 Spring Security CORS
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()));
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:3000"));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}10. 安全过滤器
10.1 自定义过滤器
java
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
public JwtAuthenticationFilter(JwtTokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = getTokenFromRequest(request);
if (token != null && tokenProvider.validateToken(token)) {
String username = tokenProvider.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}10.2 注册过滤器
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}11. 小结
本章学习了 Spring Security 的核心功能:
| 内容 | 要点 |
|---|---|
| 认证 | UserDetailsService、UserDetails、PasswordEncoder |
| 授权 | URL 授权、方法授权、权限表达式 |
| CSRF | Token 生成、表单集成 |
| Session | Session 管理、Session 固定防护 |
| OAuth2 | 第三方登录、自定义用户服务 |
| 异常处理 | AuthenticationEntryPoint、AccessDeniedHandler |
| CORS | 跨域配置 |
| 过滤器 | 自定义过滤器、过滤器链 |
下一章将学习 JWT 认证实战。