java开发发问URL匹配问题?

数据库有一个权限表t_permission

idurl
1/list
2/list/{id}
3/item
4/item/{id}
5/list/test/{id}
6/list/{type}/{id}
7/list/test2/3

所有url都是对应 spring boot 3.5.0 @PostMapping
访问前先进拦截器,查看有没有访问权限,获取请求url,然后根据url去查是否有匹配的,现在静态的1,3,7直接查询是否相等就可以了,对于动态2,4,5,6怎么匹配?

比如访问链接是/list/test/1,
是不是先查看/list/test/1有没有,
没有再查/list/test/{id},
没有再查/list/{id}/{id},
没有再查/{id}/{id}/{id}
...

这样查下去感觉组合好多种呀?具体要怎么做,spring boot 3.5.0 @PostMapping 注解是怎么匹配的?

还是说我这思路错了?,或者数据库url路径不是这么保存的?

现在我实现的方案就是全部用静态路劲,参数传递都是 @RequestParam@RequestBody,对于动态地址,不知道怎么搞?

这个只是权限表一个简单说明,实际用户角色关联权限,用户访问根据用户idurl查权限表,现在主要是这个URL查询搞不定。

高手指点一下。



完整代码库:https://gitee.com/kaipizhe/kaipizhe-test

SecurityConfig.java

package com.kaipizhe.config;

import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
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.configurers.AbstractHttpConfigurer;
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.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
import java.util.List;

@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class SecurityConfig {

    @Value("${jwt.public.key}")
    RSAPublicKey key;

    @Value("${jwt.private.key}")
    RSAPrivateKey priv;

    private final CustomAuthorizationManager customAuthorizationManager;
    public SecurityConfig(CustomAuthorizationManager customAuthorizationManager) {
        this.customAuthorizationManager = customAuthorizationManager;
    }

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http
                .httpBasic(AbstractHttpConfigurer::disable)
                .csrf(AbstractHttpConfigurer::disable)
                .logout(AbstractHttpConfigurer::disable)
                .cors(Customizer.withDefaults())
                .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests((authorize) -> authorize
                        .requestMatchers("/auth/**").permitAll()
                        .requestMatchers("/api/**").access(customAuthorizationManager)
                        .anyRequest().authenticated()
                )
                .oauth2ResourceServer(oauth2 -> {
                    oauth2.jwt(Customizer.withDefaults());
                })
                .exceptionHandling((exceptions) -> exceptions
                        .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
                        .accessDeniedHandler(new BearerTokenAccessDeniedHandler())
                );

        return http.build();
    }

    @Bean
    JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withPublicKey(this.key).build();
    }

    @Bean
    JwtEncoder jwtEncoder() {
        JWK jwk = new RSAKey.Builder(this.key).privateKey(this.priv).build();
        JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
        return new NimbusJwtEncoder(jwks);
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("*"));
        configuration.setAllowedHeaders(List.of("*"));
        configuration.setAllowedMethods(Arrays.asList("POST", "GET", "DELETE", "PUT", "OPTIONS"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }

}

CustomAuthorizationManager.java

package com.kaipizhe.config;

import com.kaipizhe.service.PermissionService;
import jakarta.validation.constraints.NotNull;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.function.Supplier;

@Component
public class CustomAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    private final PermissionService permissionService;
    public CustomAuthorizationManager(PermissionService permissionService) {
        this.permissionService = permissionService;
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
        return getAuthorizationDecision(authentication, object, permissionService);
    }

    @NotNull
    static AuthorizationDecision getAuthorizationDecision(Supplier<Authentication> authentication, RequestAuthorizationContext object, PermissionService permissionService) {
        if (authentication.get() != null) {
            // 创始人直接放行
            long count = authentication.get().getAuthorities().stream().filter(o -> o.getAuthority().equals("SCOPE_ROLE_1")).count();
            if (count > 0) {
                return new AuthorizationDecision(true);
            } else {
                // 是不是在这个地方可以通过真实的url获取到controller中动态的url,spring本身自己有一个方法,是什么,有那位大哥知道不?
                // 再通过动态的url直接去数据库查询就可以了

                String url = object.getRequest().getRequestURI();
                String username = authentication.get().getName();
                if (StringUtils.hasText(url) && StringUtils.hasText(username) && permissionService.getPermissionByUrlUsername(url, username) != null) {
                    return new AuthorizationDecision(true);
                }
            }
        }

        throw new AccessDeniedException("没有权限访问!");
    }

}

