Spring Security是Spring家族中负责认证(Authentication)和授权(Authorization)的框架,它为基于Java EE的企业应用提供了全面的安全服务。我们先从最基础的概念开始,逐步深入。
认证和授权是安全框架的两个核心概念,它们的关系可以用以下表格说明:
对比维度 | 认证(Authentication) | 授权(Authorization) |
---|---|---|
定义 | 验证用户身份的真实性 | 验证用户是否有权限执行特定操作 |
关注点 | 你是谁 | 你能做什么 |
典型实现 | 用户名密码、指纹、短信验证码等 | 角色检查、权限检查等 |
执行时机 | 通常在授权之前 | 通常在认证成功之后 |
示例 | 登录系统 | 访问管理员后台 |
Spring Security的核心架构主要包含以下组件:
// 典型认证流程代码示例
public class AuthenticationExample {
public void authenticate(String username, String password) {
AuthenticationManager authenticationManager = // 获取认证管理器
Authentication authentication = new UsernamePasswordAuthenticationToken(username, password);
try {
Authentication result = authenticationManager.authenticate(authentication);
SecurityContextHolder.getContext().setAuthentication(result);
System.out.println("认证成功: " + result.getName());
} catch (AuthenticationException e) {
System.out.println("认证失败: " + e.getMessage());
}
}
}
Spring Security基于Servlet过滤器实现,其核心过滤器链如下:
每个过滤器的功能说明:
首先创建一个SpringBoot项目,在pom.xml中添加Spring Security依赖:
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
dependencies>
创建一个基础的安全配置类,继承WebSecurityConfigurerAdapter:
@Configuration
@EnableWebSecurity
public class BasicSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll() // 允许所有人访问首页
.anyRequest().authenticated() // 其他请求需要认证
.and()
.formLogin()
.loginPage("/login") // 自定义登录页
.permitAll() // 允许所有人访问登录页
.and()
.logout()
.permitAll(); // 允许所有人注销
}
@Bean
@Override
public UserDetailsService userDetailsService() {
// 内存中创建两个测试用户
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("admin")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
创建几个简单的控制器来测试我们的安全配置:
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "home"; // 首页模板
}
@GetMapping("/hello")
@ResponseBody
public String hello() {
return "Hello, Spring Security!";
}
@GetMapping("/admin")
@ResponseBody
public String admin() {
return "Admin Dashboard";
}
}
在resources/templates下创建login.html:
DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login Pagetitle>
head>
<body>
<div th:if="${param.error}">
无效的用户名或密码
div>
<div th:if="${param.logout}">
您已成功注销
div>
<form th:action="@{/login}" method="post">
<div>
<label>用户名: <input type="text" name="username"/>label>
div>
<div>
<label>密码: <input type="password" name="password"/>label>
div>
<div>
<input type="submit" value="登录"/>
div>
form>
body>
html>
实际项目中,我们通常不会使用内存用户,而是从数据库加载用户信息。下面展示如何实现基于数据库的认证。
@Entity
@Table(name = "users")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private boolean enabled;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
// 实现UserDetails接口的方法
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
}
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
// 其他getter和setter...
}
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String name;
// 其他getter和setter...
}
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.isEnabled(),
true, // accountNonExpired
true, // credentialsNonExpired
true, // accountNonLocked
user.getAuthorities());
}
}
@Configuration
@EnableWebSecurity
public class DatabaseSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 其他配置...
}
Spring Security支持多种密码加密方式,以下是常见加密方式的对比:
加密方式 | 安全性 | 速度 | 特点 |
---|---|---|---|
BCrypt | 高 | 中等 | 自动加盐,内置工作因子,推荐使用 |
Argon2 | 非常高 | 慢 | 密码哈希竞赛获胜者,抗GPU/ASIC攻击,但配置复杂 |
PBKDF2 | 中 | 慢 | 可配置迭代次数,简单易用 |
SCrypt | 高 | 慢 | 内存密集型,抗硬件攻击 |
Plain Text(不加密) | 无 | 快 | 绝对不要在生产环境使用 |
SHA-256/SHA-512 | 低 | 快 | 单向哈希但无加盐,易受彩虹表攻击 |
BCrypt是最常用的加密方式,使用方法如下:
@Bean
public PasswordEncoder passwordEncoder() {
// 推荐使用BCryptPasswordEncoder,强度10-12
return new BCryptPasswordEncoder(12);
}
Spring Security支持基于角色的访问控制,可以通过注解或配置方式实现。
在配置类上添加@EnableGlobalMethodSecurity注解:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
prePostEnabled = true, // 启用@PreAuthorize和@PostAuthorize
securedEnabled = true, // 启用@Secured
jsr250Enabled = true) // 启用JSR-250注解如@RolesAllowed
public class MethodSecurityConfig extends WebSecurityConfigurerAdapter {
// 配置...
}
然后在控制器方法上使用注解:
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/user")
@PreAuthorize("hasRole('USER')")
public String userEndpoint() {
return "普通用户可访问";
}
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String adminEndpoint() {
return "管理员可访问";
}
@GetMapping("/both")
@PreAuthorize("hasAnyRole('USER', 'ADMIN')")
public String bothEndpoint() {
return "用户和管理员都可访问";
}
}
Spring Security支持强大的SpEL表达式,可以实现更复杂的访问控制逻辑:
@RestController
@RequestMapping("/account")
public class AccountController {
@GetMapping("/{id}")
@PreAuthorize("hasPermission(#id, 'account', 'read')")
public Account getAccount(@PathVariable Long id) {
// 实现...
}
@PostMapping
@PreAuthorize("hasRole('ADMIN') or #account.owner == authentication.name")
public Account createAccount(@RequestBody Account account) {
// 实现...
}
}
除了基于角色的访问控制,Spring Security还支持基于权限的细粒度控制。
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Autowired
private AccountRepository accountRepository;
@Override
public boolean hasPermission(Authentication authentication,
Object targetDomainObject,
Object permission) {
if (authentication == null || !(permission instanceof String)) {
return false;
}
String username = authentication.getName();
Account account = (Account) targetDomainObject;
return account.getOwner().equals(username) ||
authentication.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
}
@Override
public boolean hasPermission(Authentication authentication,
Serializable targetId,
String targetType,
Object permission) {
if (authentication == null || !(targetType instanceof String) || !(permission instanceof String)) {
return false;
}
Account account = accountRepository.findById((Long) targetId)
.orElseThrow(() -> new ResourceNotFoundException("Account not found"));
return hasPermission(authentication, account, permission);
}
}
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Autowired
private CustomPermissionEvaluator permissionEvaluator;
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler =
new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(permissionEvaluator);
return expressionHandler;
}
}
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromJWT(jwt);
UserDetails userDetails = userDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("无法设置用户认证", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails user = userDetailsService.loadUserByUsername(username);
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("密码错误");
}
if (!user.isEnabled()) {
throw new DisabledException("用户已被禁用");
}
return new UsernamePasswordAuthenticationToken(
user, password, user.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
Spring Security提供了对OAuth2的全面支持,下面展示如何集成OAuth2登录。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-oauth2-clientartifactId>
dependency>
@Configuration
public class OAuth2LoginConfig {
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
}
private ClientRegistration googleClientRegistration() {
return ClientRegistration.withRegistrationId("google")
.clientId("your-client-id")
.clientSecret("your-client-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
.scope("openid", "profile", "email")
.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
.tokenUri("https://www.googleapis.com/oauth2/v4/token")
.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
.userNameAttributeName(IdTokenClaimNames.SUB)
.clientName("Google")
.build();
}
}
@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/login**").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login()
.loginPage("/login")
.userInfoEndpoint()
.userService(customOAuth2UserService())
.and()
.successHandler(oAuth2AuthenticationSuccessHandler())
.and()
.logout()
.logoutSuccessUrl("/")
.permitAll();
}
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> customOAuth2UserService() {
return new CustomOAuth2UserService();
}
@Bean
public AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler() {
return new CustomOAuth2AuthenticationSuccessHandler();
}
}
Spring Security默认启用CSRF防护,对于REST API可以禁用:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.ignoringAntMatchers("/api/**") // 对API禁用CSRF
.and()
// 其他配置...
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://trusted.com"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
Spring Security默认添加了一些安全相关的HTTP头,可以自定义:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers()
.contentSecurityPolicy("default-src 'self'")
.and()
.frameOptions()
.sameOrigin()
.and()
.xssProtection()
.block(true)
.and()
.httpStrictTransportSecurity()
.includeSubDomains(true)
.maxAgeInSeconds(31536000);
}
@Component
public class JwtTokenProvider {
@Value("${app.jwt.secret}")
private String jwtSecret;
@Value("${app.jwt.expirationInMs}")
private int jwtExpirationInMs;
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public Long getUserIdFromJWT(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException ex) {
logger.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
logger.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
logger.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
logger.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
logger.error("JWT claims string is empty.");
}
return false;
}
}
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenProvider tokenProvider;
@PostMapping("/signin")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = tokenProvider.generateToken(authentication);
return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
}
}
@Service
public class SmsService {
@Value("${twilio.accountSid}")
private String accountSid;
@Value("${twilio.authToken}")
private String authToken;
@Value("${twilio.phoneNumber}")
private String fromPhoneNumber;
public void sendVerificationCode(String phoneNumber, String code) {
Twilio.init(accountSid, authToken);
Message message = Message.creator(
new PhoneNumber(phoneNumber),
new PhoneNumber(fromPhoneNumber),
"您的验证码是: " + code)
.create();
}
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/css/**",
"/js/**",
"/images/**",
"/webjars/**",
"/favicon.ico",
"/actuator/**");
}
@Component
public class CustomAuthenticationEventListener {
private static final Logger logger = LoggerFactory.getLogger(CustomAuthenticationEventListener.class);
@EventListener
public void handleAuthenticationSuccess(AuthenticationSuccessEvent event) {
logger.info("用户 {} 登录成功", event.getAuthentication().getName());
// 记录登录日志...
}
@EventListener
public void handleAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
logger.warn("用户 {} 登录失败: {}",
event.getAuthentication().getName(),
event.getException().getMessage());
// 记录失败尝试...
}
@EventListener
public void handleLogoutSuccess(LogoutSuccessEvent event) {
logger.info("用户 {} 注销成功", event.getAuthentication().getName());
// 记录注销日志...
}
}
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class SecurityTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(username = "user", roles = {"USER"})
public void whenUserAccessUserEndpoint_thenOk() throws Exception {
mockMvc.perform(get("/api/user"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(username = "user", roles = {"USER"})
public void whenUserAccessAdminEndpoint_thenForbidden() throws Exception {
mockMvc.perform(get("/api/admin"))
.andExpect(status().isForbidden());
}
@Test
public void whenUnauthenticated_thenRedirectToLogin() throws Exception {
mockMvc.perform(get("/api/user"))
.andExpect(status().is3xxRedirection());
}
}
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class OAuth2LoginTest {
@LocalServerPort
private int port;
@Test
public void whenLoginWithValidCredentials_thenSuccess() {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("username", "user");
map.add("password", "password");
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
ResponseEntity<String> response = restTemplate.postForEntity(
"http://localhost:" + port + "/login", request, String.class);
assertThat(response.getStatusCodeValue()).isEqualTo(200);
assertThat(response.getHeaders().get("Set-Cookie")).isNotNull();
}
}
问题现象 | 可能原因 | 解决方案 |
---|---|---|
登录后重定向到/login?error | 用户名/密码错误 | 检查用户名密码,确保UserDetailsService返回正确用户 |
403 Forbidden | CSRF令牌缺失或无效 | 确保表单包含CSRF令牌,或对API禁用CSRF |
无法注入AuthenticationManager | 未正确暴露Bean | 在配置类中添加@Bean注解覆盖authenticationManagerBean()方法 |
密码编码器不匹配 | 使用了错误的密码编码器 | 确保注册的PasswordEncoder与存储密码时使用的编码器一致 |
权限注解不生效 | 未启用方法级安全 | 检查是否添加了@EnableGlobalMethodSecurity注解 |
静态资源被拦截 | 安全配置未忽略静态资源 | 在configure(WebSecurity web)方法中配置忽略静态资源路径 |
启用调试日志:在application.properties中添加
logging.level.org.springframework.security=DEBUG
检查过滤器链:添加自定义过滤器时注意顺序
http.addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class);
检查SecurityContext:在关键点打印SecurityContext内容
SecurityContext context = SecurityContextHolder.getContext();
System.out.println("Authentication: " + context.getAuthentication());
使用Spring Security测试支持:
@WithMockUser(username="admin", roles={"ADMIN"})
@Test
public void testAdminEndpoint() {
// 测试代码
}
@Configuration
@EnableWebSecurity
public class OAuth2ResourceServerConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.decoder(jwtDecoder());
}
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri("https://idp.example.com/.well-known/jwks.json").build();
}
}
@EnableWebFluxSecurity
public class ReactiveSecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange()
.pathMatchers("/public/**").permitAll()
.anyExchange().authenticated()
.and()
.httpBasic().and()
.formLogin().and()
.build();
}
}
密码安全:
会话管理:
API安全:
持续监控:
官方文档:
书籍:
在线课程:
通过本指南,您应该已经掌握了SpringBoot集成Spring Security的全面知识,从基础配置到高级特性,从认证授权到安全防护。记住,安全是一个持续的过程,需要不断学习、实践和更新知识。希望这篇详尽的指南能帮助您构建更加安全的SpringBoot应用!
关注不关注,你自己决定(但正确的决定只有一个)。
喜欢的点个关注,想了解更多的可以关注微信公众号 “Eric的技术杂货库” ,提供更多的干货以及资料下载保存!