Spring Security AccessDeniedException全解析:排查与修复实战

发布时间:2026/10/10 20:50:49

Spring Security AccessDeniedException全解析:排查与修复实战 最近又收到一条这类报错日志里一行org.springframework.security.access.AccessDeniedException: 不允许访问前端同事盯着页面直挠头——“按钮都看得到为什么点一下就被拦”我接手之后翻了半小时配置才意识到这行异常在 Spring Security 里压根不是“看起来那么单纯”的错误。它可能是授权规则写严了可能是角色前缀不对也可能只是 CSRF 冒名顶替甚至是你自己写的过滤器在“帮倒忙”。这篇文章就把我实际排查这类问题的全过程拆开讲从异常抛出的位置到六条排查方向再到修复方案怎么选一步一步说清楚。1. 先看现场这类报错通常出现在哪几个场景1.1 一通来自前端的反馈先还原一下我遇到过的典型现场。项目是一个后台管理系统前端用的 Vue后端 Spring Boot 2.7 Spring Security 5.8。用户登录后能正常看到菜单但是点进“操作日志”页面时接口直接返回 403前端拿到的响应体是{ timestamp: 2024-05-11T10:23:41.12000:00, status: 403, error: Forbidden, path: /api/aspectLog/list }后端日志里恰好就有开头那行AccessDeniedException: 不允许访问。注意这个页面用户明明能看得到菜单说明前端拿到的菜单接口是通的但真正查数据时却被告知“不允许访问”。这种“页面能进、接口被拦”的场景最容易让人误判成“是不是接口地址写错了”“是不是 token 过期了”。其实这里已经能确定一件事用户是已认证的否则 Spring Security 会把它引导到登录流程而不是返回 403。既然认证没问题那就是授权阶段被拦了。授权被拦的原因很多但绝大多数跑不出后面我要讲的六个方向。1.2 报错的三种典型形态同样一个AccessDeniedException在不同项目里的表现差异很大。我把它归纳为三种形态你拿到报错后先对号入座能少走很多弯路形态表现常见原因默认白标页浏览器里出现 Whitelabel Error Page状态码 403未配置自定义 AccessDeniedHandler统一 JSON 返回前后端分离项目返回 403 业务 code/message实现了 AccessDeniedHandler但规则有问题登录页反复跳转访问接口时被 302 到登录页登录后还是跳回来认证信息丢失或匿名用户访问受保护资源第一种和第二种相对好排查问题集中在授权规则本身。第三种则要先回头查认证链确认 token 有没有正确传到后端、SecurityContext 是否真正建立了认证信息别一上来就改授权配置方向容易跑偏。1.3 第一反应应该做什么我的习惯是拿到报错先做三件事顺序不能乱确认当前登录用户的身份和角色——通过日志打印SecurityContextHolder.getContext().getAuthentication()看 authentication 是否为空、principal 是谁、authorities 里有哪些权限。确认访问的 URL 命中了哪条安全规则——把logging.level.org.springframework.securityDEBUG打开或者看授权决策日志判断是否真的命中了hasRole/hasAuthority。确认请求的方法和路径——是 GET 还是 POST同样一个路径GET 可能放行了POST 却被 CSRF 挡掉这类案例我遇到不止一次。这三步做完基本上能把问题缩小到“授权规则配置”还是“认证状态异常”两条路上。2. 报错背后的机制AccessDeniedException是在哪个环节被抛出来的2.1 认证和授权是两个不同的事很多刚接触 Spring Security 的开发者会把“登录失败”和“不允许访问”混为一谈。我平时喜欢用一个类比解释认证是门卫查身份证看你是不是这个小区的人授权是门卫查工牌看你是哪个部门的、能不能进这栋楼的某层。对应到代码里认证由AuthenticationManager和AuthenticationProvider负责解决的问题是“你是谁”授权由AuthorizationManager旧版本是AccessDecisionManager 投票器负责解决的问题是“你能干什么”。AccessDeniedException属于授权阶段的异常它抛出来的前提是——你已经是一个“认识的人”了但你的权限不够。2.2 ExceptionTranslationFilter那个决定要不要放行的“交警”Spring Security 的过滤器链里有一个关键角色叫ExceptionTranslationFilter它不负责授权本身只负责捕获链上其他过滤器抛出的AccessDeniedException和AuthenticationException然后决定下一步怎么办。它的处理逻辑大致是这样捕获到AccessDeniedException后先看当前用户是不是匿名用户或者认证信息是否完整。如果未认证或匿名就调用AuthenticationEntryPoint通常是跳转到登录页或返回 401引导用户去登录。如果已认证但权限不足就调用AccessDeniedHandler默认返回 403 并抛出AccessDeniedException。问题来了很多博客里会说“AccessDeniedException 会被 ExceptionTranslationFilter 处理”但实际你看到的异常堆栈往往不是在这个 Filter 里打印的而是授权过滤器Spring Security 6.x 里是AuthorizationFilter5.x 里是FilterSecurityInterceptor抛出后异常处理器处理失败或者在别的地方被重新抛出时才打印出来。所以排查的时候别纠结异常到底是谁打印的而是顺着这条链去定位“是哪一条规则判定拒绝的”。2.3 为什么说直接往上抛异常到Controller是不正常的有一种情况需要注意如果你在ControllerAdvice里写了ExceptionHandler(AccessDeniedException.class)并且发现它真的捕获到了这个异常那说明这个异常不是从安全过滤器链里抛出来的。为什么因为安全过滤器链在请求到达DispatcherServlet之前就已经执行完了异常在链上就被ExceptionTranslationFilter消化掉了根本到不了 Controller 层的异常解析器。那什么情况下 ControllerAdvice 能捕获到往往是你自己的业务代码里主动throw new AccessDeniedException(不允许访问)比如在 Service 层做某个数据权限判断时手动抛出。这种异常属于业务异常它传播路径和 Spring Security 过滤器链抛出的AccessDeniedException是两码事。这个区别非常重要因为它直接影响你的修复方案如果所有人都往ExceptionHandler里加AccessDeniedException的统一处理反而可能把安全过滤器的真实判定逻辑掩盖掉导致线上问题越来越难查。3. 我的排查链路六个方向逐个排除3.1 先看URL授权规则requestMatchers写对了吗第一站看SecurityFilterChain里的授权规则。Spring Security 6.x 的写法是http.authorizeHttpRequests(auth - auth .requestMatchers(/api/public/**).permitAll() .requestMatchers(/api/admin/**).hasRole(ADMIN) .anyRequest().authenticated() );这段配置里有三个隐藏的坑。第一个坑是匹配顺序。authorizeHttpRequests里的规则是按声明顺序匹配的一旦命中某条规则后面的规则就不再参与判断。如果你把.anyRequest().authenticated()写在了requestMatchers(/api/admin/**).hasRole(ADMIN)前面那么任何带认证信息的请求包括普通用户访问/api/admin/**都会因为命中authenticated()而通过授权——这反而更危险。反过来如果你把更宽松的规则写在前面又会把后面更严格的规则“短路”掉。第二个坑是匹配器的语义。requestMatchers底层用的是PathPatternRequestMatcher还是AntPathRequestMatcher取决于你的配置方式。比如requestMatchers(/api/**)不会匹配/api本身/api/aspectLog/list才会匹配而requestMatchers(/api)只匹配精确路径。更隐蔽的是/api/**这样的写法在某些版本中也不会匹配/api/aspectLog/list这种多层路径之外的带斜杠的情况——总之路径匹配规则比你想象中严格排查时最好直接打开 DEBUG 日志看它到底匹配了哪条规则。第三个坑是老项目升级的遗留代码。Spring Boot 2.7 之前很多人用的是antMatchers升到 Spring Security 6 之后antMatchers被移除了IDE 里会直接飘红。但如果你还在用 5.xantMatchers和mvcMatchers的语义也略有差异容易混淆。3.2 再看方法级安全注解注解生效了吗如果 URL 规则看起来没问题接口还是被拦第二站检查方法级安全注解也就是PreAuthorize、Secured、RolesAllowed这几位。这里有一个最常见的坑注解写了但没启用方法安全。Spring Security 5.x 需要Configuration EnableGlobalMethodSecurity(prePostEnabled true) public class SecurityConfig { }Spring Security 6.x 改成了Configuration EnableMethodSecurity public class SecurityConfig { }如果你没加这个注解PreAuthorize就是一堆安静的注释根本不会执行。这类问题的典型表现是接口“意外地”能访问——注意不是报 403。但还有一种更隐蔽的情况注解生效了可你写的方法名或权限字符串和实际登录用户的 authority 对不上。举个例子我在一个项目里看到过PreAuthorize(hasAuthority(admin)) GetMapping(/api/aspectLog/list) public Result list() { ... }而用户的权限是ROLE_ADMIN。hasAuthority(admin)要求 authority 里有一个值恰好等于adminROLE_ADMIN显然不等于admin于是无论你怎么登录都会被拒。这种问题靠肉眼很难发现必须对照数据库里用户的实际角色进行核对。3.3 角色前缀hasRole和hasAuthority的差异说到权限字符串就不得不提 Spring Security 一个容易让人“翻车”的设计角色前缀。hasRole(ADMIN)在底层等价于hasAuthority(ROLE_ADMIN)它会自动把传入的字符串拼上ROLE_前缀再去比对。反过来hasAuthority(ROLE_ADMIN)则是直接比对完整的权限标识。这意味着两件事如果你的用户角色在数据库里存的是一串ADMIN这种不带前缀的值而UserDetailsService在构造SimpleGrantedAuthority时也没有手动加ROLE_那么hasRole(ADMIN)永远返回 false。如果你的权限设计走的是“权限点”模式比如sys:user:list、sys:user:add那应该用hasAuthority或hasAnyAuthority不要用hasRole。我在实际项目里的建议是角色和权限分开设计。角色用hasRole判断数据库里不存 ROLE_ 前缀交给 Spring Security 拼细粒度权限用hasAuthority判断两者不要混着写。一旦既用了hasRole(sys:user:list)又用了hasAuthority(ROLE_ADMIN)排查起来会非常头大。3.4 CSRF拦截伪装成权限问题这是一个特别容易误诊的方向。Spring Security 默认开启 CSRF 防护对 POST、PUT、DELETE、PATCH 这类非安全方法会校验 CSRF token。如果你的前端没有把 token 放在请求头或表单里后端会直接返回 403。但这里有个迷惑性极强的点这个 403 和授权失败的 403 长得几乎一样唯一区别是响应体里的 message。Spring Security 默认的 CSRF 失败信息类似Invalid CSRF token found for http://localhost:8080/api/aspectLog/list注意日志里不一定有AccessDeniedException的完整堆栈因为 CSRF 过滤器CsrfFilter自己就有AccessDeniedHandler它会直接处理异常不一定会把异常抛给ExceptionTranslationFilter去打印。这就是为什么很多同事拿着日志说“没看到 AccessDeniedException 啊”其实问题根本不在授权。排查方法很简单看请求到底是什么方法。如果接口是 POST 而前端没带X-CSRF-TOKEN或X-XSRF-TOKEN取决于你配置的CsrfTokenRepository优先怀疑 CSRF。特别典型的场景是前端页面登录后菜单 GET 接口全通但所有 POST 都报 403且只在“登录之后”才出现——因为 CsrfFilter 的 token 依赖于会话一旦会话里的 token 和请求带的不一致就会拦截。不要一上来就无脑关闭 CSRF。如果项目是前后端分离且所有接口都不依赖会话可以配置无状态 session 并关闭 CSRF但如果你只是图省事csrf(csrf - csrf.disable())得想清楚安全边界在哪里。3.5 自定义过滤器与全局异常处理互相干扰接下来这个方向比较“进阶”但它往往是老项目最容易踩的暗坑。有人在过滤器链里加了自己的OncePerRequestFilter在 filter 里面做了业务判断比如检查请求参数里有没有某个标识没有就直接throw new AccessDeniedException(不允许访问);问题在于你自定义的这个 Filter 在链上的位置如果不在ExceptionTranslationFilter的“管辖范围”内比如你把它放在了ExceptionTranslationFilter之后或者干脆用addFilterBefore加在了链的末尾那么ExceptionTranslationFilter根本捕不到这个异常。异常会直接往外冒最终被DispatcherServlet的异常解析兜住返回的可能就是 500 而不是 403。还有一种情况是双重处理自定义 filter 抛了异常ControllerAdvice里也写了AccessDeniedException的处理器结果异常被 Controller 层捕获并包装此时你已经离开了 Spring Security 的安全上下文Authentication信息可能都拿不到了。排查这个方向的思路是把日志级别调到 DEBUG看异常到底是从哪个过滤器抛出来的调用栈里有没有OncePerRequestFilter、doFilterInternal这些字样。如果有说明问题十有八九出在你自己的过滤器逻辑里而不是 Spring Security 的授权规则。3.6 登录成功后的重定向触发了二次请求最后这个方向比较“隐蔽”我见过不止一次因为登录成功后的跳转地址踩坑。场景是这样的你配置了http.formLogin(form - form .loginPage(/login) .defaultSuccessUrl(/admin/home, true) );用户登录成功后浏览器会 302 到/admin/home。这个/admin/home本身也是一个受保护资源如果当前用户的角色不满足它要求的规则会在登录成功之后紧接着爆发AccessDeniedException。表现形态很迷惑用户确实登录成功了session 也确实建立了但页面始终打不开一直 403。而且日志里那个AccessDeniedException看起来和登录完全无关。另一种变体是登录成功后跳到了一个需要 POST 的地址或者跳转到/再被转发到某个受保护页面中途出现二次鉴权失败。排查时记得把defaultSuccessUrl和successForwardUrl的目标路径也纳入授权规则的检查范围。4. 修复方案怎么选不要在“加白名单”一条路上走到黑4.1 方案A精确调整授权规则如果确认是授权规则配置问题优先做“规则层面的修正”而不是把接口一刀切到permitAll。比如实际业务里/api/aspectLog/list应当允许“日志管理员”和“系统管理员”访问那就明确写.requestMatchers(/api/aspectLog/**).hasAnyRole(LOG_ADMIN, SYSTEM_ADMIN)同时确保数据库里用户的角色值在通过UserDetailsService加载时能被正确映射为ROLE_LOG_ADMIN或ROLE_SYSTEM_ADMIN。这里有个原则我一直强调精确控制只放行需要的接口而不是放行整个 URL 段。很多人图省事把/api/**全部permitAll结果各种越权问题接踵而至。授权规则宁可多写几条也不要图省事一放一大片。4.2 方案B修正方法级校验逻辑如果问题出在方法级安全注解上先看两件事第一有没有启用方法安全。Spring Security 6.x 用EnableMethodSecurity5.x 用EnableGlobalMethodSecurity(prePostEnabled true)。没启用就赶紧补上。第二注解里的表达式和你实际的权限体系是否一致。如果权限标识是sys:aspectLog:list这种权限点就写PreAuthorize(hasAuthority(sys:aspectLog:list))如果是角色判断就明确用hasRole并保证角色前缀一致。修正时不要只在 Controller 方法上加注解Service 层如果有同样的数据权限校验逻辑也要一并检查。重点排查那些“看起来能用但权限字符串对不上”的情况。4.3 方案C自定义统一异常响应前后端分离项目通常需要把 403 的响应体格式统一成自己的业务结构。正确做法是实现AccessDeniedHandler和AuthenticationEntryPoint而不是在ControllerAdvice里拦截异常。举个例子Component public class JsonAccessDeniedHandler implements AccessDeniedHandler { Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:403,\message\:\抱歉您没有权限访问该接口\}); } }然后在 SecurityConfig 里装配http.exceptionHandling(exception - exception .authenticationEntryPoint(jsonAuthenticationEntryPoint) .accessDeniedHandler(jsonAccessDeniedHandler) );这里要特别提醒如果业务代码里也会主动抛出AccessDeniedException那你可能确实需要在ControllerAdvice里加一个处理器兜底。但一定不要把它当作唯一方案否则过滤链里抛出的安全异常根本不会走到这里。4.4 选型之前先问自己的三句话修复方案不是拍脑袋选的。我在改动配置前会先问自己三句话这个接口是真的应该被所有已登录用户访问还是只应该被特定角色/权限访问如果是后者permitAll就是错误的解法。前端拿到 403 之后要怎么处理是要跳回登录页还是弹出“无权限”提示这决定了我要配置AuthenticationEntryPoint还是AccessDeniedHandler以及两者返回的 HTTP 状态码是否要区分401 vs 403。如果我现在把这个接口放开最坏情况是什么会不会导致越权访问敏感数据这是安全底线想清楚了再做。没有通吃所有场景的方案也没有“改一处就能一劳永逸”的银弹。安全配置的核心思路永远是明确边界再写规则。5. 复盘与加固让“不允许访问”不再成为疑难杂症5.1 学会看DEBUG级别日志排查这类问题最有效的手段就是打开安全相关的日志。在application.yml里加logging: level: org.springframework.security: DEBUG然后重新请求一次你会看到类似这样的关键日志AuthorizationFilter - Authorized filter invocation [GET /api/aspectLog/list] AuthorizationManager - authorized ... with authorization decision ...这能直接告诉你请求命中了哪条规则、授权决策的结果是“允许”还是“拒绝”。比起盯着异常堆栈猜看日志的定位效率高得多。调试完记得把级别改回 INFODEBUG 日志量很大线上开着会刷爆日志文件。5.2 用测试固化权限行为权限这种逻辑光靠人工点页面验证远远不够。建议用SpringBootTestMockMvc把关键接口的授权行为固化下来比如SpringBootTest AutoConfigureMockMvc class SecurityAccessTest { Autowired private MockMvc mockMvc; Test void adminCanAccessAspectLogList() throws Exception { mockMvc.perform(get(/api/aspectLog/list) .with(user(admin).roles(ADMIN))) .andExpect(status().isOk()); } Test void normalUserCannotAccessAspectLogList() throws Exception { mockMvc.perform(get(/api/aspectLog/list) .with(user(normal).roles(USER))) .andExpect(status().isForbidden()); } }这类测试的意义在于以后谁改了授权规则运行一遍测试就知道有没有破坏原有权限边界。我在项目里推广这个做法之后因为误改权限规则导致的事故明显少了。5.3 升级版本时最容易踩的隐藏变更点最后聊一下版本升级带来的“隐藏炸弹”。如果你最近把项目从 Spring Boot 2.x 升到 3.x下面几个变更点要特别注意antMatchers、mvcMatchers全部换成了requestMatchers升级时 IDE 会提示但很多人只改了 URL 字符串忽略了匹配语义的变化。WebSecurityConfigurerAdapter被废弃配置方式变成了SecurityFilterChainBean。很多老教程的代码直接复制会编译不过然后有人就硬改反而把原有规则弄乱。EnableGlobalMethodSecurity换成了EnableMethodSecurity这个之前提过少加一个注解所有PreAuthorize静默失效接口“意外开放”比“意外拒绝”更可怕。csrf、cors这些配置的 DSL 风格也变了升级后最好重新审视一遍安全配置而不是只保证“能编译通过”。我的建议是升级 Spring Security 大版本一定要单独安排一个“安全回归测试”的排期把认证、授权、CSRF、CORS、方法安全这几个维度都过一遍别指望运行期自己发现。这两年处理过不下十几起AccessDeniedException的报错回过头看绝大多数问题都出在“对机制理解不透 排查方向不聚焦”上。这个异常本身的含义很明确——安全框架在告诉你当前用户已经认识但权限不够。你只要沿着“认没认证、匹配哪条规则、角色前缀对不对、是不是 CSRF 冒名、异常是不是从过滤器链抛出来的”这个思路去查基本都能在一个小时内定位到根因。别一上来就质疑框架、别无脑加白名单先看清楚异常是谁抛的、在哪个环节抛的再动手改。
延伸阅读

更多相关文章

2026/10/10 20:45:48

声发射AE波形模拟:MATLAB衰减正弦模型与特征提取实战

做声发射检测的人一定绕不开一个操作——把传感器贴到试件上,然后盯着示波器上那一串跳出来的毛刺。理论上这就是声发射波形,但实际工程里你很难凭运气等到一个标准的单峰事件:现场有电磁干扰、结构噪声、多源叠加,很多时候你抓到…

2026/10/10 20:45:48

OpenCV+Python车牌识别实战:源码拆解、模型训练与避坑指南

简介:基于OpenCV与Python的车牌识别系统完整工程代码,面向具备一定图像处理基础的学生和开发者,适合课程设计、毕业设计或真实场景中的车牌识别原型搭建。压缩包共25个文件,主要包含Python脚本、SVM模型数据、中英文训练数据集以及…

2026/10/10 21:55:54

MCP协议握手到LangGraph多Server调用:从原理到实战全解析

MCP 这个圈子今年是真的热闹,光是“mcp是什么”这类搜索就天天有人在问。但光知道概念没用,真到自己上手把 MCP Server 接进 LangGraph 流程里,你会发现坑全藏在细节里——尤其是从协议握手到多 Server 调用这一段,文档写得云里雾…

2026/10/10 21:55:54

LangGraph 多 MCP Server 接入实战:协议握手到编排避坑

接手这个分享主题前,我先说句实在的:MCP(Model Context Protocol,模型上下文协议)最近在圈里确实热得发烫,但大部分教程都停留在“跑通一个 Server”的阶段,真正到“多个 Server 同时接入、交给…

2026/10/10 21:55:54

情绪桶、关键词检索与自动学图:dsh-meme 进阶用法拆解

dsh-meme 装完能发图,但真正决定它好不好用的,是你会不会用它的两个工具:send_meme 和 learn_meme。前者负责「取候选」,后者负责「收图入库」。这篇不讲安装,只讲这两个工具怎么组合、情绪桶和关键词检索各在什么时候…

2026/10/10 21:55:54

SWAT模型Sobol与PAWN敏感性分析对比与Matlab实现

做SWAT模型的人,多少都有过同样的体验:参数多到让你怀疑人生,几十个水文、土壤、植被参数堆在一起,率定工具一跑就是整夜,最后还说不清到底是哪个参数起了决定作用。我早期也干过靠“经验”猜参数优先级的事&#xff0…

2026/10/10 21:55:54

326.安卓 PBL/XBL/ABL 完整启动链拆解,揭秘刷机底层逻辑

摘要 本文面向具备一定计算机基础的开发者与维修工程师,系统讲解安卓手机刷机与维修的底层原理、分区结构、引导流程、刷机协议及实战案例。文章从Fastboot与EDL两种核心模式切入,结合高通平台实例,提供完整可运行的Python脚本用于自动化解析分区表与刷写镜像,并给出常见故…

2026/10/10 21:50:53

Python手写PL0编译器:从词法分析到递归下降的完整实现与避坑指南

简介:NUAA编译原理课设的PL0编译器Python实现,面向计算机专业学生及编译原理入门者。资源覆盖词法分析、语法分析、语义分析及后端代码生成等完整流程,适合需要从零构建编译器的课程设计参考,也可作为自学编译原理的动手范例。压缩…

2026/10/10 7:31:36

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/9 20:15:56

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/8 6:05:44

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 0:04:53

从逻辑门到计算机:数字电路核心原理与全加器搭建实战

如果你拆过一台旧电脑的主板,盯着那些黑乎乎的小芯片看上一会儿,可能会冒出同一个疑问:这堆引脚密集的元件,到底是怎么“变”出那么复杂的应用的?答案并不在某个神秘的部件里,而是在所有芯片内部都在反复使…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