就是再 CustomAuthorizationManager.javagetAuthorizationDecision() 方法中怎么根据真实的URL去获取controller中动态的url?

阅读 496
4 个回答

不建议递归方案

你提到的 /list/test/1/list/test/{id}/list/{id}/{id}/{id}/{id}/{id} 这种方式有几个问题:

  • 组合爆炸:路径越长,可能的组合越多
  • 匹配不准确:可能匹配到错误的权限
  • 性能问题:需要大量的数据库查询
  • 维护困难:逻辑复杂,难以调试

实际使用建议

对于你的场景,建议:

  1. 简单权限:继续使用静态路径 + @RequestParam
  2. 复杂权限:使用动态路径模式,在权限表中存储如 /list/{id} 这样的模式
  3. 性能优化:启动时将所有权限模式加载到缓存中
  4. 分层设计:可以设计权限的层级关系,比如有 /list/** 权限就拥有所有list相关的权限

匹配策略

  1. 先精确匹配:直接查询是否有完全相同的静态URL
  2. 再模式匹配:按优先级使用AntPathMatcher进行模式匹配
  3. 缓存优化:将权限模式缓存到内存中,避免每次都查数据库

方案1: 使用Spring的AntPathMatcher (推荐)

// 方案1: 使用Spring的AntPathMatcher (推荐)
@Component
public class PermissionMatcher {
    
    private final AntPathMatcher pathMatcher = new AntPathMatcher();
    
    @Autowired
    private PermissionService permissionService;
    
    public boolean hasPermission(String requestUrl, Long userId) {
        // 1. 先精确匹配静态路径
        if (permissionService.hasExactPermission(requestUrl, userId)) {
            return true;
        }
        
        // 2. 获取所有动态路径模式
        List<String> patterns = permissionService.getAllPatterns(userId);
        
        // 3. 使用AntPathMatcher进行模式匹配
        for (String pattern : patterns) {
            if (pathMatcher.match(pattern, requestUrl)) {
                return true;
            }
        }
        
        return false;
    }
}

// 拦截器实现
@Component
public class PermissionInterceptor implements HandlerInterceptor {
    
    @Autowired
    private PermissionMatcher permissionMatcher;
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
        
        String requestUrl = request.getRequestURI();
        Long userId = getCurrentUserId(request); // 获取当前用户ID
        
        if (!permissionMatcher.hasPermission(requestUrl, userId)) {
            response.setStatus(HttpStatus.FORBIDDEN.value());
            return false;
        }
        
        return true;
    }
    
    private Long getCurrentUserId(HttpServletRequest request) {
        // 从token或session中获取用户ID
        return 1L; // 示例
    }
}
       

方案二数据库表结构优化

// 方案2: 数据库表结构优化
/*
建议的权限表结构:
CREATE TABLE t_permission (
    id BIGINT PRIMARY KEY,
    url_pattern VARCHAR(255),  -- 存储模式,如 /list/{id}
    permission_code VARCHAR(100), -- 权限编码
    description VARCHAR(255),
    is_pattern BOOLEAN DEFAULT FALSE, -- 标识是否为模式匹配
    pattern_order INT DEFAULT 0  -- 匹配优先级,数字越小优先级越高
);

示例数据:
1, '/list', 'LIST_VIEW', '列表查看', false, 0
2, '/list/{id}', 'LIST_DETAIL', '列表详情', true, 1
3, '/item', 'ITEM_VIEW', '项目查看', false, 0
4, '/item/{id}', 'ITEM_DETAIL', '项目详情', true, 1
5, '/list/test/{id}', 'LIST_TEST_DETAIL', '测试详情', true, 2
6, '/list/{type}/{id}', 'LIST_TYPE_DETAIL', '分类详情', true, 3
*/

// Service层实现
@Service
public class PermissionService {
    
    @Autowired
    private PermissionRepository permissionRepository;
    
    // 精确匹配静态路径
    public boolean hasExactPermission(String url, Long userId) {
        return permissionRepository.existsByUrlAndUserId(url, userId);
    }
    
