1. Spring Security概述
Spring Security是Spring生态系统中的安全框架,为Spring Boot应用提供全面的安全解决方案。它支持认证、授权、密码加密、会话管理、CSRF保护等安全功能。
1.1 Spring Security核心概念
- 认证(Authentication):验证用户身份
- 授权(Authorization):控制用户访问权限
- 主体(Principal):当前用户身份
- 权限(Authority):用户拥有的权限
- 角色(Role):权限的集合
1.2 核心依赖
<dependencies>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Security OAuth2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<!-- JWT支持 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- 密码加密 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
</dependencies>
2. 基础安全配置
2.1 安全配置类
package com.example.demo.config;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home", "/register", "/login").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.failureUrl("/login?error=true")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.permitAll()
.and()
.csrf()
.disable();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2.2 用户实体和权限
package com.example.demo.entity;
import javax.persistence.*;
import java.util.Set;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
@Column(name = "full_name")
private String fullName;
@Column(name = "is_enabled")
private Boolean enabled = true;
@Column(name = "is_account_non_locked")
private Boolean accountNonLocked = true;
@Column(name = "is_account_non_expired")
private Boolean accountNonExpired = true;
@Column(name = "is_credentials_non_expired")
private Boolean credentialsNonExpired = true;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles;
// 构造方法
public User() {}
public User(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = password;
}
// getter和setter方法
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getFullName() { return fullName; }
public void setFullName(String fullName) { this.fullName = fullName; }
public Boolean getEnabled() { return enabled; }
public void setEnabled(Boolean enabled) { this.enabled = enabled; }
public Boolean getAccountNonLocked() { return accountNonLocked; }
public void setAccountNonLocked(Boolean accountNonLocked) { this.accountNonLocked = accountNonLocked; }
public Boolean getAccountNonExpired() { return accountNonExpired; }
public void setAccountNonExpired(Boolean accountNonExpired) { this.accountNonExpired = accountNonExpired; }
public Boolean getCredentialsNonExpired() { return credentialsNonExpired; }
public void setCredentialsNonExpired(Boolean credentialsNonExpired) { this.credentialsNonExpired = credentialsNonExpired; }
public Set<Role> getRoles() { return roles; }
public void setRoles(Set<Role> roles) { this.roles = roles; }
}
package com.example.demo.entity;
import javax.persistence.*;
import java.util.Set;
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String name;
@Column
private String description;
@ManyToMany(mappedBy = "roles")
private Set<User> users;
// 构造方法
public Role() {}
public Role(String name, String description) {
this.name = name;
this.description = description;
}
// getter和setter方法
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public Set<User> getUsers() { return users; }
public void setUsers(Set<User> users) { this.users = users; }
}
3. 用户认证服务
3.1 用户详情服务
package com.example.demo.service;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
@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 CustomUserPrincipal(user);
}
public static class CustomUserPrincipal implements UserDetails {
private final User user;
public CustomUserPrincipal(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 user.getAccountNonExpired();
}
@Override
public boolean isAccountNonLocked() {
return user.getAccountNonLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return user.getCredentialsNonExpired();
}
@Override
public boolean isEnabled() {
return user.getEnabled();
}
public User getUser() {
return user;
}
}
}
3.2 认证服务
package com.example.demo.service;
import com.example.demo.entity.Role;
import com.example.demo.entity.User;
import com.example.demo.repository.RoleRepository;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashSet;
import java.util.Set;
@Service
@Transactional
public class AuthService {
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Autowired
private PasswordEncoder passwordEncoder;
// 用户注册
public User registerUser(String username, String email, String password, String fullName) {
// 检查用户名是否已存在
if (userRepository.findByUsername(username).isPresent()) {
throw new RuntimeException("用户名已存在");
}
// 检查邮箱是否已存在
if (userRepository.findByEmail(email).isPresent()) {
throw new RuntimeException("邮箱已存在");
}
// 创建用户
User user = new User();
user.setUsername(username);
user.setEmail(email);
user.setPassword(passwordEncoder.encode(password));
user.setFullName(fullName);
// 设置默认角色
Set<Role> roles = new HashSet<>();
Role userRole = roleRepository.findByName("USER")
.orElseThrow(() -> new RuntimeException("角色不存在"));
roles.add(userRole);
user.setRoles(roles);
return userRepository.save(user);
}
// 用户登录
public User loginUser(String username, String password) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("用户不存在"));
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new RuntimeException("密码错误");
}
if (!user.getEnabled()) {
throw new RuntimeException("账户已被禁用");
}
return user;
}
// 更新用户信息
public User updateUser(Long userId, String fullName, String email) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("用户不存在"));
user.setFullName(fullName);
user.setEmail(email);
return userRepository.save(user);
}
// 修改密码
public void changePassword(Long userId, String oldPassword, String newPassword) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("用户不存在"));
if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
throw new RuntimeException("原密码错误");
}
user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user);
}
// 禁用/启用用户
public void toggleUserStatus(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("用户不存在"));
user.setEnabled(!user.getEnabled());
userRepository.save(user);
}
}
4. JWT认证
4.1 JWT工具类
package com.example.demo.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtTokenUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
// 从token中获取用户名
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
// 从token中获取过期时间
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
// 从token中获取指定声明
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
// 从token中获取所有声明
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
// 检查token是否过期
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
// 生成token
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
// 生成带额外声明的token
public String generateToken(Map<String, Object> claims, String subject) {
return createToken(claims, subject);
}
// 创建token
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
// 验证token
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
// 刷新token
public String refreshToken(String token) {
final Claims claims = getAllClaimsFromToken(token);
claims.setIssuedAt(new Date());
return createToken(claims, claims.getSubject());
}
}
4.2 JWT认证过滤器
package com.example.demo.filter;
import com.example.demo.service.CustomUserDetailsService;
import com.example.demo.util.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
// JWT Token在请求头中的格式是 "Bearer token"
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (Exception e) {
logger.error("无法获取JWT Token或JWT Token已过期", e);
}
}
// 如果用户名不为空且SecurityContext中没有认证信息
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 如果token有效,配置Spring Security手动设置认证
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
4.3 JWT安全配置
package com.example.demo.config;
import com.example.demo.filter.JwtAuthenticationFilter;
import com.example.demo.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class JwtSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
5. OAuth2认证
5.1 OAuth2配置
package com.example.demo.config;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/login", "/oauth2/**").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login()
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.failureUrl("/login?error=true")
.and()
.logout()
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID");
}
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(
googleClientRegistration(),
githubClientRegistration()
);
}
private ClientRegistration googleClientRegistration() {
return ClientRegistration.withRegistrationId("google")
.clientId("your-google-client-id")
.clientSecret("your-google-client-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUriTemplate("{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();
}
private ClientRegistration githubClientRegistration() {
return ClientRegistration.withRegistrationId("github")
.clientId("your-github-client-id")
.clientSecret("your-github-client-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
.scope("read:user")
.authorizationUri("https://github.com/login/oauth/authorize")
.tokenUri("https://github.com/login/oauth/access_token")
.userInfoUri("https://api.github.com/user")
.userNameAttributeName("id")
.clientName("GitHub")
.build();
}
}
5.2 OAuth2用户服务
package com.example.demo.service;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
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;
import org.springframework.transaction.annotation.Transactional;
import java.util.Map;
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Autowired
private UserRepository userRepository;
@Override
@Transactional
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
OAuth2UserInfo userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(registrationId, oAuth2User.getAttributes());
if (userInfo.getEmail() == null || userInfo.getEmail().isEmpty()) {
throw new OAuth2AuthenticationException("无法从OAuth2提供商获取用户邮箱");
}
User user = userRepository.findByEmail(userInfo.getEmail())
.orElseGet(() -> {
User newUser = new User();
newUser.setEmail(userInfo.getEmail());
newUser.setUsername(userInfo.getName());
newUser.setFullName(userInfo.getName());
newUser.setEnabled(true);
return userRepository.save(newUser);
});
return new CustomOAuth2User(oAuth2User.getAttributes(), userInfo, user);
}
public static class CustomOAuth2User implements OAuth2User {
private final Map<String, Object> attributes;
private final OAuth2UserInfo userInfo;
private final User user;
public CustomOAuth2User(Map<String, Object> attributes, OAuth2UserInfo userInfo, User user) {
this.attributes = attributes;
this.userInfo = userInfo;
this.user = user;
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public java.util.Collection<? extends org.springframework.security.core.GrantedAuthority> getAuthorities() {
return user.getRoles().stream()
.map(role -> new org.springframework.security.core.authority.SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(java.util.stream.Collectors.toList());
}
@Override
public String getName() {
return userInfo.getName();
}
public User getUser() {
return user;
}
}
}
6. 方法级安全
6.1 方法安全配置
package com.example.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
@Configuration
@EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true
)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
}
6.2 方法级安全注解
package com.example.demo.service;
import com.example.demo.entity.User;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class SecureUserService {
// 基于角色的访问控制
@Secured("ROLE_ADMIN")
public List<User> getAllUsers() {
// 只有ADMIN角色可以访问
return userRepository.findAll();
}
// 基于表达式的访问控制
@PreAuthorize("hasRole('ADMIN') or hasRole('USER')")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
// 基于用户名的访问控制
@PreAuthorize("authentication.name == #username")
public User getUserByUsername(String username) {
return userRepository.findByUsername(username).orElse(null);
}
// 基于用户ID的访问控制
@PreAuthorize("authentication.principal.id == #userId")
public User updateUser(Long userId, User userDetails) {
User user = userRepository.findById(userId).orElse(null);
if (user != null) {
user.setFullName(userDetails.getFullName());
user.setEmail(userDetails.getEmail());
return userRepository.save(user);
}
return null;
}
// 后置授权检查
@PostAuthorize("returnObject.username == authentication.name")
public User getCurrentUser() {
return getCurrentUserFromSecurityContext();
}
// 基于权限的访问控制
@PreAuthorize("hasAuthority('USER_READ')")
public List<User> getUsers() {
return userRepository.findAll();
}
@PreAuthorize("hasAuthority('USER_WRITE')")
public User createUser(User user) {
return userRepository.save(user);
}
@PreAuthorize("hasAuthority('USER_DELETE')")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
private User getCurrentUserFromSecurityContext() {
// 从SecurityContext获取当前用户
return null;
}
}
7. 密码策略
7.1 密码验证器
package com.example.demo.validator;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.regex.Pattern;
@Component
public class PasswordValidator {
private static final String PASSWORD_PATTERN =
"^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,}$";
private static final Pattern pattern = Pattern.compile(PASSWORD_PATTERN);
public boolean isValid(String password) {
return pattern.matcher(password).matches();
}
public String getPasswordRequirements() {
return "密码必须包含:至少8个字符,包含大小写字母、数字和特殊字符";
}
public boolean isCommonPassword(String password) {
String[] commonPasswords = {
"123456", "password", "123456789", "12345678", "12345",
"1234567", "1234567890", "qwerty", "abc123", "111111"
};
for (String common : commonPasswords) {
if (password.equalsIgnoreCase(common)) {
return true;
}
}
return false;
}
}
7.2 密码策略服务
package com.example.demo.service;
import com.example.demo.validator.PasswordValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class PasswordPolicyService {
@Autowired
private PasswordValidator passwordValidator;
@Autowired
private PasswordEncoder passwordEncoder;
public PasswordValidationResult validatePassword(String password) {
List<String> errors = new ArrayList<>();
if (password == null || password.isEmpty()) {
errors.add("密码不能为空");
return new PasswordValidationResult(false, errors);
}
if (password.length() < 8) {
errors.add("密码长度至少8位");
}
if (!password.matches(".*[a-z].*")) {
errors.add("密码必须包含小写字母");
}
if (!password.matches(".*[A-Z].*")) {
errors.add("密码必须包含大写字母");
}
if (!password.matches(".*[0-9].*")) {
errors.add("密码必须包含数字");
}
if (!password.matches(".*[@#$%^&+=].*")) {
errors.add("密码必须包含特殊字符");
}
if (passwordValidator.isCommonPassword(password)) {
errors.add("密码过于简单,请使用更复杂的密码");
}
return new PasswordValidationResult(errors.isEmpty(), errors);
}
public String encodePassword(String rawPassword) {
return passwordEncoder.encode(rawPassword);
}
public boolean matches(String rawPassword, String encodedPassword) {
return passwordEncoder.matches(rawPassword, encodedPassword);
}
public static class PasswordValidationResult {
private final boolean valid;
private final List<String> errors;
public PasswordValidationResult(boolean valid, List<String> errors) {
this.valid = valid;
this.errors = errors;
}
public boolean isValid() { return valid; }
public List<String> getErrors() { return errors; }
}
}
8. 会话管理
8.1 会话配置
package com.example.demo.config;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;
@Configuration
@EnableWebSecurity
public class SessionSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.and()
.sessionManagement()
.maximumSessions(1)
.maxSessionsPreventsLogin(false)
.expiredUrl("/login?expired=true")
.and()
.sessionFixation().migrateSession()
.invalidSessionUrl("/login?invalid=true");
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}
8.2 会话监听器
package com.example.demo.listener;
import org.springframework.context.event.EventListener;
import org.springframework.security.core.session.SessionDestroyedEvent;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class SessionEventListener {
@Autowired
private SessionRegistry sessionRegistry;
@EventListener
public void handleSessionDestroyed(SessionDestroyedEvent event) {
String sessionId = event.getId();
System.out.println("会话已销毁: " + sessionId);
// 清理会话相关的数据
cleanupSessionData(sessionId);
}
private void cleanupSessionData(String sessionId) {
// 清理会话相关的缓存、临时文件等
System.out.println("清理会话数据: " + sessionId);
}
public List<String> getActiveSessions() {
return sessionRegistry.getAllPrincipals().stream()
.map(principal -> sessionRegistry.getAllSessions(principal, false))
.flatMap(List::stream)
.map(sessionInformation -> sessionInformation.getSessionId())
.collect(java.util.stream.Collectors.toList());
}
}
9. CSRF保护
9.1 CSRF配置
package com.example.demo.config;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
@Configuration
@EnableWebSecurity
public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.and()
.csrf()
.csrfTokenRepository(csrfTokenRepository())
.ignoringAntMatchers("/api/public/**", "/api/webhook/**");
}
@Bean
public CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-CSRF-TOKEN");
return repository;
}
}
9.2 CSRF Token处理
package com.example.demo.controller;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
public class CsrfController {
@GetMapping("/api/csrf-token")
public CsrfToken getCsrfToken(HttpServletRequest request) {
return (CsrfToken) request.getAttribute("_csrf");
}
}
10. 安全监控
10.1 安全事件监听
package com.example.demo.listener;
import org.springframework.context.event.EventListener;
import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent;
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
import org.springframework.security.authentication.event.AuthenticationFailureLockedEvent;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class SecurityEventListener {
@EventListener
public void handleAuthenticationSuccess(AuthenticationSuccessEvent event) {
String username = event.getAuthentication().getName();
System.out.println("用户登录成功: " + username + " at " + LocalDateTime.now());
// 记录登录日志
logSecurityEvent("LOGIN_SUCCESS", username, "用户登录成功");
}
@EventListener
public void handleAuthenticationFailure(AuthenticationFailureBadCredentialsEvent event) {
String username = event.getAuthentication().getName();
System.out.println("用户登录失败: " + username + " at " + LocalDateTime.now());
// 记录失败日志
logSecurityEvent("LOGIN_FAILURE", username, "用户登录失败");
// 检查是否需要锁定账户
checkAccountLockout(username);
}
@EventListener
public void handleAccountLocked(AuthenticationFailureLockedEvent event) {
String username = event.getAuthentication().getName();
System.out.println("账户被锁定: " + username + " at " + LocalDateTime.now());
// 记录锁定日志
logSecurityEvent("ACCOUNT_LOCKED", username, "账户被锁定");
}
private void logSecurityEvent(String eventType, String username, String description) {
// 实现安全事件日志记录
System.out.println("安全事件: " + eventType + " - " + username + " - " + description);
}
private void checkAccountLockout(String username) {
// 实现账户锁定检查逻辑
System.out.println("检查账户锁定状态: " + username);
}
}
10.2 安全配置属性
# application.yml
spring:
security:
user:
name: admin
password: admin123
roles: ADMIN
# JWT配置
jwt:
secret: mySecretKey
expiration: 86400 # 24小时
# 密码策略
password:
policy:
min-length: 8
require-uppercase: true
require-lowercase: true
require-numbers: true
require-special-chars: true
max-attempts: 5
lockout-duration: 300 # 5分钟
# 会话配置
server:
servlet:
session:
timeout: 30m
cookie:
max-age: 1800
secure: false
http-only: true
11. 总结
Spring Boot安全框架提供了全面的安全解决方案:
- 认证机制:支持表单认证、JWT认证、OAuth2认证
- 授权控制:基于角色和权限的访问控制
- 密码安全:密码加密、密码策略、账户锁定
- 会话管理:会话超时、并发控制、会话固定攻击防护
- CSRF保护:跨站请求伪造防护
- 安全监控:安全事件监听、日志记录
- 方法级安全:细粒度的权限控制
通过合理配置这些安全特性,可以构建出安全可靠的Spring Boot应用。
下一篇:Spring Boot测试框架详解
1013

被折叠的 条评论
为什么被折叠?



