尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

若依框架开放接口配置:Spring Security白名单精准放行与安全实践

若依框架开放接口配置:Spring Security白名单精准放行与安全实践 1. 项目缘起一个看似简单却绕不开的“权限”问题最近在做一个项目需要把若依Ruoyi框架里的一些数据接口开放给一个外部网站调用。这个需求听起来挺常见的对吧不就是做个API网关或者加个白名单的事儿。但真动起手来才发现若依这套基于Spring Security的权限体系就像一堵设计精密的墙把内外网隔得明明白白。外部请求过来要么被登录拦截器挡在门外要么就是各种“权限不足”、“未授权”的响应。我猜不少朋友都遇到过类似场景公司有个老旧的内部管理系统是用若依搭的现在业务发展了需要让合作伙伴的站点或者移动端H5能直接获取一些公开数据比如产品目录、新闻公告之类的。你肯定不想为了这点事让外部用户去走一套完整的若依登录流程那体验太割裂了。更不想去动核心的业务代码风险太大。所以最理想的方案就是在若依框架的“围墙”上巧妙地开几扇“小门”让特定的、无需认证的请求能畅通无阻。这个“去掉权限”的过程绝不是简单粗暴地把PreAuthorize注解一删了事或者关掉Security配置。那样做无异于拆掉承重墙整个系统的安全性就崩塌了。我们需要的是精准的、外科手术式的修改在保持系统主体安全架构不变的前提下为少数特定的外部接口“亮绿灯”。接下来我就把自己趟过的路、踩过的坑以及最终稳定运行的方案详细拆解一遍。2. 理解若依的权限“围墙”Spring Security配置核心要开门先得看懂门是怎么建的。若依的权限控制核心在SecurityConfig这个配置类里。网上很多教程一上来就让你改这里但如果不明白其工作原理很容易改出问题。2.1 默认配置的“三道关卡”若依默认的Security配置通常构建了至少三道防线登录状态检查通过http.formLogin()和相关的LoginFilter任何访问非公开资源未在白名单内的URL的请求如果没有有效的登录会话Session都会被重定向到登录页面。这是第一道也是最外层的关卡。请求路径鉴权在http.authorizeRequests()链中通过antMatchers()方法定义了一系列URL模式的白名单如/login,/captchaImage,/webjars/**等。不在白名单内的路径都需要认证。这是我们重点要操作的地方。方法级权限控制在Service或Controller方法上通过PreAuthorize(“ss.hasPermi(‘system:user:list’)”)或PreAuthorize(“hasRole(‘admin’)”)这样的注解进行更细粒度的权限校验。即使通过了前两道关卡这里通不过依然会返回403错误。我们的目标主要是为外部调用“绕过”前两道关卡特别是第二道“路径鉴权”关。对于只需要公开数据的接口第三道关卡通常不需要因为相关方法本身就不应加权限注解。2.2 关键配置类SecurityConfig与ApplicationConfig在若依项目中权限相关的配置通常分散在几个地方SecurityConfig: 定义核心的安全规则如白名单、密码编码器、会话策略等。ApplicationConfig: 可能存放一些应用级配置有时也会定义一些全局的Bean。XxxFilter: 各种自定义过滤器如JWT过滤器、验证码过滤器等。我们需要修改的主要是SecurityConfig。在动手前务必备份原文件。一个典型的若依SecurityConfig核心部分如下Configuration EnableGlobalMethodSecurity(prePostEnabled true, securedEnabled true) public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http // 关闭CSRF通常API项目会关闭但需评估风险 .csrf().disable() // 会话管理无状态或基于Session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 授权配置这是核心 .authorizeRequests() // 白名单放行登录、验证码等无需认证的接口 .antMatchers(/login, /captchaImage).anonymous() .antMatchers( HttpMethod.GET, /, /*.html, /**/*.html, /**/*.css, /**/*.js, /webjars/**, /swagger-resources/**, /v2/api-docs ).permitAll() // 除上面放行的所有请求都需要认证 .anyRequest().authenticated() .and() .headers().frameOptions().disable(); // 通常这里还会添加JWT过滤器等 // http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } }我们的手术刀就要精准地落在.authorizeRequests()这个链上。3. 精准“开门”配置外部接口白名单最推荐、也是最安全的方式就是扩展白名单。我们把需要被外部调用的接口路径添加到antMatchers().permitAll()的列表中。3.1 确定接口路径模式首先你需要规划好哪些接口要对开放。一个好的实践是为这些外部接口设计统一的前缀例如/api/open/**或/external/**。这样做有两个巨大好处配置简单在Security配置中一条antMatchers(“/api/open/**”).permitAll()规则就能覆盖所有未来新增的同类接口无需反复修改配置。意图清晰在代码中所有以该前缀开头的Controller其“无需认证”的特性一目了然便于团队协作和维护。假设我们决定使用/api/open/**这个路径。那么所有需要被外部网站调用的Controller都应该放在这个路径下。3.2 修改SecurityConfig配置接下来修改SecurityConfig.configure(HttpSecurity http)方法Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() // 原有的白名单 .antMatchers(/login, /captchaImage).anonymous() .antMatchers( HttpMethod.GET, /, /*.html, /**/*.html, /**/*.css, /**/*.js, /webjars/**, /swagger-resources/**, /v2/api-docs ).permitAll() // 新增放行所有开放API接口 .antMatchers(/api/open/**).permitAll() // 除上面放行的所有请求都需要认证 .anyRequest().authenticated() .and() .headers().frameOptions().disable(); // ... 其他配置如过滤器 }关键点permitAll()表示完全放行不进行任何安全拦截。anonymous()表示允许匿名访问但和permitAll()在大多数场景下效果类似。对于纯外部接口用permitAll()更贴切。3.3 创建对应的开放接口Controller现在你可以创建一个新的Controller或者修改现有的Controller将其映射路径改为/api/open/前缀下。RestController RequestMapping(/api/open/news) // 统一使用 /api/open 前缀 public class OpenNewsController { Autowired private INewsService newsService; /** * 获取公开的新闻列表 * 注意此接口已通过SecurityConfig配置放行无需登录即可访问 */ GetMapping(/list) public AjaxResult list(News news) { // 这里可以加一些业务逻辑比如只查询状态为“已发布”的新闻 news.setStatus(1); // 假设1代表已发布 ListNews list newsService.selectNewsList(news); return AjaxResult.success(list); } /** * 根据ID获取新闻详情 */ GetMapping(value /{newsId}) public AjaxResult getInfo(PathVariable(newsId) Long newsId) { // 同样可以增加校验如只返回已发布的新闻 News news newsService.selectNewsById(newsId); if (news null || !1.equals(news.getStatus())) { return AjaxResult.error(新闻不存在或未发布); } return AjaxResult.success(news); } }为什么这么做是安全的路径隔离开放接口和内部管理接口在URL层面就分开了不会误放行内部接口。权限注解依然有效即使这个Controller的方法上不小心加了PreAuthorize因为请求根本不会走到那个校验逻辑在过滤链早期就被放行了所以不会出错但为了代码清晰建议不要加。业务层可控在Service层或Controller里你仍然可以对查询条件做限制例如只查“已发布”状态的数据这是业务安全不属于框架权限范畴。4. 深入排查当“开门”后依然被拦截按照上面的步骤配置后大部分情况接口就能通了。但如果还是返回401或403别慌大概率是遇到了“隐藏关卡”。我们需要系统性地排查。4.1 排查链1过滤器Filter链Spring Security的本质就是一个过滤器链。除了路径匹配规则自定义的过滤器也可能拦截请求。JWT过滤器若依如果采用了令牌模式通常会有一个JwtAuthenticationTokenFilter。这个过滤器会尝试从请求头如Authorization中解析令牌。即使路径被permitAll()了请求依然会经过这个过滤器。问题过滤器可能发现请求没有令牌然后抛出一个异常或设置一个空的认证信息导致后续流程出错。解决在JWT过滤器的doFilterInternal方法最开头判断请求路径是否属于开放接口。如果是直接调用chain.doFilter(request, response)放行不再执行令牌解析逻辑。public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // 新增检查是否为开放接口路径 String uri request.getRequestURI(); if (uri.startsWith(/api/open/)) { chain.doFilter(request, response); return; } // 原有的令牌解析逻辑... } }CORS过滤器跨域问题。外部网站调用属于跨域如果后端没有正确配置CORS浏览器会拦截响应。这虽然不一定是401/403但会导致前端拿不到数据。解决在SecurityConfig中或通过单独的CorsFilter配置跨域。在SecurityConfig中配置更简单Override protected void configure(HttpSecurity http) throws Exception { http.cors().and() // 启用CORS配置 .csrf().disable() // ... 其他配置 } // 同时需要定义一个CorsConfigurationSource Bean Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList(https://external-site.com)); // 允许的源生产环境要写具体 configuration.setAllowedMethods(Arrays.asList(GET, POST, PUT, DELETE, OPTIONS)); configuration.setAllowedHeaders(Arrays.asList(*)); configuration.setAllowCredentials(true); // 如果需要传递Cookie设为true UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, configuration); // 对所有路径生效 return source; }4.2 排查链2Spring MVC拦截器Interceptor若依或你自己的项目里可能注册了全局的Spring MVC拦截器用于日志记录、参数预处理等。这些拦截器在Spring Security过滤器之后、Controller之前执行。问题拦截器里可能包含了对登录状态的判断如果发现没有登录用户可能会重定向或返回错误。解决同样在拦截器的preHandle方法中对开放接口路径进行判断并放行。public class MyInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String uri request.getRequestURI(); if (uri.startsWith(/api/open/)) { return true; // 直接放行 } // ... 原有的拦截逻辑 } }4.3 排查链3全局异常处理器与响应格式有时候请求其实已经通过了但由于业务代码抛出的异常被全局异常处理器捕获后返回的格式不符合外部调用者的预期。检查点确保你的开放接口返回的是统一的、友好的数据格式如JSON。若依的AjaxResult是一个不错的选择。避免在开放接口中抛出会被Security框架捕获的异常如AccessDeniedException。4.4 实用调试技巧开启Debug日志在application.yml中设置logging.level.org.springframework.securityDEBUG。这会打印出Security过滤器链的详细决策过程你能清晰地看到请求经过了哪些过滤器每个过滤器是放行PERMIT_ALL还是要求认证AUTHENTICATED。浏览器开发者工具/Postman仔细查看网络请求的响应头和响应体。401/403错误信息通常会在这里体现。同时检查请求头确认没有携带不该带的Cookie或Token导致服务端误以为是内部请求。断点调试在SecurityConfig的配置方法、自定义过滤器的doFilter方法、拦截器的preHandle方法中打上断点一步步跟踪请求的流向。5. 进阶考量安全、监控与架构优化仅仅能调通接口只是第一步。让这套机制在生产环境稳定、安全地运行还需要更多思考。5.1 安全加固防止白名单被滥用无限制的开放接口是危险的。我们必须增加一些保护措施频率限制Rate Limiting使用Guava的RateLimiter或Spring Boot的spring-boot-starter-data-redis配合Redisson对/api/open/**路径下的接口进行IP级或全局的频率限制防止恶意刷接口。RestController RequestMapping(/api/open/news) public class OpenNewsController { // 创建一个每秒最多10个请求的限流器 private final RateLimiter rateLimiter RateLimiter.create(10.0); GetMapping(/list) public AjaxResult list(News news) { // 尝试获取令牌如果获取不到即超频可以返回错误或等待 if (!rateLimiter.tryAcquire()) { return AjaxResult.error(“请求过于频繁请稍后再试”); } // ... 业务逻辑 } }API密钥API Key对于需要一定身份标识但又不至于用完整登录的场景可以采用简单的API Key机制。在请求头或参数中携带一个预先分配好的密钥服务端进行校验。GetMapping(/list) public AjaxResult list(News news, RequestHeader(“X-API-Key”) String apiKey) { if (!isValidApiKey(apiKey)) { return AjaxResult.error(“无效的API密钥”); } // ... 业务逻辑 }注意这种方式密钥容易泄露适合对安全性要求不高的内部或合作伙伴系统。更安全的方式是使用签名机制。请求签名更安全的做法是要求调用方对请求参数、时间戳等进行签名服务端用相同的算法验签。这能有效防止请求被篡改和重放。阿里云、腾讯云等开放API都采用这种方式。5.2 监控与日志知道谁在调用开放接口的访问日志尤为重要你需要知道接口被调用的频率、来源、是否出错。专用日志在开放接口的Controller或一个专门的切面Aspect中记录每一次访问的IP、URL、参数、时间、耗时和结果。可以将这些日志输出到独立的文件或发送到ELK等日志平台。监控告警对接监控系统如Prometheus Grafana对接口的QPS、错误率、响应时间设置告警阈值。当接口被异常频繁调用或大量出错时能及时通知到负责人。5.3 架构演进何时需要API网关当开放接口越来越多管理压力变大时就该考虑引入API网关了如Spring Cloud Gateway, Kong, Apisix。网关的优势统一入口所有外部请求先到网关由网关转发到后端的若依服务或其他服务。统一安全管控在网关层集中实现认证如JWT校验、鉴权、限流、熔断、日志记录后端若依服务的SecurityConfig可以大幅简化甚至只处理内部权限。协议转换与聚合网关可以处理GraphQL、gRPC等协议或将多个后端接口聚合成一个返回给前端。我们的场景如果只是暴露少数几个简单接口修改若依配置是最高效的。但如果未来有几十个开放接口且需要复杂的流量管理、协议支持那么早期引入一个轻量级网关会是更优雅的架构选择。你可以让网关监听/api/open/**的路径转发到若依服务内部一个完全不需要Security拦截的端口或路径上实现权限的物理隔离。6. 避坑指南我踩过的那些“雷”最后分享几个实际操作中容易忽略的坑点希望能帮你节省时间。路径匹配的优先级陷阱Spring Security的antMatchers()规则是有顺序的。更具体的规则应该放在前面更通用的规则如/**放在后面。如果你把.antMatchers(“/**”).permitAll()放在了最前面那么后面所有的.authenticated()规则都会失效。我们的修改一定要把新增的/api/open/**这条规则放在anyRequest().authenticated()这条规则之前但在其他更具体的规则如静态资源之后是一个比较安全的位置。permitAll()与anonymous()的细微差别在大多数情况下两者对于放行请求的效果是一样的。但anonymous()表示“允许匿名访问”Spring Security会创建一个AnonymousAuthenticationToken对象放入安全上下文而permitAll()是根本不做任何安全限制。如果你的某个过滤器或后续逻辑强依赖安全上下文中存在一个Authentication对象即使是匿名的那么用anonymous()可能更合适。对于纯粹的外部调用我通常用permitAll()。静态资源被拦截在添加了新的安全规则后务必检查原本放行的静态资源如/css/,/js/,/img/是否还能正常访问。有时规则顺序的改动会导致这些路径被要求认证。确保你的静态资源路径也在permitAll()的白名单中。Swagger/knife4j接口文档的访问如果你使用了API文档工具记得把它们的相关路径如/doc.html,/v2/api-docs,/swagger-resources,/webjars/**也加入白名单。否则外部开发者连你的接口文档都看不了。测试时清理浏览器状态在测试开放接口时务必使用浏览器的“无痕模式”或直接使用Postman、curl等工具并清除所有Cookie和本地存储。因为你可能已经登录了若依后台浏览器会自动携带登录会话的Cookie导致你测试时请求通过了误以为配置成功但实际上走的是已认证的会话流程。真正的测试一定要模拟一个“完全陌生”的客户端请求。版本差异请注意若依有多个版本单体、前后端分离、微服务Spring Security也有大版本更新如从Spring Boot 2.x 到 3.x Security配置方式有变化。本文基于Spring Security 5.x 和若依常见单体架构。如果你的版本不同核心思想配置白名单不变但具体配置类可能不再是继承WebSecurityConfigurerAdapter和注解的写法需要参考对应版本的官方文档进行调整。
返回列表