    // 获取用户所有权限模式,按优先级排序
    public List<String> getAllPatterns(Long userId) {
        return permissionRepository.findPatternsByUserIdOrderByPriority(userId);
    }
    
    // 完整的权限检查方法
    public boolean checkPermission(String requestUrl, Long userId) {
        // 1. 先检查精确匹配
        List<Permission> exactMatches = permissionRepository
            .findByUrlPatternAndIsPattern(requestUrl, false);
        
        if (hasUserPermissions(exactMatches, userId)) {
            return true;
        }
        
        // 2. 按优先级检查模式匹配
        List<Permission> patterns = permissionRepository
            .findByIsPatternTrueOrderByPatternOrder();
        
        AntPathMatcher matcher = new AntPathMatcher();
        
        for (Permission permission : patterns) {
            if (matcher.match(permission.getUrlPattern(), requestUrl)) {
                if (hasUserPermission(permission.getId(), userId)) {
                    return true;
                }
            }
        }
        
        return false;
    }
    
    private boolean hasUserPermissions(List<Permission> permissions, Long userId) {
        return permissions.stream()
            .anyMatch(p -> hasUserPermission(p.getId(), userId));
    }
    
    private boolean hasUserPermission(Long permissionId, Long userId) {
        // 查询用户角色权限关联表
        return permissionRepository.existsUserPermission(userId, permissionId);
    }
}

方案三使用Spring Security表达式 (如果项目使用Spring Security)

// 方案3: 使用Spring Security表达式 (如果项目使用Spring Security)
@RestController
public class ExampleController {
    
    // 可以使用SpEL表达式进行权限控制
    @PostMapping("/list/{id}")
    @PreAuthorize("hasPermission(#id, 'LIST_DETAIL')")
    public ResponseEntity<?> getListDetail(@PathVariable Long id) {
        // 业务逻辑
        return ResponseEntity.ok().build();
    }
}

// 自定义PermissionEvaluator
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
    
    @Override
    public boolean hasPermission(Authentication authentication, 
                               Object targetDomainObject, 
                               Object permission) {
        // 自定义权限检查逻辑
        return checkPermission(authentication, targetDomainObject, permission);
    }
    
    @Override
    public boolean hasPermission(Authentication authentication, 
                               Serializable targetId, 
                               String targetType, 
                               Object permission) {
        return checkPermission(authentication, targetId, permission);
    }
    
    private boolean checkPermission(Authentication auth, Object target, Object permission) {
        // 实现具体的权限检查逻辑
        String currentUrl = getCurrentRequestURL();
        Long userId = getUserId(auth);
        
        // 使用上面的PermissionService进行检查
        return true; // 示例返回
    }
    
    private String getCurrentRequestURL() {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes instanceof ServletRequestAttributes) {
            HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
            return request.getRequestURI();
        }
        return null;
    }
}

// 配置类
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
    
    @Autowired
    private PermissionInterceptor permissionInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(permissionInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/login", "/logout", "/error");
    }
}

// Repository接口示例
@Repository
public interface PermissionRepository extends JpaRepository<Permission, Long> {
    
    @Query("SELECT p FROM Permission p WHERE p.urlPattern = :url AND p.isPattern = false")
    List<Permission> findByUrlPatternAndIsPattern(@Param("url") String url, 
                                                 @Param("isPattern") boolean isPattern);
    
    @Query("SELECT p FROM Permission p WHERE p.isPattern = true ORDER BY p.patternOrder")
    List<Permission> findByIsPatternTrueOrderByPatternOrder();
    
    @Query("SELECT CASE WHEN COUNT(up) > 0 THEN true ELSE false END " +
           "FROM UserPermission up WHERE up.userId = :userId AND up.permissionId = :permissionId")
    boolean existsUserPermission(@Param("userId") Long userId, 
                               @Param("permissionId") Long permissionId);
    
    @Query("SELECT p.urlPattern FROM Permission p " +
           "JOIN UserPermission up ON p.id = up.permissionId " +
           "WHERE up.userId = :userId AND p.isPattern = true " +
           "ORDER BY p.patternOrder")
    List<String> findPatternsByUserIdOrderByPriority(@Param("userId") Long userId);
}

