Spring Security 完全指南
目录
- [Spring Security 概述](#Spring Security 概述)
- 快速入门
- 核心概念
- 认证配置
- 授权配置
- 密码加密
- 自定义登录
- 记住我功能
- 会话管理
- [CSRF 防护](#CSRF 防护)
- [JWT 集成](#JWT 集成)
- [OAuth2 集成](#OAuth2 集成)
- 方法级安全
- 实战案例
- 最佳实践
1. Spring Security 概述
1.1 什么是 Spring Security
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,是 Spring 生态系统中保护应用程序的事实标准。
1.2 主要功能
Spring Security 功能:
├── 认证(Authentication)- 验证用户身份
├── 授权(Authorization)- 控制访问权限
├── 防护攻击 - CSRF、XSS、会话固定等
├── 集成支持 - LDAP、OAuth2、SAML 等
└── 方法级安全 - 注解控制方法访问
1.3 核心模块
xml
<!-- 核心模块 -->
spring-security-core <!-- 核心功能 -->
spring-security-web <!-- Web 安全 -->
spring-security-config <!-- 配置支持 -->
spring-security-oauth2 <!-- OAuth2 支持 -->
spring-security-jwt <!-- JWT 支持 -->
2. 快速入门
2.1 添加依赖
xml
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 如果需要测试 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
groovy
// Gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
2.2 默认行为
添加依赖后,Spring Security 自动启用:
-
所有请求都需要认证
-
生成默认登录页面
/login -
生成默认登出页面
/logout -
创建默认用户
user,密码在控制台输出 -
启用 CSRF 防护
Using generated security password: 8e4f5c2a-1234-5678-9abc-def012345678
2.3 配置默认用户
yaml
# application.yml
spring:
security:
user:
name: admin
password: admin123
roles: ADMIN
2.4 基本配置类
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**", "/login", "/register").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/home")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
);
return http.build();
}
}
3. 核心概念
3.1 SecurityContext
java
// 获取当前认证信息
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
// 获取用户信息
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
// 判断是否认证
boolean isAuthenticated = authentication.isAuthenticated();
3.2 Authentication
java
public interface Authentication extends Principal, Serializable {
// 获取权限集合
Collection<? extends GrantedAuthority> getAuthorities();
// 获取凭证(密码)
Object getCredentials();
// 获取详细信息
Object getDetails();
// 获取主体(用户)
Object getPrincipal();
// 是否已认证
boolean isAuthenticated();
// 设置认证状态
void setAuthenticated(boolean isAuthenticated);
}
3.3 UserDetails
java
public interface UserDetails extends Serializable {
// 获取权限
Collection<? extends GrantedAuthority> getAuthorities();
// 获取密码
String getPassword();
// 获取用户名
String getUsername();
// 账户是否未过期
boolean isAccountNonExpired();
// 账户是否未锁定
boolean isAccountNonLocked();
// 凭证是否未过期
boolean isCredentialsNonExpired();
// 是否启用
boolean isEnabled();
}
3.4 UserDetailsService
java
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
3.5 GrantedAuthority
java
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
// 常用实现
SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_ADMIN");
3.6 过滤器链
Spring Security 过滤器链:
├── SecurityContextPersistenceFilter - 安全上下文持久化
├── UsernamePasswordAuthenticationFilter - 用户名密码认证
├── BasicAuthenticationFilter - Basic 认证
├── RememberMeAuthenticationFilter - 记住我
├── AnonymousAuthenticationFilter - 匿名认证
├── ExceptionTranslationFilter - 异常处理
└── FilterSecurityInterceptor - 授权过滤器
4. 认证配置
4.1 内存用户
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@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("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
4.2 数据库用户
java
// 用户实体
@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;
private boolean enabled = true;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
@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;
}
@Override
public boolean isEnabled() {
return enabled;
}
// 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
}
java
// UserDetailsService 实现
@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 user;
}
}
java
// 配置类
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private CustomUserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.userDetailsService(userDetailsService);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
4.3 自定义 AuthenticationProvider
java
@Component
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 UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
5. 授权配置
5.1 URL 授权
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// 公开访问
.requestMatchers("/", "/home", "/public/**").permitAll()
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
// 角色控制
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
// 权限控制
.requestMatchers("/api/users/**").hasAuthority("USER_READ")
.requestMatchers(HttpMethod.POST, "/api/users/**").hasAuthority("USER_WRITE")
.requestMatchers(HttpMethod.DELETE, "/api/users/**").hasAuthority("USER_DELETE")
// IP 限制
.requestMatchers("/internal/**").hasIpAddress("192.168.1.0/24")
// 其他请求需要认证
.anyRequest().authenticated()
);
return http.build();
}
5.2 表达式授权
java
.authorizeHttpRequests(auth -> auth
// 使用 SpEL 表达式
.requestMatchers("/admin/**").access(
AuthorizationManagers.allOf(
AuthorityAuthorizationManager.hasRole("ADMIN"),
new WebExpressionAuthorizationManager("hasIpAddress('192.168.1.0/24')")
)
)
)
5.3 自定义授权管理器
java
@Component
public class CustomAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication,
RequestAuthorizationContext context) {
HttpServletRequest request = context.getRequest();
Authentication auth = authentication.get();
// 自定义授权逻辑
if (auth == null || !auth.isAuthenticated()) {
return new AuthorizationDecision(false);
}
// 检查特定条件
String path = request.getRequestURI();
if (path.startsWith("/api/admin")) {
boolean hasAdminRole = auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
return new AuthorizationDecision(hasAdminRole);
}
return new AuthorizationDecision(true);
}
}
// 使用
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
CustomAuthorizationManager customAuthorizationManager) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").access(customAuthorizationManager)
);
return http.build();
}
6. 密码加密
6.1 BCryptPasswordEncoder
java
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 使用
@Autowired
private PasswordEncoder passwordEncoder;
// 加密密码
String encodedPassword = passwordEncoder.encode("rawPassword");
// 验证密码
boolean matches = passwordEncoder.matches("rawPassword", encodedPassword);
6.2 DelegatingPasswordEncoder
java
@Bean
public PasswordEncoder passwordEncoder() {
// 支持多种加密方式
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
// 密码格式:{id}encodedPassword
// {bcrypt}$2a$10$...
// {noop}plainTextPassword
// {sha256}...
6.3 自定义 PasswordEncoder
java
@Bean
public PasswordEncoder passwordEncoder() {
String idForEncode = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("sha256", new StandardPasswordEncoder());
return new DelegatingPasswordEncoder(idForEncode, encoders);
}
7. 自定义登录
7.1 自定义登录页面
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.formLogin(form -> form
.loginPage("/login") // 登录页面
.loginProcessingUrl("/doLogin") // 登录处理 URL
.usernameParameter("username") // 用户名参数
.passwordParameter("password") // 密码参数
.defaultSuccessUrl("/home", true) // 登录成功跳转
.failureUrl("/login?error=true") // 登录失败跳转
.successHandler(authenticationSuccessHandler())
.failureHandler(authenticationFailureHandler())
.permitAll()
);
return http.build();
}
7.2 登录成功处理器
java
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
// 记录登录日志
String username = authentication.getName();
String ip = request.getRemoteAddr();
System.out.println("用户登录成功: " + username + ", IP: " + ip);
// 根据角色跳转不同页面
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
if (authorities.stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
response.sendRedirect("/admin/dashboard");
} else {
response.sendRedirect("/user/home");
}
}
}
// API 登录成功返回 JSON
@Component
public class JsonAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "登录成功");
result.put("username", authentication.getName());
response.getWriter().write(objectMapper.writeValueAsString(result));
}
}
7.3 登录失败处理器
java
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException {
String errorMessage = "登录失败";
if (exception instanceof BadCredentialsException) {
errorMessage = "用户名或密码错误";
} else if (exception instanceof DisabledException) {
errorMessage = "账户已被禁用";
} else if (exception instanceof LockedException) {
errorMessage = "账户已被锁定";
} else if (exception instanceof AccountExpiredException) {
errorMessage = "账户已过期";
}
response.sendRedirect("/login?error=" + URLEncoder.encode(errorMessage, "UTF-8"));
}
}
// API 登录失败返回 JSON
@Component
public class JsonAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
Map<String, Object> result = new HashMap<>();
result.put("code", 401);
result.put("message", exception.getMessage());
response.getWriter().write(objectMapper.writeValueAsString(result));
}
}
7.4 登录页面示例
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>登录</title>
</head>
<body>
<div class="login-container">
<h2>用户登录</h2>
<div th:if="${param.error}" class="error">
用户名或密码错误
</div>
<div th:if="${param.logout}" class="success">
已成功退出
</div>
<form th:action="@{/doLogin}" method="post">
<div>
<label>用户名:</label>
<input type="text" name="username" required/>
</div>
<div>
<label>密码:</label>
<input type="password" name="password" required/>
</div>
<div>
<label>
<input type="checkbox" name="remember-me"/> 记住我
</label>
</div>
<button type="submit">登录</button>
</form>
</div>
</body>
</html>
8. 记住我功能
8.1 基于 Cookie
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.rememberMe(remember -> remember
.key("uniqueAndSecret") // 加密 key
.tokenValiditySeconds(86400 * 7) // 有效期 7 天
.rememberMeParameter("remember-me") // 表单参数名
.rememberMeCookieName("remember-me") // Cookie 名
.userDetailsService(userDetailsService)
);
return http.build();
}
8.2 持久化 Token
java
// 创建数据库表
CREATE TABLE persistent_logins (
username VARCHAR(64) NOT NULL,
series VARCHAR(64) PRIMARY KEY,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP NOT NULL
);
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 启动时创建表(仅首次)
// tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.rememberMe(remember -> remember
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(86400 * 7)
.userDetailsService(userDetailsService)
);
return http.build();
}
}
9. 会话管理
9.1 会话配置
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
// 会话创建策略
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
// 会话固定攻击防护
.sessionFixation().migrateSession()
// 最大会话数
.maximumSessions(1)
// 达到最大会话数时阻止新登录
.maxSessionsPreventsLogin(true)
// 会话过期跳转
.expiredUrl("/login?expired")
);
return http.build();
}
9.2 会话创建策略
java
// ALWAYS - 总是创建会话
// IF_REQUIRED - 需要时创建(默认)
// NEVER - 不创建,但使用已存在的
// STATELESS - 不创建也不使用(适合 REST API)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
9.3 并发会话控制
java
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
.maximumSessions(1)
.maxSessionsPreventsLogin(false) // 踢掉旧会话
.expiredSessionStrategy(event -> {
HttpServletResponse response = event.getResponse();
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"会话已过期\"}");
})
);
return http.build();
}
10. CSRF 防护
10.1 默认 CSRF 配置
java
// CSRF 默认开启,表单需要包含 CSRF Token
<form method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<!-- 或使用 Thymeleaf -->
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
</form>
10.2 禁用 CSRF
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用 CSRF(REST API 通常禁用)
.csrf(csrf -> csrf.disable());
return http.build();
}
10.3 自定义 CSRF 配置
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
// 忽略特定路径
.ignoringRequestMatchers("/api/**", "/webhook/**")
// 自定义 Token 仓库
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
return http.build();
}
11. JWT 集成
11.1 添加依赖
xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
11.2 JWT 工具类
java
@Component
public class JwtUtils {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private long expiration;
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(keyBytes);
}
// 生成 Token
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
// 解析 Token
public Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
// 获取用户名
public String getUsername(String token) {
return parseToken(token).getSubject();
}
// 验证 Token
public boolean validateToken(String token, UserDetails userDetails) {
try {
Claims claims = parseToken(token);
String username = claims.getSubject();
Date expiration = claims.getExpiration();
return username.equals(userDetails.getUsername())
&& !expiration.before(new Date());
} catch (Exception e) {
return false;
}
}
// 判断是否过期
public boolean isTokenExpired(String token) {
try {
return parseToken(token).getExpiration().before(new Date());
} catch (ExpiredJwtException e) {
return true;
}
}
}
11.3 JWT 认证过滤器
java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
try {
String username = jwtUtils.getUsername(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtils.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (ExpiredJwtException e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"Token已过期\"}");
return;
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"无效的Token\"}");
return;
}
filterChain.doFilter(request, response);
}
}
11.4 JWT 安全配置
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
11.5 登录接口
java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsService userDetailsService;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
try {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(), request.getPassword())
);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
String token = jwtUtils.generateToken(userDetails);
Map<String, Object> response = new HashMap<>();
response.put("code", 200);
response.put("message", "登录成功");
response.put("token", token);
response.put("username", userDetails.getUsername());
return ResponseEntity.ok(response);
} catch (AuthenticationException e) {
return ResponseEntity.status(401)
.body(Map.of("code", 401, "message", "用户名或密码错误"));
}
}
}
// 登录请求 DTO
public class LoginRequest {
private String username;
private String password;
// getter/setter
}
12. OAuth2 集成
12.1 添加依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
12.2 配置 OAuth2
yaml
# application.yml
spring:
security:
oauth2:
client:
registration:
github:
client-id: your-client-id
client-secret: your-client-secret
scope: user:email,read:user
google:
client-id: your-client-id
client-secret: your-client-secret
scope: openid,profile,email
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/user
user-name-attribute: login
12.3 安全配置
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login/**", "/error").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/home")
.failureUrl("/login?error")
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService())
)
);
return http.build();
}
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> customOAuth2UserService() {
return new CustomOAuth2UserService();
}
}
12.4 自定义 OAuth2UserService
java
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Autowired
private UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = super.loadUser(userRequest);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String email = oauth2User.getAttribute("email");
String name = oauth2User.getAttribute("name");
// 查找或创建用户
User user = userRepository.findByEmail(email)
.orElseGet(() -> {
User newUser = new User();
newUser.setEmail(email);
newUser.setName(name);
newUser.setProvider(registrationId);
return userRepository.save(newUser);
});
return new CustomOAuth2User(oauth2User, user);
}
}
13. 方法级安全
13.1 启用方法安全
java
@Configuration
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig {
}
13.2 @PreAuthorize
java
@Service
public class UserService {
// 需要 ADMIN 角色
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {
// 删除用户
}
// 需要特定权限
@PreAuthorize("hasAuthority('USER_READ')")
public User getUser(Long id) {
// 获取用户
}
// 多条件
@PreAuthorize("hasRole('ADMIN') or hasRole('MANAGER')")
public List<User> getAllUsers() {
// 获取所有用户
}
// 使用参数
@PreAuthorize("#username == authentication.name or hasRole('ADMIN')")
public User getUserByUsername(String username) {
// 获取用户
}
// 使用 SpEL
@PreAuthorize("@securityService.canAccess(#id)")
public void updateUser(Long id, User user) {
// 更新用户
}
}
13.3 @PostAuthorize
java
@Service
public class DocumentService {
// 返回后检查
@PostAuthorize("returnObject.owner == authentication.name or hasRole('ADMIN')")
public Document getDocument(Long id) {
return documentRepository.findById(id).orElse(null);
}
}
13.4 @Secured
java
@Service
public class OrderService {
@Secured("ROLE_ADMIN")
public void deleteOrder(Long id) {
// 删除订单
}
@Secured({"ROLE_USER", "ROLE_ADMIN"})
public Order getOrder(Long id) {
// 获取订单
}
}
13.5 自定义安全表达式
java
@Component("securityService")
public class SecurityService {
@Autowired
private UserRepository userRepository;
public boolean canAccess(Long userId) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null) return false;
String currentUsername = auth.getName();
User user = userRepository.findById(userId).orElse(null);
if (user == null) return false;
// 检查是否是本人或管理员
return user.getUsername().equals(currentUsername)
|| auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
}
}
// 使用
@PreAuthorize("@securityService.canAccess(#id)")
public void updateUser(Long id, UserDTO dto) {
// 更新用户
}
14. 实战案例
14.1 完整的 JWT 认证系统
java
// 1. 配置文件
// application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/security_demo
username: root
password: root
jpa:
hibernate:
ddl-auto: update
jwt:
secret: dGhpcyBpcyBhIHZlcnkgbG9uZyBzZWNyZXQga2V5IGZvciBqd3QgdG9rZW4=
expiration: 86400000 # 24小时
java
// 2. 用户实体
@Entity
@Table(name = "users")
@Data
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(unique = true)
private String email;
private boolean enabled = true;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "role")
private Set<String> roles = new HashSet<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
}
java
// 3. Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
}
java
// 4. DTO
@Data
public class LoginRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}
@Data
public class RegisterRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度3-20")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 40, message = "密码长度6-40")
private String password;
@Email(message = "邮箱格式不正确")
private String email;
}
@Data
@AllArgsConstructor
public class JwtResponse {
private String token;
private String type = "Bearer";
private Long id;
private String username;
private List<String> roles;
public JwtResponse(String token, Long id, String username, List<String> roles) {
this.token = token;
this.id = id;
this.username = username;
this.roles = roles;
}
}
java
// 5. Service
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
}
public User register(RegisterRequest request) {
if (userRepository.existsByUsername(request.getUsername())) {
throw new RuntimeException("用户名已存在");
}
if (request.getEmail() != null && userRepository.existsByEmail(request.getEmail())) {
throw new RuntimeException("邮箱已被使用");
}
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setEmail(request.getEmail());
user.setRoles(Set.of("ROLE_USER"));
return userRepository.save(user);
}
}
java
// 6. Controller
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
@Autowired
private JwtUtils jwtUtils;
@PostMapping("/login")
public ResponseEntity<?> login(@Valid @RequestBody LoginRequest request) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
SecurityContextHolder.getContext().setAuthentication(authentication);
User user = (User) authentication.getPrincipal();
String token = jwtUtils.generateToken(user);
List<String> roles = user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
return ResponseEntity.ok(new JwtResponse(token, user.getId(), user.getUsername(), roles));
}
@PostMapping("/register")
public ResponseEntity<?> register(@Valid @RequestBody RegisterRequest request) {
try {
User user = userService.register(request);
return ResponseEntity.ok(Map.of(
"message", "注册成功",
"username", user.getUsername()
));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
}
}
@GetMapping("/me")
public ResponseEntity<?> getCurrentUser(@AuthenticationPrincipal User user) {
return ResponseEntity.ok(Map.of(
"id", user.getId(),
"username", user.getUsername(),
"email", user.getEmail(),
"roles", user.getRoles()
));
}
}
java
// 7. 安全配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private UserService userService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.exceptionHandling(exception -> exception
.authenticationEntryPoint((request, response, authException) -> {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(401);
response.getWriter().write("{\"code\":401,\"message\":\"未认证\"}");
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(403);
response.getWriter().write("{\"code\":403,\"message\":\"权限不足\"}");
})
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
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);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
15. 最佳实践
15.1 密码安全
java
// ✅ 使用 BCrypt 加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 强度 12
}
// ✅ 密码复杂度验证
public boolean isValidPassword(String password) {
// 至少8位,包含大小写字母和数字
return password.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$");
}
15.2 Token 安全
java
// ✅ 使用足够长的密钥
jwt:
secret: dGhpcyBpcyBhIHZlcnkgbG9uZyBzZWNyZXQga2V5IGZvciBqd3QgdG9rZW4gYXQgbGVhc3QgMjU2IGJpdHM=
// ✅ 设置合理的过期时间
jwt:
expiration: 3600000 # 1小时
// ✅ 实现 Token 刷新机制
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestHeader("Authorization") String token) {
// 验证并刷新 Token
}
15.3 异常处理
java
@ControllerAdvice
public class SecurityExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<?> handleAccessDeniedException(AccessDeniedException e) {
return ResponseEntity.status(403)
.body(Map.of("code", 403, "message", "权限不足"));
}
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<?> handleAuthenticationException(AuthenticationException e) {
return ResponseEntity.status(401)
.body(Map.of("code", 401, "message", "认证失败"));
}
}
15.4 日志审计
java
@Component
public class SecurityAuditListener {
private static final Logger logger = LoggerFactory.getLogger(SecurityAuditListener.class);
@EventListener
public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
String username = event.getAuthentication().getName();
logger.info("用户登录成功: {}", username);
}
@EventListener
public void onAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
String username = event.getAuthentication().getName();
logger.warn("用户登录失败: {}, 原因: {}", username, event.getException().getMessage());
}
}
15.5 安全配置检查清单
✅ 使用 HTTPS
✅ 启用 CSRF 防护(Web 应用)
✅ 配置 CORS
✅ 使用强密码加密
✅ 设置合理的会话超时
✅ 限制登录尝试次数
✅ 记录安全日志
✅ 定期更新依赖
✅ 最小权限原则
✅ 输入验证
附录:常用注解速查
| 注解 | 说明 |
|---|---|
@EnableWebSecurity |
启用 Web 安全 |
@EnableMethodSecurity |
启用方法级安全 |
@PreAuthorize |
方法执行前检查 |
@PostAuthorize |
方法执行后检查 |
@Secured |
角色检查 |
@AuthenticationPrincipal |
注入当前用户 |
@CurrentSecurityContext |
注入安全上下文 |