前言
安全性,是现代Web应用开发中绕不开的核心命题。Spring Security作为Spring生态里最成熟、应用最广泛的安全框架,为开发者提供了强大的认证(Authentication)和授权(Authorization)能力。其中,接口级别的URL权限控制,是实现细粒度访问控制的关键一环。
这篇文章会带着大家,在Spring Boot项目中,一步步把Spring Security的接口权限控制从基础做到高级。不玩虚的,全程可运行的Ja va代码,从零开始搭一个权限体系完善的RESTful API应用。
为什么非要做到接口级别的URL权限控制?
先问一个问题:在传统Web应用里,权限控制往往停留在页面或菜单级别,够用吗?答案是不够用——尤其是在微服务架构和前后端分离成为主流的今天。后端以RESTful API的方式提供服务,前端调用接口拿数据,页面级别的权限控制已经完全跟不上节奏,必须对每个API接口做精确的权限管理。
举个很实际的例子:
- 普通用户,只能看自己的订单信息
- 管理员,能看所有用户的订单
- 财务人员,能导出销售报表,但不能改用户信息
这些需求,都要求我们在接口层面做权限判断,确保只有具备对应权限的用户才能访问特定URL路径。
Spring Security的核心概念,先捋清楚
在写代码之前,有必要先理解几个关键概念:
认证(Authentication)
验证用户身份,通常就是用户名+密码。认证成功后,Spring Security会创建一个Authentication对象,里面装着用户的身份信息和权限列表。
授权(Authorization)
用户身份确认后,决定他有没有权访问某个资源。授权可以基于角色(Role)、权限(Authority),或者自定义的逻辑。
SecurityContext
存储当前用户安全上下文的大容器,通过SecurityContextHolder.getContext()就能获取。里面装着当前用户的Authentication对象。
AccessDecisionManager
最终决定权在它手里。它会调用多个AccessDecisionVoter投票,根据投票结果决定放行还是拒绝。