补充

使用HandlerInterceptorAdapter和@RequestMappingHandlerMapping
基于映射机制的逻辑:获取映射模式 → 查询权限表 → 放行/拒绝

// 更精确的方式 - 使用HandlerInterceptorAdapter和@RequestMappingHandlerMapping
@Component
public class MappingBasedPermissionInterceptor implements HandlerInterceptor {
    
    @Autowired
    private PermissionService permissionService;
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
        
        if (!(handler instanceof HandlerMethod)) {
            return true; // 非Controller方法,直接放行
        }
        
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Long userId = getCurrentUserId(request);
        
        // 获取方法上的映射模式
        String mappingPattern = extractMappingPattern(handlerMethod);
        
        // 直接查询数据库权限表
        boolean hasPermission = permissionService.checkPermissionByPattern(userId, mappingPattern);
        
        if (!hasPermission) {
            response.setStatus(HttpStatus.FORBIDDEN.value());
            response.getWriter().write("{\"error\":\"Access denied\"}");
            return false;
        }
        
        return true;
    }
    
    /**
     * 提取方法的映射模式
     */
    private String extractMappingPattern(HandlerMethod handlerMethod) {
        // 检查PostMapping注解
        PostMapping postMapping = handlerMethod.getMethodAnnotation(PostMapping.class);
        if (postMapping != null) {
            return getFirstPattern(postMapping.value(), postMapping.path());
        }
        
        // 检查GetMapping注解
        GetMapping getMapping = handlerMethod.getMethodAnnotation(GetMapping.class);
        if (getMapping != null) {
            return getFirstPattern(getMapping.value(), getMapping.path());
        }
        
        // 检查RequestMapping注解
        RequestMapping requestMapping = handlerMethod.getMethodAnnotation(RequestMapping.class);
        if (requestMapping != null) {
            return getFirstPattern(requestMapping.value(), requestMapping.path());
        }
        
        // 检查类级别的RequestMapping
        RequestMapping classMapping = handlerMethod.getBeanType().getAnnotation(RequestMapping.class);
        if (classMapping != null) {
            String classPattern = getFirstPattern(classMapping.value(), classMapping.path());
            String methodPattern = extractMethodPattern(handlerMethod);
            return combinePatterns(classPattern, methodPattern);
        }
        
        return "/"; // 默认根路径
    }
    
    private String extractMethodPattern(HandlerMethod handlerMethod) {
        // 提取方法级别的映射模式
        PostMapping postMapping = handlerMethod.getMethodAnnotation(PostMapping.class);
        if (postMapping != null) {
            return getFirstPattern(postMapping.value(), postMapping.path());
        }
        // 其他映射注解...
        return "";
    }
    
    private String getFirstPattern(String[] values, String[] paths) {
        if (values.length > 0 && !values[0].isEmpty()) {
            return values[0];
        }
        if (paths.length > 0 && !paths[0].isEmpty()) {
            return paths[0];
        }
        return "";
    }
    
    private String combinePatterns(String classPattern, String methodPattern) {
        if (classPattern.isEmpty()) return methodPattern;
        if (methodPattern.isEmpty()) return classPattern;
        
        String combined = classPattern;
        if (!classPattern.endsWith("/")) {
            combined += "/";
        }
        if (methodPattern.startsWith("/")) {
            combined += methodPattern.substring(1);
        } else {
            combined += methodPattern;
        }
        return combined;
    }
    
    private Long getCurrentUserId(HttpServletRequest request) {
        // 实现获取当前用户ID的逻辑
        return 1L;
    }
}

// 权限服务 - 简化版,直接查询映射模式
@Service
public class PermissionService {
    
    @Autowired
    private PermissionRepository permissionRepository;
    
    /**
     * 根据映射模式检查权限
     * 这样就不需要复杂的模式匹配了,直接精确查询
     */
    public boolean checkPermissionByPattern(Long userId, String mappingPattern) {
        // 直接查询用户是否有这个映射模式的权限
        return permissionRepository.existsUserPermissionByPattern(userId, mappingPattern);
    }
    
    public boolean hasPermission(Long userId, String mappingPattern) {
        return checkPermissionByPattern(userId, mappingPattern);
    }
}

