SpringBoot整合Shiro权限管理实战:认证授权与缓存避坑指南
1. 权限框架选型背后的真实考量做后台系统绕不开登录和权限这两块我见过太多项目在这上面翻车。有的用拦截器加Session硬写代码散落在各个Controller里改一个权限规则要翻十几个文件有的直接上Spring Security结果被那一套过滤器链和配置类绕得晕头转向最后权限没管明白登录还时不时出问题。SpringBoot整合Shiro这套组合是我在中小型项目里用得最顺手、踩坑最少的方案没有之一。Shiro的核心优势在于它的抽象层次刚刚好。它不像Spring Security那样把认证和授权拆成十几个组件让你自己组装而是用SecurityManager、Subject、Realm三个核心概念就把事情说清楚了。Subject代表当前操作的用户SecurityManager是幕后总管Realm是连接你数据库的桥梁。你只需要告诉Shiro“怎么查用户”和“怎么查权限”剩下的会话管理、密码加密、记住我、验证码校验这些它全帮你兜住。这套方案适合谁如果你正在做一个后台管理系统、企业内部工具、或者任何需要区分管理员和普通用户权限的Web应用Shiro都能在半天内帮你把骨架搭起来。它不挑数据库不挑缓存实现甚至不挑Web框架SpringBoot只是让它更省事而已。我试过在一个老SSM项目里单独引入Shiro只加几个配置类就跑通了迁移成本极低。但要注意Shiro不是银弹。如果你的系统需要OAuth2、JWT无状态认证、或者微服务间的细粒度权限控制Shiro的原生支持会比较吃力需要大量自定义扩展。所以选型前先想清楚你是要做传统的Session-based后台系统还是要做前后端分离的Token认证前者Shiro是绝配后者建议看看Sa-Token或者Spring Security JWT的组合。2. 核心概念拆解与依赖配置2.1 Shiro三大核心组件到底在干什么很多人看Shiro文档时被SecurityManager和Subject的关系绕晕我用一个生活化的类比来解释。把整个权限系统想象成一家公司Subject就是来办事的员工每次进门都要刷卡SecurityManager是前台保安队长他手里有所有员工的档案和门禁规则Realm是人事部保安队长需要核实身份时就打电话给人事部查档案。你作为开发者只需要把人事部的档案整理好实现Realm保安队长会自动处理所有进出验证。具体到代码层面Subject通过SecurityUtils.getSubject()获取它封装了当前请求的用户信息。SecurityManager在SpringBoot中通常由ShiroFilterFactoryBean自动装配你不需要手动new。Realm是你唯一需要重点实现的部分继承AuthorizingRealm后重写doGetAuthenticationInfo认证和doGetAuthorizationInfo授权两个方法即可。这里有个关键细节Shiro的认证和授权是分开走的。认证只发生在登录那一刻授权则在每次访问受保护资源时都会触发。这意味着如果你的权限数据查询很慢每次请求都会拖慢响应。我一般会在Realm的授权方法里加一层缓存用Redis或者Caffeine都行后面会详细说。2.2 SpringBoot项目中的依赖引入与版本选择依赖这块有个坑我踩过Shiro的starter包和原生包混用会导致Bean冲突。正确的做法是只引入shiro-spring-boot-web-starter它会自动帮你装配SecurityManager和ShiroFilter。如果你用的是前后端分离项目不需要Session那就引入shiro-spring-boot-starter然后手动配置过滤器。dependency groupIdorg.apache.shiro/groupId artifactIdshiro-spring-boot-web-starter/artifactId version1.13.0/version /dependency版本选择上1.13.0是目前稳定版修复了之前几个反序列化漏洞。不要用1.7以下的版本有已知安全问题。另外注意如果你的项目用了SpringBoot 3.xShiro的jakarta包需要单独引入因为javax.servlet已经改成jakarta.servlet了。这个坑我在升级项目时遇到过启动直接报ClassNotFoundException排查了半天才发现是包名问题。提示引入starter后Shiro会自动注册一个默认的ShiroFilterChainDefinition但通常你需要自定义过滤链规则所以建议还是手动配置一个ShiroFilterChainDefinition Bean。3. 认证流程的完整实现与细节打磨3.1 自定义Realm的认证逻辑怎么写才稳认证的核心在doGetAuthenticationInfo方法。这个方法接收一个AuthenticationToken里面装着用户提交的用户名和密码。你的任务是根据用户名去数据库查用户如果查不到就抛UnknownAccountException如果查到了把数据库里的密码和盐值封装成SimpleAuthenticationInfo返回Shiro会自动帮你比对密码。Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken (UsernamePasswordToken) token; String username upToken.getUsername(); User user userMapper.findByUsername(username); if (user null) { throw new UnknownAccountException(用户不存在); } if (user.getStatus() 0) { throw new LockedAccountException(账号已被锁定); } ByteSource salt ByteSource.Util.bytes(user.getSalt()); return new SimpleAuthenticationInfo(user, user.getPassword(), salt, getName()); }这里有几个容易出问题的地方。第一盐值必须和注册时使用的一致否则密码永远比对不上。我见过有人注册时用随机盐存库登录时却用固定盐结果怎么都登不进去。第二SimpleAuthenticationInfo的第一个参数传的是user对象这个对象会在授权阶段被用到所以最好把用户ID和用户名都放进去。第三异常类型要区分清楚UnknownAccountException和IncorrectCredentialsException在前端要展示不同的提示前者是“用户不存在”后者是“密码错误”但出于安全考虑很多项目会统一提示“用户名或密码错误”防止攻击者枚举用户名。3.2 密码加密与盐值处理的正确姿势密码绝对不能明文存库这是底线。Shiro提供了HashedCredentialsMatcher支持MD5、SHA-256等算法。但单纯MD5已经不安全了彩虹表一查就出来。正确的做法是每个用户生成一个随机盐值用SHA-256加盐哈希后存库。public class PasswordUtil { public static String generateSalt() { SecureRandom random new SecureRandom(); byte[] salt new byte[16]; random.nextBytes(salt); return Base64.getEncoder().encodeToString(salt); } public static String encrypt(String password, String salt) { return new SimpleHash(SHA-256, password, ByteSource.Util.bytes(salt), 1024).toHex(); } }注意这里的1024是迭代次数意味着哈希函数要跑1024次。这个数字越大越安全但登录时的计算耗时也越长。我实测下来1024次在普通服务器上大约耗时50毫秒用户体验可以接受。如果你追求更高安全性可以调到10000次但登录接口的响应时间会明显增加需要权衡。配置HashedCredentialsMatcher时要把算法名称、迭代次数和是否加盐都设置好然后注入到自定义Realm中。这里有个细节如果你在Realm里手动返回了带盐的SimpleAuthenticationInfo那么CredentialsMatcher会自动从里面取盐不需要额外配置salt参数。3.3 登录接口与Session管理的配合登录接口本身很简单构造UsernamePasswordToken调用subject.login()捕获异常做相应处理。但Session管理有几个坑要注意。Shiro默认会创建Session如果你在前后端分离项目里不需要Session要记得关闭。另外Session超时时间默认是30分钟可以在application.yml里配置。PostMapping(/login) public Result login(RequestBody LoginDTO dto) { Subject subject SecurityUtils.getSubject(); try { subject.login(new UsernamePasswordToken(dto.getUsername(), dto.getPassword())); return Result.success(登录成功); } catch (UnknownAccountException e) { return Result.fail(用户名或密码错误); } catch (LockedAccountException e) { return Result.fail(账号已被锁定); } catch (AuthenticationException e) { return Result.fail(认证失败); } }登录成功后用户信息会存在Session里后续请求Shiro会自动从Session中恢复Subject。这里有个性能优化点如果用户信息经常变可以在每次请求时刷新Session中的用户对象否则会出现权限更新后不生效的问题。我一般会在Realm的授权方法里重新查一次用户权限而不是依赖Session里的缓存。4. 授权控制的多种实现方式对比4.1 基于过滤链的URL级别权限控制这是最粗粒度的权限控制在ShiroConfig里定义过滤链规则。比如/login和/register放行/admin/**需要admin角色/user/**需要登录。这种方式的优点是配置集中一眼就能看出整个系统的权限布局缺点是粒度太粗无法控制到具体按钮或数据行。Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition chain new DefaultShiroFilterChainDefinition(); chain.addPathDefinition(/login, anon); chain.addPathDefinition(/register, anon); chain.addPathDefinition(/admin/**, authc, roles[admin]); chain.addPathDefinition(/**, authc); return chain; }过滤链的匹配顺序是从上到下一旦匹配到就停止。所以/**必须放在最后否则后面的规则永远不会生效。这个坑我见过不止一次有人把/**放在第一行结果所有接口都要求登录连登录接口本身都被拦截了直接死循环。4.2 注解驱动的细粒度权限控制在Controller方法上加RequiresPermissions或RequiresRoles注解可以实现方法级别的权限控制。这种方式比过滤链灵活得多适合控制具体操作。但要注意注解生效需要开启Shiro的AOP支持在ShiroConfig里注册AuthorizationAttributeSourceAdvisor。RequiresPermissions(user:delete) DeleteMapping(/user/{id}) public Result deleteUser(PathVariable Long id) { userService.deleteById(id); return Result.success(); }注解里的权限字符串建议遵循“资源:操作”的命名规范比如user:add、user:edit、user:delete。这样在数据库里存储和查询都很清晰。授权时Realm的doGetAuthorizationInfo方法返回用户拥有的所有权限字符串Shiro会自动比对。注意RequiresPermissions注解在同一个类中方法互相调用时不会生效因为AOP代理的是外部调用。如果你在Service层用注解要确保是通过Spring注入的代理对象调用而不是this调用。4.3 页面按钮级别的权限渲染后端控制住了接口权限前端页面上的按钮也要根据权限显示或隐藏。Thymeleaf有Shiro的方言包JSP也有对应的标签库。如果是前后端分离项目可以在登录成功后返回用户的权限列表前端用v-if或v-show控制按钮显示。button shiro:hasPermissionuser:delete onclickdeleteUser()删除/button这里有个原则前端隐藏按钮只是提升用户体验真正的安全防线在后端。我见过有人只在前端隐藏了删除按钮后端接口却没加权限校验结果用户直接调接口就把数据删了。所以后端注解和前端隐藏要同时做缺一不可。5. 缓存与Session持久化的实战配置5.1 用Redis缓存权限数据减轻数据库压力每次请求都查数据库获取权限列表在高并发场景下数据库扛不住。Shiro支持CacheManager可以无缝接入Redis。配置好后doGetAuthorizationInfo方法只在第一次请求时执行后续都走缓存。Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheManager redisCacheManager new RedisCacheManager(factory); return new ShiroRedisCacheManager(redisCacheManager); }这里需要自己实现一个ShiroRedisCacheManager把Shiro的Cache接口适配到Redis。网上有现成的实现核心就是实现get、put、remove、clear这几个方法。缓存key建议用“shiro:auth:” username的格式方便排查问题。缓存带来的问题是权限更新不及时。用户权限被修改后缓存里的旧权限还在需要手动清除。我一般会在权限修改的Service方法里调用shiroCache.remove(username)确保下次请求重新加载。如果权限变动不频繁也可以设置一个较短的过期时间比如5分钟让缓存自动失效。5.2 Session共享与序列化问题如果项目部署了多个节点Session需要共享否则用户请求打到不同节点会反复要求登录。Shiro的Session可以通过Redis持久化配置一个RedisSessionDAO即可。但这里有个大坑Session里的对象必须实现Serializable接口否则序列化会失败。Bean public SessionDAO sessionDAO(RedisTemplateString, Object redisTemplate) { RedisSessionDAO sessionDAO new RedisSessionDAO(); sessionDAO.setRedisTemplate(redisTemplate); sessionDAO.setExpire(1800); return sessionDAO; }我踩过的坑是User对象里包含了一个LocalDateTime字段序列化时抛异常。后来把所有时间字段改成String或者Long才解决。另外如果User对象关联了其他实体那些实体也要实现Serializable否则会连锁报错。建议Session里只存用户ID和用户名需要详细信息时再查数据库这样既减小序列化体积又避免各种序列化问题。6. 常见问题排查与避坑经验实录6.1 登录后立刻跳回登录页是什么原因这个问题我遇到过三次每次原因都不一样。第一次是过滤链配置顺序错了/**放在了最前面导致登录接口也被拦截。第二次是Session超时时间设置太短登录成功后Session立刻过期。第三次最隐蔽前端请求没有带上Cookie因为跨域配置里没设置withCredentials。排查思路先看浏览器控制台有没有Cookie再看后端日志里Session是否创建成功最后检查过滤链配置。如果是前后端分离项目还要确认CORS配置里allowCredentials(true)和allowedOrigins不能是*必须是具体域名。6.2 权限注解不生效的几种可能RequiresPermissions加了但没起作用通常是以下几个原因第一没有注册AuthorizationAttributeSourceAdvisorAOP没生效第二Controller没有被Spring代理比如手动new出来的对象第三权限字符串拼写错误数据库里存的是user:delete注解里写的是user:del。排查时可以在doGetAuthorizationInfo方法里打日志看看返回的权限集合里到底有没有那个权限。如果没有就是数据库查询的问题如果有但注解还是不生效那就是AOP配置的问题。6.3 密码比对失败的排查步骤密码比对失败是最常见的问题排查步骤我整理成了一张表排查项检查方法常见问题盐值是否一致对比注册和登录时的盐值注册用随机盐登录用固定盐加密算法是否一致检查HashedCredentialsMatcher配置注册用SHA-256配置写MD5迭代次数是否一致检查SimpleHash的迭代参数注册1024次配置1次数据库密码长度查看密码字段长度varchar(32)存不下SHA-256的64位结果空格和大小写检查用户名前后是否有空格前端传参带了空格我建议在开发阶段把加密后的密码和盐值打印出来和数据库里的值逐字符对比很快就能定位问题。6.4 记住我功能失效的处理Shiro的RememberMe功能依赖Cookie如果失效先检查Cookie是否被浏览器拦截。另外RememberMe的密钥要配置默认是随机生成的重启后旧Cookie就失效了。在ShiroConfig里设置一个固定的密钥即可。Bean public CookieRememberMeManager rememberMeManager() { CookieRememberMeManager manager new CookieRememberMeManager(); manager.setCipherKey(Base64.getDecoder().decode(固定的Base64密钥)); return manager; }密钥必须是Base64编码的长度有要求太短会报错。我一般用AES的128位密钥生成一次后固定写在配置里。注意不要把这个密钥提交到代码仓库放在环境变量或配置中心里。7. 从认证到授权的完整链路回顾整套流程跑下来核心链路是这样的用户提交用户名密码Controller构造Token调用subject.login()SecurityManager委托Realm的doGetAuthenticationInfo查库比对密码比对通过后创建Session。后续每次请求ShiroFilter根据过滤链判断是否需要认证需要则从Session恢复Subject然后根据注解或过滤链规则触发doGetAuthorizationInfo查权限比对通过才放行。这条链路上每个环节都有优化空间。认证环节可以加验证码、限制登录失败次数授权环节可以加缓存、支持动态权限Session环节可以持久化到Redis、支持多端登录互踢。我一般会先跑通基础版本再根据项目实际需求逐步叠加这些功能避免一上来就搞得太复杂反而容易出问题。最后分享一个我常用的调试技巧在ShiroConfig里把日志级别调到DEBUGShiro会打印每次认证和授权的详细过程包括匹配了哪条过滤链、查询了哪些权限、比对结果是什么。这个日志在排查权限问题时比任何断点都管用强烈建议在开发环境打开。

相关新闻

Python进阶:for循环与with语句的底层逻辑与工程实践

Python进阶:for循环与with语句的底层逻辑与工程实践

如果你已经能写出带if/else的 Python 脚本,多半会碰到这么两类让人抓狂的情况:一是用for遍历列表时总觉得“绕”,比如想同时拿下标又想要元素,或者遍历字典时被各种报错打懵;二是打开文件后忘了close(),程序…

2026/10/12 12:06:08 阅读更多 →
群论统一视角下的位置编码:GRAPE框架解析与复现

群论统一视角下的位置编码:GRAPE框架解析与复现

1. 从"位置"这件事说起:为什么Transformer需要位置编码如果你接触过Transformer架构,一定绕不开一个核心矛盾:自注意力机制本身是置换不变的。换句话说,你把输入序列里的词打乱顺序,注意力算出来的结果只是跟…

2026/10/12 12:06:08 阅读更多 →
Python采集腾讯新闻实例

Python采集腾讯新闻实例

前言 拿一个真实门户站当例子来练手,是很有价值的,因为门户站会把你在练习站上遇不到的问题一次性摆到面前:列表页和详情页要分两步抓、页面里混着广告和推荐位、内容可能由 JavaScript 渲染、站点还会不时改版。但正因为是真实站点&#xff…

2026/10/12 12:06:08 阅读更多 →

最新新闻

机器学习最小可行知识链:从公式到可调试代码的完整实践

机器学习最小可行知识链:从公式到可调试代码的完整实践

简介:本资源是邹博主讲的《机器学习全方位解析:从基础到实践》配套教学包,面向零基础入门者与希望系统夯实理论与工程能力的学习者,解决机器学习概念抽象、代码实践脱节、算法调优无从下手等核心痛点。压缩包共613个文件&#xff…

2026/10/12 13:00:09 阅读更多 →
Harbor v2.13.1 ARM64离线部署实战:内网镜像仓库搭建与避坑指南

Harbor v2.13.1 ARM64离线部署实战:内网镜像仓库搭建与避坑指南

简介:本资源为 Harbor 最新 v2.13.1 的 ARM64 架构离线安装包,面向在 ARM64 服务器或 Kubernetes 集群上部署私有镜像仓库的运维与开发人员,解决国产化平台、信创环境及边缘节点无法直接拉取官方 x86 安装包的问题。压缩包共 6 个文件&#x…

2026/10/12 13:00:09 阅读更多 →
物联网平台二次开发选型指南:架构边界、扩展点与实战避坑

物联网平台二次开发选型指南:架构边界、扩展点与实战避坑

1. 为什么“适合二开”才是物联网平台选型的硬指标做物联网平台选型这件事,我踩过不少坑。早年做某智慧园区项目,技术评审时大家盯着功能清单看——设备接入、规则引擎、数据大屏、告警中心,列表很丰满,厂商演示也很流畅。结果签完…

2026/10/12 13:00:09 阅读更多 →
批处理驱动Matlab实现参数自动扫描:原理、传参与排错指南

批处理驱动Matlab实现参数自动扫描:原理、传参与排错指南

说实话,用批处理去反复跑Matlab这事儿,我最早是在一次大规模参数寻优中被逼出来的。那时候手里有一堆实验数据要拟合,模型参数有四五种组合方式,每组参数跑一遍得几十秒,加起来几百个组合,手动改一个跑一个…

2026/10/12 13:00:08 阅读更多 →
PyTorch新手必学:ResNet18从原理到手写实现详解

PyTorch新手必学:ResNet18从原理到手写实现详解

1. 项目概述:为什么ResNet18是PyTorch新手绕不开的“第一座桥”如果你刚打开Jupyter Notebook,敲下import torch,接着想跑一个真正能分类图像的模型——不是MNIST手写数字那种玩具级任务,而是能在CIFAR-10上稳定达到92%准确率、结…

2026/10/12 13:00:08 阅读更多 →
LCA最近公共祖先算法详解:倍增法、Tarjan与树链剖分

LCA最近公共祖先算法详解:倍增法、Tarjan与树链剖分

1. 从一道面试题说起:为什么LCA值得花时间搞透第一次接触LCA是在刷题的时候,题目很直白:给一棵树,多次询问两个节点的最近公共祖先。当时我的第一反应是暴力——两个节点分别往上走,标记走过的路径,第一个相…

2026/10/12 12:59:08 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/11 14:36:54 阅读更多 →