先搭一个基础项目
从一个最简单的Spring Boot项目开始。先把必要的依赖加上:
org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-data-jpa com.h2database h2 runtime
创建一个简单的用户实体类:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String username;
private String password;
private String roles; // 例如: "ROLE_USER,ROLE_ADMIN"
// 构造函数、getter、setter 省略
}
对应的Repository:
@Repository public interface UserRepository extends JpaRepository{ Optional findByUsername(String username); }
用内存做一次简单的权限配置
最粗暴的URL权限控制方式,就是用HttpSecurity的authorizeHttpRequests()方法。先搞一个基础的安全配置类:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
这段配置告诉Spring Security:
/public/**路径完全开放,谁都能进/admin/**路径,只有ROLE_ADMIN角色的用户才能进/user/**路径,ROLE_USER或ROLE_ADMIN都能进- 其他所有请求,至少得先认证通过
这里有个细节:Spring Security会自动给角色名称加上ROLE_前缀,所以数据库里存的应该是ROLE_ADMIN,而不是ADMIN。
配套的控制器:
@RestController
public class DemoController {
@GetMapping("/public/hello")
public String publicHello() {
return "Hello, Public!";
}
@GetMapping("/user/profile")
public String userProfile(Authentication authentication) {
return "Hello, " + authentication.getName() + "! This is your profile.";
}
@GetMapping("/admin/dashboard")
public String adminDashboard() {
return "Admin Dashboard - Welcome!";
}
}
把权限配置放到数据库里,动态加载
硬编码权限在实际项目中太僵化了。真正灵活的做法,是从数据库里动态加载权限规则。这就需要重新设计权限相关的实体类。
先重构用户模型,用更规范的多对多关系:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String username;
private String password;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set roles = new HashSet<>();
// 构造函数、getter、setter 省略
}
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String name; // 例如: "ROLE_ADMIN"
@ManyToMany(mappedBy = "roles")
private Set users = new HashSet<>();
// 构造函数、getter、setter 省略
}
@Entity
@Table(name = "permissions")
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String urlPattern; // 例如: "/api/admin/**"
private String requiredRole; // 例如: "ROLE_ADMIN"
// 构造函数、getter、setter 省略
}
创建对应的Repository:
@Repository public interface PermissionRepository extends JpaRepository{ List findAll(); }
接下来,需要写一个自定义的RequestMatcher,能从数据库加载权限规则并进行匹配:
@Component public class DatabaseUrlAuthorizationManager implements AuthorizationManager{ @Autowired private PermissionRepository permissionRepository; // 缓存权限规则,避免每次请求都查数据库 private volatile List cachedPermissions = null; private final Object cacheLock = new Object(); @Override public AuthorizationDecision check(Supplier authentication, RequestAuthorizationContext context) { HttpServletRequest request = context.getRequest(); String requestURI = request.getRequestURI(); // 加载权限规则(带缓存) List permissions = loadPermissions(); // 查找匹配的权限规则 for (Permission permission : permissions) { if (pathMatches(permission.getUrlPattern(), requestURI)) { // 检查用户是否具有所需角色 Authentication auth = authentication.get(); if (auth != null && hasRole(auth, permission.getRequiredRole())) { return new AuthorizationDecision(true); } return new AuthorizationDecision(false); } } // 如果没有匹配的规则,默认允许已认证用户访问 return new AuthorizationDecision(authentication.get() != null); } private List loadPermissions() { if (cachedPermissions == null) { synchronized (cacheLock) { if (cachedPermissions == null) { cachedPermissions = permissionRepository.findAll(); } } } return cachedPermissions; } private boolean pathMatches(String pattern, String path) { // 简单的通配符匹配实现 if (pattern.equals(path)) { return true; } if (pattern.endsWith("/**")) { String prefix = pattern.substring(0, pattern.length() - 3); return path.startsWith(prefix); } if (pattern.endsWith("/*")) { String prefix = pattern.substring(0, pattern.length() - 2); return path.startsWith(prefix) && path.indexOf('/', prefix.length()) == -1; } return false; } private boolean hasRole(Authentication auth, String requiredRole) { Collection extends GrantedAuthority> authorities = auth.getAuthorities(); return authorities.stream() .anyMatch(authority -> authority.getAuthority().equals(requiredRole)); } }
更新安全配置类,把自定义授权管理器接进去:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private DatabaseUrlAuthorizationManager urlAuthorizationManager;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.anyRequest().access(urlAuthorizationManager)
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 添加缓存清理方法,用于权限变更时刷新缓存
public void clearPermissionCache() {
urlAuthorizationManager.clearCache();
}
}
在DatabaseUrlAuthorizationManager里加上缓存清理方法:
public void clearCache() {
synchronized (cacheLock) {
this.cachedPermissions = null;
}
}
实现用户详情服务
要让Spring Security能正确加载用户信息和权限,需要实现UserDetailsService接口:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList()))
.build();
}
}
在安全配置里把UserDetailsService注入进来:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private DatabaseUrlAuthorizationManager urlAuthorizationManager;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.anyRequest().access(urlAuthorizationManager)
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.permitAll()
);
return http.build();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig)
throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
基于表达式的高级权限控制
Spring Security支持用SpEL(Spring Expression Language)做更复杂的权限表达式。通过@PreAuthorize注解,可以在方法级别控制权限。
先在主配置类上启用方法级安全:
@SpringBootApplication
@EnableMethodSecurity(prePostEnabled = true)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
然后在控制器方法上直接加注解:
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/user/{id}")
@PreAuthorize("@securityService.canAccessUser(principal, #id)")
public ResponseEntity getUser(@PathVariable Long id) {
// 返回用户信息
return ResponseEntity.ok(new UserDto());
}
@PostMapping("/order")
@PreAuthorize("hasRole('USER') and #order.userId == principal.id")
public ResponseEntity createOrder(@RequestBody Order order) {
// 创建订单
return ResponseEntity.ok(order);
}
@DeleteMapping("/user/{id}")
@PreAuthorize("hasRole('ADMIN') or (hasRole('USER') and #id == principal.id)")
public ResponseEntity deleteUser(@PathVariable Long id) {
// 删除用户
return ResponseEntity.noContent().build();
}
}
再写一个安全服务类,处理那些复杂的业务逻辑:
@Service
public class SecurityService {
@Autowired
private UserRepository userRepository;
public boolean canAccessUser(UserDetails userDetails, Long userId) {
if (userDetails.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
return true;
}
User user = userRepository.findByUsername(userDetails.getUsername()).orElse(null);
return user != null && user.getId().equals(userId);
}
}
这么做的好处是,可以把复杂的业务逻辑封装在服务方法里,权限控制变得既灵活又好维护。
自定义权限决策投票器
对于更复杂的场景,可以自己实现AccessDecisionVoter。这样能在多个投票器之间协调,实现更精细的控制。
先写一个自定义投票器:
@Component public class CustomPermissionVoter implements AccessDecisionVoter{ @Autowired private PermissionRepository permissionRepository; @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } @Override public int vote(Authentication authentication, FilterInvocation fi, Collection attributes) { HttpServletRequest request = fi.getRequest(); String requestURI = request.getRequestURI(); // 从数据库查找匹配的权限规则 List permissions = permissionRepository.findAll(); for (Permission permission : permissions) { if (pathMatches(permission.getUrlPattern(), requestURI)) { String requiredRole = permission.getRequiredRole(); if (hasRole(authentication, requiredRole)) { return ACCESS_GRANTED; } else { return ACCESS_DENIED; } } } // 如果没有匹配的规则,弃权 return ACCESS_ABSTAIN; } private boolean pathMatches(String pattern, String path) { // 同之前的实现 if (pattern.equals(path)) { return true; } if (pattern.endsWith("/**")) { String prefix = pattern.substring(0, pattern.length() - 3); return path.startsWith(prefix); } if (pattern.endsWith("/*")) { String prefix = pattern.substring(0, pattern.length() - 2); return path.startsWith(prefix) && path.indexOf('/', prefix.length()) == -1; } return false; } private boolean hasRole(Authentication auth, String requiredRole) { if (auth == null || !auth.isAuthenticated()) { return false; } Collection extends GrantedAuthority> authorities = auth.getAuthorities(); return authorities.stream() .anyMatch(authority -> authority.getAuthority().equals(requiredRole)); } }
然后配置一个自定义的AccessDecisionManager:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private CustomPermissionVoter customPermissionVoter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.permitAll()
);
return http.build();
}
@Bean
public AccessDecisionManager accessDecisionManager() {
List> decisionVoters = Arrays.asList(
new WebExpressionVoter(),
customPermissionVoter
);
return new UnanimousBased(decisionVoters);
}
// 其他配置...
}

处理权限异常
当用户访问未授权资源时,Spring Security会抛出AccessDeniedException。得给它配置一个友好的错误响应。
创建全局异常处理器:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity handleAccessDeniedException(AccessDeniedException ex) {
ErrorResponse error = new ErrorResponse(
"ACCESS_DENIED",
"You don't ha ve permission to access this resource",
HttpStatus.FORBIDDEN.value()
);
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
}
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity handleAuthenticationException(AuthenticationException ex) {
ErrorResponse error = new ErrorResponse(
"AUTHENTICATION_FAILED",
"Authentication failed",
HttpStatus.UNAUTHORIZED.value()
);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
}
}
class ErrorResponse {
private String code;
private String message;
private int status;
// 构造函数、getter、setter
public ErrorResponse(String code, String message, int status) {
this.code = code;
this.message = message;
this.status = status;
}
}
对于RESTful API,还得让Spring Security返回JSON格式的错误响应,而不是跳转到登录页面:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(exception -> exception
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json");
response.getWriter().write(
"{"error":"Unauthorized","message":"Authentication required"}"
);
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType("application/json");
response.getWriter().write(
"{"error":"Forbidden","message":"Access denied"}"
);
})
)
.csrf(csrf -> csrf.disable()) // 对于 REST API,通常禁用 CSRF
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
// 其他配置...
}
JWT集成与无状态权限控制
在前后端分离的项目里,JWT是常用的认证机制。看看怎么把JWT和Spring Security的URL权限控制整合。
先加JWT依赖:
io.jsonwebtoken jjwt-api 0.11.5 io.jsonwebtoken jjwt-impl 0.11.5 runtime io.jsonwebtoken jjwt-jackson 0.11.5 runtime
创建JWT工具类:
@Component
public class JwtUtil {
private String secret = "mySecretKey"; // 实际项目中应该从配置文件读取
private int jwtExpirationMs = 86400000; // 24小时
public String generateToken(UserDetails userDetails) {
Map claims = new HashMap<>();
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String getUsernameFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody().getSubject();
}
public boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private boolean isTokenExpired(String token) {
final Date expiration = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody().getExpiration();
return expiration.before(new Date());
}
}
创建JWT认证过滤器:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@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;
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
logger.error("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
logger.error("JWT Token has expired");
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.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);
}
}
更新安全配置,把JWT接进来:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private DatabaseUrlAuthorizationManager urlAuthorizationManager;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(authz -> authz
.requestMatchers("/auth/login", "/public/**").permitAll()
.anyRequest().access(urlAuthorizationManager)
)
.exceptionHandling(exception -> exception
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json");
response.getWriter().write(
"{"error":"Unauthorized","message":"Authentication required"}"
);
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType("application/json");
response.getWriter().write(
"{"error":"Forbidden","message":"Access denied"}"
);
})
);
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
// 其他配置...
}
创建认证控制器:
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public ResponseEntity> login(@RequestBody LoginRequest loginRequest) {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
} catch (BadCredentialsException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("INVALID_CREDENTIALS", "Invalid username or password", 401));
}
final UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.getUsername());
final String token = jwtUtil.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(token));
}
}
class LoginRequest {
private String username;
private String password;
// getter、setter
}
class JwtResponse {
private String token;
// 构造函数、getter、setter
public JwtResponse(String token) {
this.token = token;
}
}
性能优化与缓存策略
高并发场景下,频繁查数据库做权限验证会影响性能。得用缓存来优化。
权限规则缓存
之前在DatabaseUrlAuthorizationManager里已经做了简单缓存,还可以进一步优化:
@Component public class DatabaseUrlAuthorizationManager implements AuthorizationManager{ @Autowired private PermissionRepository permissionRepository; // 使用 Caffeine 缓存(需要添加依赖) private final Cache > permissionCache = Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .build(); private static final String CACHE_KEY = "all_permissions"; @Override public AuthorizationDecision check(Supplier authentication, RequestAuthorizationContext context) { HttpServletRequest request = context.getRequest(); String requestURI = request.getRequestURI(); List permissions = permissionCache.get(CACHE_KEY, k -> permissionRepository.findAll()); // 匹配逻辑... } public void clearCache() { permissionCache.invalidate(CACHE_KEY); } }
别忘了加Caffeine依赖:
com.github.ben-manes.caffeine caffeine
用户权限缓存
用户的角色和权限信息同样可以缓存:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
private final Cache userCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.build();
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userCache.get(username, key -> {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList()))
.build();
});
}
public void clearUserCache(String username) {
userCache.invalidate(username);
}
}
测试权限控制
写几个单元测试来验证权限控制逻辑:
@SpringBootTest
@AutoConfigureTestDatabase
@Transactional
class SecurityIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Test
void testPublicEndpointAccessibleWithoutAuth() {
ResponseEntity response = restTemplate.getForEntity("/public/hello", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
void testProtectedEndpointRequiresAuth() {
ResponseEntity response = restTemplate.getForEntity("/user/profile", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
@WithMockUser(username = "admin", roles = {"ADMIN"})
void testAdminEndpointWithAdminRole() {
ResponseEntity response = restTemplate.getForEntity("/admin/dashboard", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
@WithMockUser(username = "user", roles = {"USER"})
void testAdminEndpointWithoutAdminRole() {
ResponseEntity response = restTemplate.getForEntity("/admin/dashboard", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
}
@BeforeEach
void setupUsers() {
// 创建测试用户
User admin = new User();
admin.setUsername("admin");
admin.setPassword(passwordEncoder.encode("password"));
admin.setRoles(Set.of(new Role("ROLE_ADMIN")));
userRepository.sa ve(admin);
User user = new User();
user.setUsername("user");
user.setPassword(passwordEncoder.encode("password"));
user.setRoles(Set.of(new Role("ROLE_USER")));
userRepository.sa ve(user);
}
}
最佳实践与安全建议
1. 最小权限原则
始终遵循最小权限原则,只给用户完成工作所需的最小权限集。这样能减少安全漏洞的影响范围。
2. 定期审计权限配置
定期审查和审计权限配置,确保没有过度授权。可以建立权限变更的审批流程。
3. 使用HTTPS
生产环境中,始终用HTTPS保护认证凭据和敏感数据的传输。Spring Security里可以强制使用HTTPS:
http.requiresChannel(channel -> channel
.requestMatchers(r -> r.getHeader("X-Forwarded-Proto") != null)
.requiresSecure());
4. 防止权限提升攻击
确保权限检查逻辑不会被绕过。比如更新用户信息时,不光要检查用户是否有编辑权限,还要验证他是不是在编辑自己的信息(除非是管理员)。
5. 日志记录与监控
记录所有权限相关的事件,包括成功的访问和被拒绝的尝试。这对安全审计和异常检测非常有帮助。
@Component
public class SecurityEventListener {
private static final Logger logger = LoggerFactory.getLogger(SecurityEventListener.class);
@EventListener
public void handleAuthenticationSuccess(AuthenticationSuccessEvent event) {
logger.info("User {} authenticated successfully", event.getAuthentication().getName());
}
@EventListener
public void handleAccessDenied(AuthorizationDeniedEvent> event) {
Authentication auth = event.getAuthentication();
String username = auth != null ? auth.getName() : "anonymous";
logger.warn("Access denied for user {} to resource {}", username, event.getObject());
}
}
高级主题:动态权限更新
实际应用中,权限配置可能需要在运行时动态更新。得确保权限变更能及时生效。
创建一个权限管理服务:
@Service
@Transactional
public class PermissionManagementService {
@Autowired
private PermissionRepository permissionRepository;
@Autowired
private DatabaseUrlAuthorizationManager authorizationManager;
@Autowired
private CustomUserDetailsService userDetailsService;
public Permission createPermission(String urlPattern, String requiredRole) {
Permission permission = new Permission();
permission.setUrlPattern(urlPattern);
permission.setRequiredRole(requiredRole);
Permission sa ved = permissionRepository.sa ve(permission);
authorizationManager.clearCache(); // 清除权限缓存
return sa ved;
}
public void updatePermission(Long id, String urlPattern, String requiredRole) {
Permission permission = permissionRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Permission not found"));
permission.setUrlPattern(urlPattern);
permission.setRequiredRole(requiredRole);
permissionRepository.sa ve(permission);
authorizationManager.clearCache();
}
public void deletePermission(Long id) {
permissionRepository.deleteById(id);
authorizationManager.clearCache();
}
public void updateUserRoles(Long userId, Set roleNames) {
// 更新用户角色逻辑
// 清除用户缓存
User user = userRepository.findById(userId).orElse(null);
if (user != null) {
userDetailsService.clearUserCache(user.getUsername());
}
}
}
与外部系统的集成
企业环境里,权限系统可能得跟LDAP、OAuth2或SAML等外部系统集成。
OAuth2集成示例
如果用OAuth2做认证,配置会有些不同:
@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
return http.build();
}
private JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter =
new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthorityPrefix("ROLE_");
authoritiesConverter.setAuthoritiesClaimName("roles");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}
}
总结
Spring Security的接口级别URL权限控制,功能强大且灵活,能应对从简单到复杂的各种安全需求。通过这篇文章和配套的代码示例,我们梳理了以下关键内容:
- 基础配置:用
HttpSecurity做简单的URL权限控制 - 动态权限:从数据库加载权限规则,实现灵活管理
- 方法级安全:用
@PreAuthorize注解做细粒度控制 - 自定义投票器:实现复杂的权限决策逻辑
- JWT集成:支持无状态的RESTful API安全
- 性能优化:通过缓存提升权限验证性能
- 最佳实践:安全开发的重要原则和建议
实际项目里选哪种权限控制策略,得看业务需求、系统架构和安全要求。但不管选哪种方式,安全第一的原则不能动摇,定期做安全审计和测试是必须的。
合理运用Spring Security提供的各种功能,就能构建出既安全又灵活的权限控制系统,为用户提供可靠的保护。