// Repository接口
@Repository
public interface PermissionRepository extends JpaRepository<Permission, Long> {
    
    /**
     * 检查用户是否有指定映射模式的权限
     */
    @Query("SELECT CASE WHEN COUNT(up) > 0 THEN true ELSE false END " +
           "FROM Permission p " +
           "JOIN UserPermission up ON p.id = up.permissionId " +
           "WHERE up.userId = :userId AND p.urlPattern = :pattern")
    boolean existsUserPermissionByPattern(@Param("userId") Long userId, 
                                        @Param("pattern") String pattern);
}

CustomAuthorizationManager.java 文件修改

// 注入RequestMappingHandlerMapping 
@Component
public class CustomAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    private final PermissionService permissionService;
    private final RequestMappingHandlerMapping requestMappingHandlerMapping;
    
    public CustomAuthorizationManager(PermissionService permissionService,
                                    RequestMappingHandlerMapping requestMappingHandlerMapping) {
        this.permissionService = permissionService;
        this.requestMappingHandlerMapping = requestMappingHandlerMapping;
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
        if (authentication.get() != null) {
            // 创始人直接放行
            long count = authentication.get().getAuthorities().stream()
                .filter(o -> o.getAuthority().equals("SCOPE_ROLE_1"))
                .count();
            if (count > 0) {
                return new AuthorizationDecision(true);
            }
            
            // 获取动态URL模式
            String dynamicUrlPattern = getDynamicUrlPattern(object.getRequest());
            String username = authentication.get().getName();
            
            if (StringUtils.hasText(dynamicUrlPattern) && StringUtils.hasText(username)) {
                // 用动态URL模式去查询权限
                if (permissionService.hasPermissionByPattern(username, dynamicUrlPattern)) {
                    return new AuthorizationDecision(true);
                }
            }
        }

        throw new AccessDeniedException("没有权限访问!");
    }

    /**
     * 获取请求对应的动态URL模式
     */
    private String getDynamicUrlPattern(HttpServletRequest request) {
        try {
            // 方法1: 通过RequestMappingHandlerMapping获取HandlerExecutionChain
            HandlerExecutionChain executionChain = requestMappingHandlerMapping.getHandler(request);
            
            if (executionChain != null && executionChain.getHandler() instanceof HandlerMethod) {
                HandlerMethod handlerMethod = (HandlerMethod) executionChain.getHandler();
                return extractMappingPattern(handlerMethod);
            }
        } catch (Exception e) {
            // 如果出错,降级使用原始URL
            System.err.println("Failed to get dynamic URL pattern: " + e.getMessage());
        }
        
        // 降级方案:返回原始请求URI
        return request.getRequestURI();
    }

    /**
     * 从HandlerMethod中提取映射模式
     */
    private String extractMappingPattern(HandlerMethod handlerMethod) {
        // 检查PostMapping
        PostMapping postMapping = handlerMethod.getMethodAnnotation(PostMapping.class);
        if (postMapping != null) {
            return getFirstPattern(postMapping.value(), postMapping.path());
        }
        
        // 检查GetMapping
        GetMapping getMapping = handlerMethod.getMethodAnnotation(GetMapping.class);
        if (getMapping != null) {
            return getFirstPattern(getMapping.value(), getMapping.path());
        }
        
        // 检查RequestMapping
        RequestMapping requestMapping = handlerMethod.getMethodAnnotation(RequestMapping.class);
        if (requestMapping != null) {
            return getFirstPattern(requestMapping.value(), requestMapping.path());
        }
        
        // 检查类级别RequestMapping + 方法级别映射
        return getCombinedPattern(handlerMethod);
    }

    private String getCombinedPattern(HandlerMethod handlerMethod) {
        // 获取类级别的RequestMapping
        RequestMapping classMapping = handlerMethod.getBeanType().getAnnotation(RequestMapping.class);
        String classPattern = "";
        if (classMapping != null) {
            classPattern = getFirstPattern(classMapping.value(), classMapping.path());
        }
        
        // 获取方法级别的映射
        String methodPattern = getMethodPattern(handlerMethod);
        
        // 组合路径
        return combinePatterns(classPattern, methodPattern);
    }

    private String getMethodPattern(HandlerMethod handlerMethod) {
        PostMapping postMapping = handlerMethod.getMethodAnnotation(PostMapping.class);
        if (postMapping != null) {
            return getFirstPattern(postMapping.value(), postMapping.path());
        }
        
        GetMapping getMapping = handlerMethod.getMethodAnnotation(GetMapping.class);
        if (getMapping != null) {
            return getFirstPattern(getMapping.value(), getMapping.path());
        }
        
        RequestMapping requestMapping = handlerMethod.getMethodAnnotation(RequestMapping.class);
        if (requestMapping != null) {
            return getFirstPattern(requestMapping.value(), requestMapping.path());
        }
        
        return "";
    }

    private String getFirstPattern(String[] values, String[] paths) {
        if (values.length > 0 && StringUtils.hasText(values[0])) {
            return values[0];
        }
        if (paths.length > 0 && StringUtils.hasText(paths[0])) {
            return paths[0];
        }
        return "";
    }

    private String combinePatterns(String classPattern, String methodPattern) {
        if (!StringUtils.hasText(classPattern)) return methodPattern;
        if (!StringUtils.hasText(methodPattern)) return classPattern;
        
        String combined = classPattern;
        if (!classPattern.endsWith("/")) {
            combined += "/";
        }
        if (methodPattern.startsWith("/")) {
            combined += methodPattern.substring(1);
        } else {
            combined += methodPattern;
        }
        return combined;
    }
}

