Ruoyi-vue-plus-5.x第五篇Spring框架核心技术:5.2 Spring Security集成
👋 大家好,我是 阿问学长
!专注于分享优质开源项目
解析、毕业设计项目指导
支持、幼小初高
的教辅资料
推荐等,欢迎关注交流!🚀
Spring Security集成
前言
Spring Security是Spring生态系统中的安全框架,提供了全面的安全服务。虽然RuoYi-Vue-Plus框架主要使用Sa-Token作为权限认证框架,但在某些场景下仍需要集成Spring Security来处理特定的安全需求。本文将详细介绍Spring Security的集成配置、安全配置与过滤器链、认证与授权机制、CSRF防护配置以及跨域请求处理等内容。
安全配置与过滤器链
基础安全配置
/*** Spring Security配置类*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig {private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);@Autowiredprivate CustomUserDetailsService userDetailsService;@Autowiredprivate CustomAuthenticationEntryPoint authenticationEntryPoint;@Autowiredprivate CustomAccessDeniedHandler accessDeniedHandler;@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;/*** 密码编码器*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 认证管理器*/@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {return config.getAuthenticationManager();}/*** 安全过滤器链配置*/@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http// 禁用CSRF(因为使用JWT).csrf().disable()// 禁用Session(使用JWT无状态认证).sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()// 配置异常处理.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler).and()// 配置请求授权.authorizeHttpRequests(authz -> authz// 允许匿名访问的路径.requestMatchers("/auth/login", "/auth/register", "/auth/captcha").permitAll().requestMatchers("/doc.html", "/swagger-ui/**", "/v3/api-docs/**").permitAll().requestMatchers("/actuator/**").permitAll().requestMatchers("/druid/**").permitAll().requestMatchers("/common/download**").permitAll().requestMatchers("/common/download/resource**").permitAll()// 静态资源.requestMatchers("/css/**", "/js/**", "/images/**", "/fonts/**", "/favicon.ico").permitAll()// 其他请求需要认证.anyRequest().authenticated())// 添加JWT过滤器.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)// 配置跨域.cors(cors -> cors.configurationSource(corsConfigurationSource()))// 配置头部安全.headers(headers -> headers.frameOptions().deny().contentTypeOptions().and().httpStrictTransportSecurity(hstsConfig -> hstsConfig.maxAgeInSeconds(31536000).includeSubdomains(true)));return http.build();}/*** 跨域配置源*/@Beanpublic CorsConfigurationSource corsConfigurationSource() {CorsConfiguration configuration = new CorsConfiguration();configuration.setAllowedOriginPatterns(Arrays.asList("*"));configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));configuration.setAllowedHeaders(Arrays.asList("*"));configuration.setAllowCredentials(true);configuration.setMaxAge(3600L);UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", configuration);return source;}/*** Web安全配置*/@Beanpublic WebSecurityCustomizer webSecurityCustomizer() {return (web) -> web.ignoring().requestMatchers("/error").requestMatchers("/static/**").requestMatchers("/public/**");}
}/*** JWT认证过滤器*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);@Autowiredprivate JwtTokenUtil jwtTokenUtil;@Autowiredprivate CustomUserDetailsService userDetailsService;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {String token = getTokenFromRequest(request);if (StringUtils.hasText(token) && jwtTokenUtil.validateToken(token)) {String username = jwtTokenUtil.getUsernameFromToken(token);if (StringUtils.hasText(username) && SecurityContextHolder.getContext().getAuthentication() == null) {UserDetails userDetails = userDetailsService.loadUserByUsername(username);if (jwtTokenUtil.validateToken(token, userDetails)) {UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authToken);log.debug("用户 {} 认证成功", username);}}}chain.doFilter(request, response);}/*** 从请求中获取Token*/private String getTokenFromRequest(HttpServletRequest request) {String bearerToken = request.getHeader("Authorization");if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {return bearerToken.substring(7);}return null;}
}/*** 自定义认证入口点*/
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {private static final Logger log = LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class);@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {log.warn("认证失败: {}", authException.getMessage());response.setStatus(HttpStatus.UNAUTHORIZED.value());response.setContentType(MediaType.APPLICATION_JSON_VALUE);response.setCharacterEncoding("UTF-8");R<Void> result = R.fail(HttpStatus.UNAUTHORIZED.value(), "认证失败,请重新登录");response.getWriter().write(JsonUtils.toJsonString(result));}
}/*** 自定义访问拒绝处理器*/
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {private static final Logger log = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {log.warn("访问被拒绝: {}", accessDeniedException.getMessage());response.setStatus(HttpStatus.FORBIDDEN.value());response.setContentType(MediaType.APPLICATION_JSON_VALUE);response.setCharacterEncoding("UTF-8");R<Void> result = R.fail(HttpStatus.FORBIDDEN.value(), "访问被拒绝,权限不足");response.getWriter().write(JsonUtils.toJsonString(result));}
}
自定义过滤器
/*** 请求日志过滤器*/
@Component
@Order(1)
public class RequestLoggingFilter extends OncePerRequestFilter {private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {long startTime = System.currentTimeMillis();String requestId = UUID.randomUUID().toString().replace("-", "");// 设置请求ID到MDCMDC.put("requestId", requestId);try {// 记录请求信息logRequest(request, requestId);filterChain.doFilter(request, response);// 记录响应信息logResponse(request, response, requestId, startTime);} finally {MDC.clear();}}private void logRequest(HttpServletRequest request, String requestId) {log.info("请求开始 - ID: {}, Method: {}, URI: {}, IP: {}", requestId, request.getMethod(), request.getRequestURI(), getClientIp(request));}private void logResponse(HttpServletRequest request, HttpServletResponse response, String requestId, long startTime) {long duration = System.currentTimeMillis() - startTime;log.info("请求结束 - ID: {}, Status: {}, Duration: {}ms", requestId, response.getStatus(), duration);}private String getClientIp(HttpServletRequest request) {String xForwardedFor = request.getHeader("X-Forwarded-For");if (StringUtils.hasText(xForwardedFor)) {return xForwardedFor.split(",")[0].trim();}String xRealIp = request.getHeader("X-Real-IP");if (StringUtils.hasText(xRealIp)) {return xRealIp;}return request.getRemoteAddr();}
}/*** 安全头部过滤器*/
@Component
@Order(2)
public class SecurityHeadersFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 添加安全头部response.setHeader("X-Content-Type-Options", "nosniff");response.setHeader("X-Frame-Options", "DENY");response.setHeader("X-XSS-Protection", "1; mode=block");response.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");response.setHeader("Permissions-Policy", "geolocation=(), microphone=(), camera=()");// HTTPS环境下添加HSTS头部if (request.isSecure()) {response.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");}filterChain.doFilter(request, response);}
}/*** 限流过滤器*/
@Component
@Order(3)
public class RateLimitFilter extends OncePerRequestFilter {private static final Logger log = LoggerFactory.getLogger(RateLimitFilter.class);@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Value("${security.rate-limit.enabled:true}")private boolean rateLimitEnabled;@Value("${security.rate-limit.max-requests:100}")private int maxRequests;@Value("${security.rate-limit.window-seconds:60}")private int windowSeconds;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {if (!rateLimitEnabled) {filterChain.doFilter(request, response);return;}String clientIp = getClientIp(request);String key = "rate_limit:" + clientIp;try {Long currentRequests = redisTemplate.opsForValue().increment(key);if (currentRequests == 1) {redisTemplate.expire(key, Duration.ofSeconds(windowSeconds));}if (currentRequests > maxRequests) {handleRateLimitExceeded(response, clientIp);return;}} catch (Exception e) {log.error("限流检查失败", e);}filterChain.doFilter(request, response);}private void handleRateLimitExceeded(HttpServletResponse response, String clientIp) throws IOException {log.warn("IP {} 请求频率超过限制", clientIp);response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());response.setContentType(MediaType.APPLICATION_JSON_VALUE);response.setCharacterEncoding("UTF-8");R<Void> result = R.fail(HttpStatus.TOO_MANY_REQUESTS.value(), "请求频率过高,请稍后再试");response.getWriter().write(JsonUtils.toJsonString(result));}private String getClientIp(HttpServletRequest request) {String xForwardedFor = request.getHeader("X-Forwarded-For");if (StringUtils.hasText(xForwardedFor)) {return xForwardedFor.split(",")[0].trim();}String xRealIp = request.getHeader("X-Real-IP");if (StringUtils.hasText(xRealIp)) {return xRealIp;}return request.getRemoteAddr();}
}
认证与授权机制
用户详情服务
/*** 自定义用户详情服务*/
@Service
public class CustomUserDetailsService implements UserDetailsService {private static final Logger log = LoggerFactory.getLogger(CustomUserDetailsService.class);@Autowiredprivate ISysUserService userService;@Autowiredprivate ISysRoleService roleService;@Autowiredprivate ISysMenuService menuService;@Override@Transactional(readOnly = true)public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {SysUser user = userService.selectUserByUserName(username);if (user == null) {log.warn("用户不存在: {}", username);throw new UsernameNotFoundException("用户不存在: " + username);}if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {log.warn("用户已被禁用: {}", username);throw new DisabledException("用户已被禁用: " + username);}return createUserDetails(user);}/*** 创建用户详情对象*/private UserDetails createUserDetails(SysUser user) {Set<String> permissions = getPermissions(user);Set<GrantedAuthority> authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());return CustomUserPrincipal.builder().userId(user.getUserId()).username(user.getUserName()).password(user.getPassword()).email(user.getEmail()).phone(user.getPhonenumber()).enabled(UserStatus.OK.getCode().equals(user.getStatus())).accountNonExpired(true).accountNonLocked(true).credentialsNonExpired(true).authorities(authorities).build();}/*** 获取用户权限*/private Set<String> getPermissions(SysUser user) {Set<String> permissions = new HashSet<>();// 管理员拥有所有权限if (user.isAdmin()) {permissions.add("*:*:*");} else {// 获取角色权限List<SysRole> roles = roleService.selectRolesByUserId(user.getUserId());for (SysRole role : roles) {permissions.add("ROLE_" + role.getRoleKey());}// 获取菜单权限Set<String> menuPermissions = menuService.selectMenuPermsByUserId(user.getUserId());permissions.addAll(menuPermissions);}return permissions;}
}/*** 自定义用户主体*/
@Data
@Builder
public class CustomUserPrincipal implements UserDetails {private Long userId;private String username;private String password;private String email;private String phone;private boolean enabled;private boolean accountNonExpired;private boolean accountNonLocked;private boolean credentialsNonExpired;private Collection<? extends GrantedAuthority> authorities;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return authorities;}@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return username;}@Overridepublic boolean isAccountNonExpired() {return accountNonExpired;}@Overridepublic boolean isAccountNonLocked() {return accountNonLocked;}@Overridepublic boolean isCredentialsNonExpired() {return credentialsNonExpired;}@Overridepublic boolean isEnabled() {return enabled;}
}
认证提供者
/*** 自定义认证提供者*/
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {private static final Logger log = LoggerFactory.getLogger(CustomAuthenticationProvider.class);@Autowiredprivate CustomUserDetailsService userDetailsService;@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate LoginAttemptService loginAttemptService;@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {String username = authentication.getName();String password = authentication.getCredentials().toString();// 检查登录尝试次数if (loginAttemptService.isBlocked(username)) {throw new AccountLockedException("账户已被锁定,请稍后再试");}try {UserDetails userDetails = userDetailsService.loadUserByUsername(username);if (!passwordEncoder.matches(password, userDetails.getPassword())) {loginAttemptService.recordFailedAttempt(username);throw new BadCredentialsException("用户名或密码错误");}// 登录成功,清除失败记录loginAttemptService.clearFailedAttempts(username);return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());} catch (UsernameNotFoundException e) {loginAttemptService.recordFailedAttempt(username);throw new BadCredentialsException("用户名或密码错误");}}@Overridepublic boolean supports(Class<?> authentication) {return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);}
}/*** 登录尝试服务*/
@Service
public class LoginAttemptService {private static final Logger log = LoggerFactory.getLogger(LoginAttemptService.class);@Autowiredprivate RedisTemplate<String, Object> redisTemplate;private static final int MAX_ATTEMPTS = 5;private static final Duration LOCK_DURATION = Duration.ofMinutes(15);/*** 记录失败尝试*/public void recordFailedAttempt(String username) {String key = "login_attempts:" + username;try {Long attempts = redisTemplate.opsForValue().increment(key);if (attempts == 1) {redisTemplate.expire(key, LOCK_DURATION);}if (attempts >= MAX_ATTEMPTS) {log.warn("用户 {} 登录失败次数达到上限,账户已被锁定", username);}} catch (Exception e) {log.error("记录登录失败尝试失败", e);}}/*** 检查是否被锁定*/public boolean isBlocked(String username) {String key = "login_attempts:" + username;try {Integer attempts = (Integer) redisTemplate.opsForValue().get(key);return attempts != null && attempts >= MAX_ATTEMPTS;} catch (Exception e) {log.error("检查账户锁定状态失败", e);return false;}}/*** 清除失败尝试记录*/public void clearFailedAttempts(String username) {String key = "login_attempts:" + username;try {redisTemplate.delete(key);} catch (Exception e) {log.error("清除登录失败记录失败", e);}}
}
方法级安全
/*** 方法级安全示例*/
@RestController
@RequestMapping("/api/admin")
@PreAuthorize("hasRole('ADMIN')")
public class AdminController {/*** 需要管理员角色*/@GetMapping("/users")@PreAuthorize("hasRole('ADMIN')")public R<List<SysUser>> getUsers() {// 实现逻辑return R.ok();}/*** 需要特定权限*/@PostMapping("/users")@PreAuthorize("hasAuthority('system:user:add')")public R<Void> addUser(@RequestBody SysUser user) {// 实现逻辑return R.ok();}/*** 需要多个权限之一*/@PutMapping("/users/{id}")@PreAuthorize("hasAnyAuthority('system:user:edit', 'system:user:admin')")public R<Void> updateUser(@PathVariable Long id, @RequestBody SysUser user) {// 实现逻辑return R.ok();}/*** 复杂权限表达式*/@DeleteMapping("/users/{id}")@PreAuthorize("hasRole('ADMIN') and hasAuthority('system:user:remove') and #id != authentication.principal.userId")public R<Void> deleteUser(@PathVariable Long id) {// 实现逻辑return R.ok();}/*** 基于返回值的权限控制*/@GetMapping("/users/{id}")@PostAuthorize("returnObject.data.userId == authentication.principal.userId or hasRole('ADMIN')")public R<SysUser> getUser(@PathVariable Long id) {// 实现逻辑return R.ok();}
}/*** 自定义权限表达式*/
@Component("customSecurity")
public class CustomSecurityExpressionMethods {/*** 检查是否为数据所有者*/public boolean isOwner(Authentication authentication, Long resourceOwnerId) {if (authentication == null || !authentication.isAuthenticated()) {return false;}CustomUserPrincipal principal = (CustomUserPrincipal) authentication.getPrincipal();return Objects.equals(principal.getUserId(), resourceOwnerId);}/*** 检查是否有部门权限*/public boolean hasDeptPermission(Authentication authentication, Long deptId) {if (authentication == null || !authentication.isAuthenticated()) {return false;}// 实现部门权限检查逻辑return true;}/*** 检查是否在工作时间*/public boolean isWorkingHours() {LocalTime now = LocalTime.now();return now.isAfter(LocalTime.of(9, 0)) && now.isBefore(LocalTime.of(18, 0));}
}/*** 使用自定义权限表达式*/
@RestController
@RequestMapping("/api/data")
public class DataController {/*** 只能访问自己的数据*/@GetMapping("/my-data/{id}")@PreAuthorize("@customSecurity.isOwner(authentication, #id)")public R<Object> getMyData(@PathVariable Long id) {// 实现逻辑return R.ok();}/*** 需要部门权限*/@GetMapping("/dept-data/{deptId}")@PreAuthorize("@customSecurity.hasDeptPermission(authentication, #deptId)")public R<Object> getDeptData(@PathVariable Long deptId) {// 实现逻辑return R.ok();}/*** 只能在工作时间访问*/@GetMapping("/sensitive-data")@PreAuthorize("hasRole('ADMIN') and @customSecurity.isWorkingHours()")public R<Object> getSensitiveData() {// 实现逻辑return R.ok();}
}
CSRF防护配置
CSRF配置
/*** CSRF防护配置*/
@Configuration
public class CsrfConfig {/*** CSRF Token仓库*/@Beanpublic CsrfTokenRepository csrfTokenRepository() {HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();repository.setHeaderName("X-CSRF-TOKEN");repository.setParameterName("_csrf");return repository;}/*** CSRF请求匹配器*/@Beanpublic RequestMatcher csrfRequestMatcher() {return new CsrfRequestMatcher();}/*** 自定义CSRF请求匹配器*/private static class CsrfRequestMatcher implements RequestMatcher {private final Pattern allowedMethods = Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$");private final List<String> excludedPaths = Arrays.asList("/api/auth/login","/api/auth/logout","/api/public/**");@Overridepublic boolean matches(HttpServletRequest request) {// 允许的HTTP方法不需要CSRF保护if (allowedMethods.matcher(request.getMethod()).matches()) {return false;}// 排除的路径不需要CSRF保护String requestPath = request.getRequestURI();for (String excludedPath : excludedPaths) {if (requestPath.matches(excludedPath.replace("**", ".*"))) {return false;}}return true;}}
}/*** CSRF Token控制器*/
@RestController
@RequestMapping("/api/csrf")
public class CsrfController {/*** 获取CSRF Token*/@GetMapping("/token")public R<Map<String, String>> getCsrfToken(HttpServletRequest request) {CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());if (csrfToken != null) {Map<String, String> tokenInfo = new HashMap<>();tokenInfo.put("token", csrfToken.getToken());tokenInfo.put("headerName", csrfToken.getHeaderName());tokenInfo.put("parameterName", csrfToken.getParameterName());return R.ok(tokenInfo);}return R.fail("CSRF Token不可用");}
}
跨域请求处理
跨域配置
/*** 跨域配置类*/
@Configuration
public class CorsConfig {/*** 跨域配置*/@Beanpublic CorsConfigurationSource corsConfigurationSource() {CorsConfiguration configuration = new CorsConfiguration();// 允许的源configuration.setAllowedOriginPatterns(Arrays.asList("*"));// 允许的方法configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));// 允许的头部configuration.setAllowedHeaders(Arrays.asList("*"));// 允许携带凭证configuration.setAllowCredentials(true);// 预检请求缓存时间configuration.setMaxAge(3600L);// 暴露的头部configuration.setExposedHeaders(Arrays.asList("Authorization", "X-Total-Count", "X-Request-Id"));UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", configuration);return source;}/*** 跨域过滤器*/@Beanpublic CorsFilter corsFilter() {return new CorsFilter(corsConfigurationSource());}
}/*** Web MVC跨域配置*/
@Configuration
public class WebMvcCorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOriginPatterns("*").allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS").allowedHeaders("*").allowCredentials(true).maxAge(3600);}
}
总结
本文详细介绍了Spring Security集成,包括:
- 安全配置与过滤器链:基础安全配置、自定义过滤器、JWT认证过滤器
- 认证与授权机制:用户详情服务、认证提供者、方法级安全
- CSRF防护配置:CSRF配置、Token管理、请求匹配器
- 跨域请求处理:跨域配置、CORS过滤器、Web MVC配置
这些配置为应用提供了完整的安全防护能力,可以与Sa-Token配合使用,提供更全面的安全保障。
在下一篇文章中,我们将探讨Spring AOP切面编程。
参考资料
- Spring Security官方文档
- Spring Security架构指南
- CORS配置指南