SpringSecurity
2023年4月15日大约 4 分钟
配置描述
早期版本(主要是Spring Security 5.7之前)通常通过继承 WebSecurityConfigurerAdapter 抽象类并重写其方法来配置安全规则,在后面的版本中,官方推荐使用基于组件的配置方式,即通过创建 SecurityFilterChain Bean 来配置:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
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.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.time.Duration;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final UserDetailsService userDetailsService;
public SecurityConfig(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Bean
@Order(1)
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(authz -> authz
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.cors(cors -> cors.configurationSource(corsConfigurationSource()));
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain webSecurityFilterChain(HttpSecurity http) throws Exception {
http
// 授权配置
.authorizeHttpRequests(authz -> authz
.requestMatchers("/design", "/orders").hasRole("USER")
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/", "/**").permitAll()
)
// 表单登录配置
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/authenticate")
.usernameParameter("user")
.passwordParameter("pwd")
.defaultSuccessUrl("/hello")
)
// 退出登录配置
.logout(logout -> logout
.logoutSuccessUrl("/")
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
)
// H2控制台特殊处理
.csrf(csrf -> csrf
.ignoringRequestMatchers(
new AntPathRequestMatcher("/h2-console/**"),
new AntPathRequestMatcher("/api/**")
)
)
// 安全头设置
.headers(headers -> headers
.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
.xssProtection(xss -> xss
.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)
)
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:")
)
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.preload(true)
.maxAgeInSeconds(Duration.ofDays(365).toSeconds())
)
.referrerPolicy(referrer -> referrer
.policy(HeadersConfigurer.ReferrerPolicyConfig.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
)
)
// CORS配置
.cors(cors -> cors.configurationSource(corsConfigurationSource()));
return http.build();
}
// 生产级CORS配置
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList(
"https://your-production-domain.com",
"https://www.your-production-domain.com"
));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "OPTIONS"));
config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Requested-With"));
config.setExposedHeaders(Arrays.asList("X-Custom-Header"));
config.setAllowCredentials(true);
config.setMaxAge(Duration.ofHours(1).getSeconds());
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
// 强密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 强度因子12
}
// 用户服务强化
@Bean
public UserDetailsService userDetailsService() {
return username -> {
// 生产环境应添加账户锁定、密码过期等检查
var user = userDetailsService.loadUserByUsername(username);
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(user.getAuthorities())
.accountLocked(!user.isEnabled()) // 禁用账户视为锁定
.credentialsExpired(false) // 强制定期修改密码
.accountExpired(false) // 账户过期检查
.disabled(!user.isEnabled())
.build();
};
}
// 安全审计(可选)
@Bean
public org.springframework.security.access.AuditLogger auditLogger() {
return event -> {
if (event.getAccessDecisionException() != null) {
// 记录权限异常到安全审计系统
}
};
}
}- Spring Security 对 Spring 表达式语言的扩展

- Spring Security 配置方法

启用第三方认证
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>- 使用 facebook 登录
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope:
- email
- profile
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
github:
client-id: ${GITHUB_CLIENT_ID}
client-secret: ${GITHUB_CLIENT_SECRET}
scope:
- user:email
facebook:
client-id: ${FACEBOOK_CLIENT_ID}
client-secret: ${FACEBOOK_CLIENT_SECRET}
scope:
- email
- public_profile配置开启三方登录
配置类
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.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// 自定义用户服务(可选)
private final CustomOAuth2UserService customOAuth2UserService;
public SecurityConfig(CustomOAuth2UserService customOAuth2UserService) {
this.customOAuth2UserService = customOAuth2UserService;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/", "/login", "/error", "/webjars/**").permitAll()
.requestMatchers("/design", "/orders").hasRole("USER")
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/home", true)
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(oidcUserService()) // 用于OpenID Connect提供者(如Google)
.userService(customOAuth2UserService) // 用于OAuth2提供者(如GitHub, Facebook)
)
.successHandler((request, response, authentication) -> {
// 自定义成功处理(例如记录登录日志)
response.sendRedirect("/dashboard");
})
)
.logout(logout -> logout
.logoutSuccessUrl("/")
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
)
.csrf(csrf -> csrf
.ignoringRequestMatchers("/login/oauth2/code/**")
);
return http.build();
}
// 配置OpenID Connect用户服务
@Bean
public OidcUserService oidcUserService() {
OidcUserService oidcUserService = new OidcUserService();
// 可添加自定义逻辑
oidcUserService.setOauth2UserService(oidcRequest -> {
// 处理OpenID Connect用户信息
return customOAuth2UserService.loadUser(oidcRequest);
});
return oidcUserService;
}
}自定义用户服务
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Autowired
private UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// 1. 获取默认用户信息
OAuth2User oauth2User = super.loadUser(userRequest);
// 2. 提取注册ID和属性
String registrationId = userRequest.getClientRegistration().getRegistrationId();
Map<String, Object> attributes = oauth2User.getAttributes();
// 3. 构建统一用户标识
String email = extractEmail(attributes, registrationId);
// 4. 查找或创建用户
User user = userRepository.findByEmail(email)
.orElseGet(() -> createNewUser(attributes, registrationId));
// 5. 返回自定义用户对象
return new CustomOAuth2User(
user.getAuthorities(),
attributes,
userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(),
user
);
}
private String extractEmail(Map<String, Object> attributes, String provider) {
return switch (provider.toLowerCase()) {
case "google" -> (String) attributes.get("email");
case "facebook" -> (String) attributes.get("email");
case "github" -> {
// GitHub需要额外请求邮箱
String accessToken = userRequest.getAccessToken().getTokenValue();
yield fetchGitHubEmail(accessToken);
}
default -> throw new IllegalStateException("Unsupported provider: " + provider);
};
}
private User createNewUser(Map<String, Object> attributes, String provider) {
User newUser = new User();
newUser.setEmail(extractEmail(attributes, provider));
newUser.setName((String) attributes.get("name"));
newUser.setProvider(provider);
newUser.setProviderId((String) attributes.get("sub") ?? (String) attributes.get("id"));
return userRepository.save(newUser);
}
// GitHub邮箱获取方法
private String fetchGitHubEmail(String accessToken) {
// 实现调用GitHub API获取邮箱的逻辑
}
}自定义用户对象
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;
import java.util.Collection;
import java.util.Map;
public class CustomOAuth2User implements OAuth2User {
private final OAuth2User oauth2User;
private final User localUser;
private final Collection<? extends GrantedAuthority> authorities;
public CustomOAuth2User(Collection<? extends GrantedAuthority> authorities,
Map<String, Object> attributes,
String nameAttributeKey,
User localUser) {
this.oauth2User = new DefaultOAuth2User(authorities, attributes, nameAttributeKey);
this.localUser = localUser;
this.authorities = authorities;
}
@Override
public Map<String, Object> getAttributes() {
return oauth2User.getAttributes();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getName() {
return localUser.getUsername();
}
// 获取本地用户信息
public User getLocalUser() {
return localUser;
}
}三方登录跳转连接
<!-- 登录页 -->
<div class="oauth-login">
<h3>使用第三方账号登录</h3>
<a th:href="@{/oauth2/authorization/google}" class="btn btn-google">
<i class="fab fa-google"></i> Google登录
</a>
<a th:href="@{/oauth2/authorization/github}" class="btn btn-github">
<i class="fab fa-github"></i> GitHub登录
</a>
<a th:href="@{/oauth2/authorization/facebook}" class="btn btn-facebook">
<i class="fab fa-facebook"></i> Facebook登录
</a>
</div>退出登录
<form method="POST" th:action="@{/logout}">
<input type="submit" value="Logout" />
</form>