CustomAuthorizationManager.java 改进匹配前缀

  • 新增了getClassLevelPattern()方法,完善了getMethodLevelPattern()方法,改进了combinePatterns()方法,更完善的路径组合逻辑,处理各种边界情况。
// 注入RequestMappingHandlerMapping 
@Component
public class CustomAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    private final PermissionService permissionService;
    private final RequestMappingHandlerMapping requestMappingHandlerMapping;
    
    public CustomAuthorizationManager(PermissionService permissionService,
                                    RequestMappingHandlerMapping requestMappingHandlerMapping) {
        this.permissionService = permissionService;
        this.requestMappingHandlerMapping = requestMappingHandlerMapping;
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
        if (authentication.get() != null) {
            // 创始人直接放行
            long count = authentication.get().getAuthorities().stream()
                .filter(o -> o.getAuthority().equals("SCOPE_ROLE_1"))
                .count();
            if (count > 0) {
                return new AuthorizationDecision(true);
            }
            
            // 获取动态URL模式
            String dynamicUrlPattern = getDynamicUrlPattern(object.getRequest());
            String username = authentication.get().getName();
            
            if (StringUtils.hasText(dynamicUrlPattern) && StringUtils.hasText(username)) {
                // 用动态URL模式去查询权限
                if (permissionService.hasPermissionByPattern(username, dynamicUrlPattern)) {
                    return new AuthorizationDecision(true);
                }
            }
        }

        throw new AccessDeniedException("没有权限访问!");
    }

    /**
     * 获取请求对应的动态URL模式
     */
    private String getDynamicUrlPattern(HttpServletRequest request) {
        try {
            // 方法1: 通过RequestMappingHandlerMapping获取HandlerExecutionChain
            HandlerExecutionChain executionChain = requestMappingHandlerMapping.getHandler(request);
            
            if (executionChain != null && executionChain.getHandler() instanceof HandlerMethod) {
                HandlerMethod handlerMethod = (HandlerMethod) executionChain.getHandler();
                return extractMappingPattern(handlerMethod);
            }
        } catch (Exception e) {
            // 如果出错,降级使用原始URL
            System.err.println("Failed to get dynamic URL pattern: " + e.getMessage());
        }
        
        // 降级方案:返回原始请求URI
        return request.getRequestURI();
    }

    /**
     * 从HandlerMethod中提取映射模式(包含类级别前缀)
     */
    private String extractMappingPattern(HandlerMethod handlerMethod) {
        // 关键:先获取类级别的RequestMapping前缀
        String classPattern = getClassLevelPattern(handlerMethod);
        String methodPattern = getMethodLevelPattern(handlerMethod);
        
        // 组合类级别和方法级别的路径
        return combinePatterns(classPattern, methodPattern);
    }
    
    /**
     * 获取类级别的RequestMapping路径
     */
    private String getClassLevelPattern(HandlerMethod handlerMethod) {
        Class<?> controllerClass = handlerMethod.getBeanType();
        
        // 检查@RequestMapping注解
        RequestMapping classRequestMapping = controllerClass.getAnnotation(RequestMapping.class);
        if (classRequestMapping != null) {
            return getFirstPattern(classRequestMapping.value(), classRequestMapping.path());
        }
        
        // 检查@RestController上的路径(如果有的话)
        // 有些项目可能在@RestController注解上也定义路径
        return "";
    }
    
    /**
     * 获取方法级别的映射路径
     */
    private String getMethodLevelPattern(HandlerMethod handlerMethod) {
        // 检查DeleteMapping
        DeleteMapping deleteMapping = handlerMethod.getMethodAnnotation(DeleteMapping.class);
        if (deleteMapping != null) {
            return getFirstPattern(deleteMapping.value(), deleteMapping.path());
        }
        
        // 检查PostMapping
        PostMapping postMapping = handlerMethod.getMethodAnnotation(PostMapping.class);
        if (postMapping != null) {
            return getFirstPattern(postMapping.value(), postMapping.path());
        }
        
        // 检查GetMapping
        GetMapping getMapping = handlerMethod.getMethodAnnotation(GetMapping.class);
        if (getMapping != null) {
            return getFirstPattern(getMapping.value(), getMapping.path());
        }
        
        // 检查PutMapping
        PutMapping putMapping = handlerMethod.getMethodAnnotation(PutMapping.class);
        if (putMapping != null) {
            return getFirstPattern(putMapping.value(), putMapping.path());
        }
        
        // 检查RequestMapping
        RequestMapping requestMapping = handlerMethod.getMethodAnnotation(RequestMapping.class);
        if (requestMapping != null) {
            return getFirstPattern(requestMapping.value(), requestMapping.path());
        }
        
        return "";
    }

    /**
     * 组合类级别和方法级别的路径模式
     */
    private String combinePatterns(String classPattern, String methodPattern) {
        // 如果类级别没有路径,直接返回方法级别路径
        if (!StringUtils.hasText(classPattern)) {
            return StringUtils.hasText(methodPattern) ? methodPattern : "";
        }
        
        // 如果方法级别没有路径,返回类级别路径
        if (!StringUtils.hasText(methodPattern)) {
            return classPattern;
        }
        
        // 组合两个路径
        String combined = classPattern;
        
        // 保证类路径以/结尾
        if (!classPattern.endsWith("/")) {
            combined += "/";
        }
        
        // 处理方法路径的开头/
        if (methodPattern.startsWith("/")) {
            combined += methodPattern.substring(1);
        } else {
            combined += methodPattern;
        }
        
        // 保证最终路径以/开头
        if (!combined.startsWith("/")) {
            combined = "/" + combined;
        }
        
        return combined;
    }
}

创建自定义的处理器映射信息提取器

@Component
public class CustomUrlPatternExtractor implements ApplicationListener<ContextRefreshedEvent> {
    
    @Autowired
    private RequestMappingHandlerMapping handlerMapping;
    
    private final Map<String, String> packagePrefixMap = Map.of(
        "com.kaipizhe.controller.web", "/web",
        "com.kaipizhe.controller.app", "/app"
    );
    
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        extractAllUrls();
    }
    
    public void extractAllUrls() {
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
        
        for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) {
            RequestMappingInfo mappingInfo = entry.getKey();
            HandlerMethod handlerMethod = entry.getValue();
            
            String controllerPackage = handlerMethod.getBeanType().getPackage().getName();
            String prefix = getMatchingPrefix(controllerPackage);
            
            Set<String> patterns = mappingInfo.getPatternsCondition().getPatterns();
            
            for (String pattern : patterns) {
                String completeUrl = prefix + pattern;
                
                // 检查是否包含路径变量
                if (pattern.contains("{") && pattern.contains("}")) {
                    System.out.println("Dynamic URL: " + completeUrl);
                    // 这里就能获取到 /web/api/items/type/{id}
                }
            }
        }
    }
    
    private String getMatchingPrefix(String packageName) {
        return packagePrefixMap.entrySet().stream()
            .filter(entry -> packageName.startsWith(entry.getKey()))
            .map(Map.Entry::getValue)
            .findFirst()
            .orElse("");
    }
}

这里问题的关键在于​​将数据库中的路径模式转换为可匹配的正则表达式​​,并与实际请求路径进行模式匹配,以下方法可以试试看。

1,在权限表中添加 ​​路径模式类型​​ 和 ​​正则表达式​​ 字段:

CREATE TABLE t_permission (
    id BIGINT PRIMARY KEY,
    url VARCHAR(255) NOT NULL,      -- 原始路径模式
    pattern_type ENUM('STATIC', 'DYNAMIC') NOT NULL,
    regex_pattern VARCHAR(255) NOT NULL -- 转换后的正则表达式
);

示例数据:

id    url    pattern_type    regex_pattern
1    /list    STATIC    ^/list$
2    /list/{id}    DYNAMIC    ^/list/[^/]+$

2,将 Spring 的路径语法转换为正则表达式:

public class PathConverter {
    public static String convertToRegex(String path) {
        // 1. 转义特殊字符(如 . 等)
        String regex = path.replace(".", "\\.");
        
        // 2. 替换 {variable} 为 [^/]+(匹配非斜杠字符)
        regex = regex.replaceAll("\\{[^/]+?\\}", "[^/]+");
        
        // 3. 添加起止锚点
        return "^" + regex + "$";
    }
}

// 使用示例
String springPath = "/list/{type}/{id}";
String regex = PathConverter.convertToRegex(springPath); 
// 结果: ^/list/[^/]+/[^/]+$

3,在拦截器中实现路径匹配:

@Component
public class AuthInterceptor implements HandlerInterceptor {
    
    @Autowired
    private PermissionService permissionService;
    
    // 缓存正则表达式和权限的映射
    private final Map<Pattern, Permission> patternCache = new ConcurrentHashMap<>();
    
    @PostConstruct
    public void init() {
        // 启动时加载所有权限规则并编译正则
        List<Permission> permissions = permissionService.getAllPermissions();
        for (Permission p : permissions) {
            Pattern pattern = Pattern.compile(p.getRegexPattern());
            patternCache.put(pattern, p);
        }
    }

    @Override
    public boolean preHandle(HttpServletRequest request, 
                             HttpServletResponse response, 
                             Object handler) throws Exception {
        
        String requestPath = getRequestPath(request); // 获取纯净路径如 "/list/test/123"
        
        // 1. 先尝试精确匹配静态路径
        Permission staticPerm = permissionService.findByExactUrl(requestPath);
        if (staticPerm != null) {
            return checkPermission(staticPerm, request);
        }
        
        // 2. 正则匹配动态路径
        for (Map.Entry<Pattern, Permission> entry : patternCache.entrySet()) {
            if (entry.getKey().matcher(requestPath).matches()) {
                return checkPermission(entry.getValue(), request);
            }
        }
        
        // 3. 无匹配权限
        response.sendError(403, "No permission for path: " + requestPath);
        return false;
    }
    
    private String getRequestPath(HttpServletRequest request) {
        // 示例: 去掉上下文路径和结尾斜杠
        String path = request.getRequestURI()
            .substring(request.getContextPath().length())
            .replaceAll("/+$", "");
        return path;
    }
    
    private boolean checkPermission(Permission perm, HttpServletRequest request) {
        // 这里实现你的具体权限验证逻辑
        // 例如从request中获取用户信息,验证是否有perm权限
        return true; // 或 false
    }
}

搞那么复杂干啥,直接用ant风格的path匹配就行了。了解下Spring的类org.springframework.util.AntPathMatcher
遵循apache Ant规范

PathMatcher pathMatcher = new AntPathMatcher();

List<String> patterns = new ArrayList<>();
patterns.add("/**");
patterns.add("/users/*");
patterns.add("/orders/{id:[0-9]+}");

for (String pattern : patterns) {
    System.out.println("A:" + pathMatcher.match(pattern, "/users/12121"));
    System.out.println("B:" + pathMatcher.match(pattern, "/orders/18"));
    System.out.println("C:" + pathMatcher.match(pattern, "/orders/get/1"));
}
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题