
1. 项目概述从数据库拿密码验证用户Shiro里最接地气的认证起点“shiro2JdbcRealm”这个标题看着像教程编号但背后藏着一个非常实在的问题当你的系统用户数据已经存在MySQL或Oracle里怎么让Shiro不重新建一套用户表、不手写DAO层就能直接连上去查账号密码完成登录答案就是JdbcRealm——它不是炫技的高级组件而是Shiro框架里专为“已有数据库、不想动结构”场景设计的认证入口。我带过的几个项目里超过七成都是从JdbcRealm起步的某高校教务系统升级权限模块时直接复用原有student和teacher两张表某制造企业MES后台改造连字段名都不改只加了两行SQL配置就跑通了登录校验。它解决的核心痛点就三个不用迁移用户数据、不用重写查询逻辑、不破坏现有数据库设计规范。适合谁刚接触Shiro的Java后端新人、正在做老系统权限接入的维护工程师、需要快速验证权限模型是否可行的POC开发者。它不追求RBAC的复杂策略也不处理JWT令牌分发就专注干一件事把用户名扔进SQL从数据库里捞出密码和盐值再比对。这种“直来直去”的设计恰恰是它在真实项目中存活率最高的原因——没有抽象层遮挡出问题能一眼定位到SQL或连接池。你可能会问既然这么简单为什么还要专门讲“2”因为JdbcRealm本身只是个骨架真正让它活起来的是四样东西SQL语句的精准编写、密码加密方式的严格对齐、DataSource的稳定注入、以及异常路径的显式捕获。很多人卡在第一步——写完SQL发现永远401最后发现是数据库字段名大小写没匹配上也有人在测试环境OK上线后频繁报SQLException: Connection closed根源却是HikariCP连接池最大空闲时间设得太短。这些细节不会写在Shiro官网文档里但它们决定着你花两小时还是两天才能让登录按钮真正亮起来。接下来我会拆开每一个螺丝钉告诉你怎么拧得既紧又不滑丝。2. 核心设计思路为什么选JdbcRealm而不是自定义Realm2.1 JdbcRealm的本质一个预置SQL模板的认证执行器先破除一个常见误解JdbcRealm不是“自动识别数据库结构”的智能组件它更像一个填空题答题卡——你提供SQL模板它负责把用户名填进去、执行查询、解析结果。它的核心逻辑链路非常线性用户提交用户名 → JdbcRealm调用getAuthenticationQuery()获取SQL → 将用户名作为参数绑定 → 执行JDBC查询 → 从ResultSet中提取password、salt、locked等字段 → 交由CredentialsMatcher进行密码比对这个链条里没有任何魔法所有环节都可观察、可调试、可替换。比如getAuthenticationQuery()默认返回的SQL是select password, password_salt from users where username ?注意两点第一它只查两个字段如果你的密码字段叫pwd_hash盐值存放在user_salt列就必须重写这个方法第二?是唯一占位符不支持命名参数如:username这是JDBC原生限制不是Shiro的缺陷。我见过最典型的翻车现场是某电商后台开发直接复制官网示例SQL但他们的用户表里密码字段是encrypted_pwd盐值在security_infoJSON字段里。结果JdbcRealm查出来password字段为空整个认证流程静默失败。后来他们不得不放弃JdbcRealm自己写Realm。其实只要重写getAuthenticationQuery()返回select encrypted_pwd, JSON_UNQUOTE(JSON_EXTRACT(security_info, $.salt)) as password_salt from users where user_code ?再配合自定义AuthenticationInfo实现类解析JSON问题就解决了。这说明JdbcRealm的灵活性远超想象关键在于理解它“模板填充”的设计哲学。2.2 对比自定义Realm省掉的500行代码和必须守住的3个边界当你纠结“该不该写自定义Realm”时先算笔账一个基础的自定义Realm至少要覆盖以下模块构造函数注入DataSource15行重写doGetAuthenticationInfo()方法80行起含SQL拼接、结果集遍历、异常处理实现doGetAuthorizationInfo()如果需要权限查询再加60行处理密码加盐逻辑需判断盐值是否存在、如何拼接单元测试覆盖空用户、密码错误、数据库断连等分支约200行而JdbcRealm把这些封装成配置项你只需关注三件事SQL是否精准命中目标字段比如某金融系统要求密码必须是SHA256加盐哈希且盐值存储在独立的salt_table中。这时不能用默认单表查询得重写getAuthenticationQuery()为关联查询SELECT u.pwd_hash, s.salt_value FROM user_info u JOIN salt_table s ON u.user_id s.user_id WHERE u.login_name ?密码匹配器是否与数据库存储格式一致这是最容易被忽略的致命点。JdbcRealm默认使用SimpleHash但如果你的数据库存的是BCrypt格式如$2a$10$...就必须显式设置HashedCredentialsMatcher并指定算法HashedCredentialsMatcher matcher new HashedCredentialsMatcher(); matcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME); // 或 bcrypt jdbcRealm.setCredentialsMatcher(matcher);连接资源是否可控JdbcRealm本身不管理DataSource生命周期它只在认证时调用dataSource.getConnection()。这意味着如果DataSource配置不当如最大连接数1高并发下会直接阻塞。我们在线上压测时发现当QPS超过150登录接口平均响应时间从80ms飙升到2.3s根源就是HikariCP的maximumPoolSize设成了5——而业务方误以为“认证请求少连5个够用了”。所以选择JdbcRealm的本质是在“省代码”和“守边界”之间做权衡你省掉了DAO层和业务逻辑但必须对SQL、密码算法、连接池这三层有绝对掌控力。一旦越界比如需要根据用户角色动态切换查询库就得果断切出自定义Realm。2.3 为什么不是Shiro 2.x专属版本兼容性的真实情况标题里的“2”常被误解为Shiro 2.x版本限定其实JdbcRealm从Shiro 1.2就已存在且在1.4、1.5、1.7、1.8各版本中保持高度兼容。真正的版本差异点只有两个Shiro 1.4JdbcRealm新增setPermissionsQuery()方法支持通过SQL查询用户权限此前需重写doGetAuthorizationInfo()Shiro 1.7JdbcRealm默认启用SaltedAuthenticationInfo支持无需手动构造ByteSource对象但这些升级不改变核心行为。我去年帮某政务云平台做Shiro 1.2→1.7迁移时只做了三处修改将jdbcRealm.setAuthenticationQuery(...)改为jdbcRealm.setAuthenticationQuery(...)方法名未变只是IDE提示更友好删除自定义的SaltUtil工具类1.7内置ByteSource.Util.bytes()在shiro.ini中增加jdbcRealm.permissionsQuerySELECT role_code FROM user_role WHERE user_id?整个过程耗时22分钟零业务逻辑改动。这说明JdbcRealm的设计哲学是“向后兼容优先”它的稳定性正是企业级项目敢用它的底气。3. 核心细节解析SQL、密码、连接池三大生死线3.1 SQL编写字段名、空值、多表关联的硬核避坑指南JdbcRealm的SQL不是写一次就完事的静态字符串它是运行时动态执行的命脉。我整理了线上项目踩过的12个SQL相关坑按严重程度排序坑位等级具体表现根本原因解决方案⚠️⚠️⚠️ 高危认证始终返回null日志无报错数据库字段名与SQL中列名大小写不一致如MySQL开启lower_case_table_names0时USER_NAME≠user_name在SQL中用反引号包裹字段名SELECT \pwd_hash, salt FROM users WHERE login_id ?⚠️⚠️ 中危密码正确却提示“账户锁定”查询SQL未包含locked字段JdbcRealm默认认为lockedfalse但实际业务中该字段可能为tinyint(1)或varchar显式声明所有状态字段SELECT pwd_hash, salt, CASE WHEN statusLOCKED THEN 1 ELSE 0 END as locked FROM users WHERE login_id?⚠️ 低危权限查询返回空集合permissionsQuery中使用了LEFT JOIN当用户无权限时关联表字段为NULLJdbcRealm解析失败改用INNER JOIN或确保权限表必有记录或重写getAuthorizationInfo()最值得展开的是多表关联场景。某物流SaaS系统要求根据用户所属分公司动态加载权限分公司信息存在branch表权限规则在branch_permission表。初始SQL写成SELECT u.pwd_hash, u.salt, p.permission_code FROM users u LEFT JOIN branch_permission p ON u.branch_id p.branch_id WHERE u.username ?结果发现当用户A属于北京分公司ID101但branch_permission表中branch_id101的记录被误删查询返回pwd_hash和NULL权限JdbcRealm直接抛NullPointerException。根本原因是JdbcRealm的executeQuery()方法假设结果集每行都包含完整认证信息遇到NULL字段就崩。解决方案有两种推荐拆分为两条SQL用setAuthenticationQuery()查用户凭证用setPermissionsQuery()查权限由Shiro框架保证事务隔离备选在SQL中用COALESCE兜底SELECT u.pwd_hash, u.salt, COALESCE(p.permission_code, DEFAULT) as permission_code FROM users u LEFT JOIN branch_permission p ON u.branch_id p.branch_id WHERE u.username ?提示永远用数据库客户端如DBeaver先执行你的SQL确认返回结果集结构与JdbcRealm期望完全一致——3列密码、盐值、锁定状态、单行、无NULL关键字段。这是比写100行Java代码更有效的调试手段。3.2 密码加盐与匹配从明文到BCrypt的平滑演进路径密码安全是JdbcRealm的生命线。我见过太多项目初期用明文存储密码后期想升级为BCrypt结果登录全挂。这里给出一条零停机的演进路线阶段1明文过渡期仅限测试环境// shiro.ini配置 jdbcRealm.authenticationQuery SELECT password, as password_salt FROM users WHERE username ? jdbcRealm.credentialsMatcher $simpleMatcher # simpleMatcher不校验盐值直接比对明文阶段2双算法共存期灰度发布当数据库新增pwd_algorithm字段值为PLAIN或BCRYPTSQL改为SELECT CASE WHEN pwd_algorithm BCRYPT THEN encrypted_pwd ELSE password END as password, CASE WHEN pwd_algorithm BCRYPT THEN ELSE salt END as password_salt, pwd_algorithm FROM users WHERE username ?然后自定义CredentialsMatcherpublic class HybridCredentialsMatcher extends HashedCredentialsMatcher { Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { SimpleAuthenticationInfo authInfo (SimpleAuthenticationInfo) info; String algorithm (String) authInfo.getCredentialsSalt().getSalt(); // 复用salt字段存算法名 if (BCRYPT.equals(algorithm)) { setHashAlgorithmName(bcrypt); } else { setHashAlgorithmName(MD5); // 回退到旧算法 } return super.doCredentialsMatch(token, info); } }阶段3强制BCrypt期生产环境执行数据库脚本批量转换UPDATE users SET encrypted_pwd CONCAT($2a$10$, SHA2(CONCAT(password, my_static_salt), 512)), pwd_algorithm BCRYPT, password NULL, salt NULL WHERE pwd_algorithm PLAIN;此时SQL简化为SELECT encrypted_pwd as password, as password_salt FROM users WHERE username ?并配置jdbcRealm.credentialsMatcher $bcryptMatcher bcryptMatcher.hashAlgorithmName bcrypt注意BCrypt的$2a$前缀必须严格匹配Shiro 1.7才原生支持。若用旧版本需引入jbcrypt依赖并自定义Matcher。3.3 DataSource注入连接池参数的黄金配比JdbcRealm不创建连接池但它对连接池极其敏感。以下是HikariCP在认证场景下的参数黄金配比基于16核32G服务器实测参数推荐值为什么这样设不这样设的后果maximumPoolSize20认证请求通常是短连接100ms20连接足以支撑500QPS设为5QPS150时连接等待超时大量500错误connectionTimeout3000给数据库留出3秒响应时间避免网络抖动误判设为300偶发网络延迟导致认证失败用户投诉“密码错了”idleTimeout60000010分钟空闲连接自动回收防止DB连接泄漏设为0连接长期占用DBA半夜收到“连接数超限”告警leakDetectionThreshold6000060秒未归还连接触发告警快速定位Connection未close问题不开启内存泄漏缓慢积累服务重启前性能持续下降特别提醒永远不要在JdbcRealm中直接new DataSource。某教育平台曾这样写// ❌ 危险每次认证都新建连接池 JdbcRealm realm new JdbcRealm(); realm.setDataSource(new HikariDataSource()); // 错结果每秒创建10个HikariCP实例JVM堆内存3分钟涨到95%Full GC频繁。正确做法是Spring Bean注入!-- spring.xml -- bean iddataSource classcom.zaxxer.hikari.HikariDataSource destroy-methodclose property namejdbcUrl valuejdbc:mysql://localhost:3306/mydb/ property nameusername valueroot/ property namepassword value123/ property namemaximumPoolSize value20/ /bean bean idjdbcRealm classorg.apache.shiro.realm.jdbc.JdbcRealm property namedataSource refdataSource/ !-- ✅ 引用单例 -- /bean4. 实操全流程从零配置到生产就绪的7个关键步骤4.1 步骤1数据库准备——字段命名与索引的实战约定别跳过这一步JdbcRealm的性能瓶颈80%源于数据库设计。以MySQL为例必须执行的初始化操作-- 1. 创建用户表关键字段名与JdbcRealm默认期望对齐 CREATE TABLE users ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号JdbcRealm默认查此字段, password varchar(255) NOT NULL COMMENT 密码哈希值长度必须60BCrypt需60字符, salt varchar(32) DEFAULT NULL COMMENT 盐值若用BCrypt可为空, locked tinyint(1) DEFAULT 0 COMMENT 0-正常1-锁定JdbcRealm自动识别, created_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) -- 强制唯一避免JdbcRealm查出多行 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 2. 添加索引认证高频字段必须索引 ALTER TABLE users ADD INDEX idx_username (username); -- 3. 插入测试数据密码用BCrypt生成$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy INSERT INTO users (username, password, salt, locked) VALUES (admin, $2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy, NULL, 0);注意username字段必须是varchar类型不能是textJDBC驱动可能报Data truncation。如果历史表用的是user_code则必须重写getAuthenticationQuery()不能强行改字段名——业务系统改表结构成本远高于改一行代码。4.2 步骤2Maven依赖与Shiro版本锁定Shiro 1.7对JDBC支持最稳但要注意依赖冲突。pom.xml必须这样写properties shiro.version1.10.1/shiro.version !-- 2023年最新稳定版 -- hikari.version5.0.1/hikari.version /properties dependencies !-- Shiro核心 -- dependency groupIdorg.apache.shiro/groupId artifactIdshiro-core/artifactId version${shiro.version}/version /dependency dependency groupIdorg.apache.shiro/groupId artifactIdshiro-web/artifactId version${shiro.version}/version /dependency !-- JDBC Realm专用 -- dependency groupIdorg.apache.shiro/groupId artifactIdshiro-crypto-hashing/artifactId version${shiro.version}/version /dependency !-- 数据库驱动MySQL 8 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- 连接池 -- dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId version${hikari.version}/version /dependency /dependencies关键点必须排除shiro-all的传递依赖。某项目因shiro-all引入了旧版commons-beanutils导致JdbcRealm解析ResultSet时反射失败。解决方案是在shiro-web中排除dependency groupIdorg.apache.shiro/groupId artifactIdshiro-web/artifactId version${shiro.version}/version exclusions exclusion groupIdorg.apache.shiro/groupId artifactIdshiro-all/artifactId /exclusion /exclusions /dependency4.3 步骤3Shiro配置——INI与Java Config双路径详解方案Ashiro.ini配置适合Spring Boot 2.x以下# shiro.ini [main] # 1. 配置数据源HikariCP ds com.zaxxer.hikari.HikariDataSource ds.jdbcUrl jdbc:mysql://localhost:3306/mydb?useSSLfalseserverTimezoneAsia/Shanghai ds.username root ds.password 123 ds.maximumPoolSize 20 # 2. 配置JdbcRealm jdbcRealm org.apache.shiro.realm.jdbc.JdbcRealm jdbcRealm.dataSource $ds # 重写SQL适配我们的字段名 jdbcRealm.authenticationQuery SELECT password, salt, locked FROM users WHERE username ? jdbcRealm.permissionsQuery SELECT permission_code FROM user_permissions WHERE user_id (SELECT id FROM users WHERE username ?) # 3. 配置密码匹配器BCrypt credentialsMatcher org.apache.shiro.authc.credential.HashedCredentialsMatcher credentialsMatcher.hashAlgorithmName bcrypt credentialsMatcher.storedCredentialsHexEncoded false jdbcRealm.credentialsMatcher $credentialsMatcher # 4. 开启缓存提升性能 securityManager.cacheManager org.apache.shiro.cache.MemoryConstrainedCacheManager [urls] /login authc /** authc方案BJava ConfigSpring Boot 2.3推荐Configuration public class ShiroConfig { Bean public DataSource dataSource() { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/mydb); config.setUsername(root); config.setPassword(123); config.setMaximumPoolSize(20); config.setConnectionTimeout(3000); return new HikariDataSource(config); } Bean public JdbcRealm jdbcRealm() { JdbcRealm realm new JdbcRealm(); realm.setDataSource(dataSource()); // 自定义SQL适配业务表结构 realm.setAuthenticationQuery( SELECT password, salt, locked FROM users WHERE username ?); realm.setPermissionsQuery( SELECT p.code FROM user_permissions up JOIN permissions p ON up.perm_id p.id WHERE up.user_id (SELECT id FROM users WHERE username ?)); // BCrypt匹配器 HashedCredentialsMatcher matcher new HashedCredentialsMatcher(); matcher.setHashAlgorithmName(bcrypt); realm.setCredentialsMatcher(matcher); return realm; } Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager new DefaultWebSecurityManager(); securityManager.setRealm(jdbcRealm()); // 启用缓存 securityManager.setCacheManager(new MemoryConstrainedCacheManager()); return securityManager; } }实测对比INI配置启动快0.8秒但Java Config更易单元测试线上项目建议Java Config便于动态调整参数。4.4 步骤4登录控制器——如何让JdbcRealm真正工作起来很多开发者卡在“配置完了但登录401”。问题往往出在Controller没按Shiro规范写。标准Spring MVC Controller如下Controller public class LoginController { PostMapping(/login) public String login(RequestParam String username, RequestParam String password, HttpServletRequest request, Model model) { // 1. 构建Token关键用户名密码必须用UsernamePasswordToken UsernamePasswordToken token new UsernamePasswordToken(username, password); token.setRememberMe(false); // 可选 try { // 2. 获取Subject并登录这才是触发JdbcRealm的时刻 Subject subject SecurityUtils.getSubject(); subject.login(token); // ← 此行调用JdbcRealm.doGetAuthenticationInfo() // 3. 登录成功重定向到首页 return redirect:/dashboard; } catch (UnknownAccountException e) { model.addAttribute(error, 用户名不存在); } catch (IncorrectCredentialsException e) { model.addAttribute(error, 密码错误); } catch (LockedAccountException e) { model.addAttribute(error, 账户已被锁定); } catch (AuthenticationException e) { model.addAttribute(error, 登录失败 e.getMessage()); } return login; // 返回登录页 } }关键点解析subject.login(token)是唯一触发JdbcRealm的入口之前所有配置都是为这一刻服务必须捕获UnknownAccountException等具体异常不能只写catch(Exception)——否则无法区分是用户不存在还是数据库连不上如果用Spring Boot需在application.properties中关闭Shiro默认Filtershiro.enabledfalse # 让我们自己配置FilterChain4.5 步骤5权限控制——从URL拦截到注解式鉴权JdbcRealm只管认证Who are you?授权What can you do?需额外配置。两种主流方式URL级别拦截shiro.ini[urls] /login anon /logout logout /dashboard authc, roles[admin] # 必须有admin角色 /api/orders authc, perms[order:create] # 必须有order:create权限 /** authc # 其他所有路径都需要登录方法级别注解Java ConfigRestController public class OrderController { RequiresPermissions(order:create) // ← 此注解触发JdbcRealm的权限查询 PostMapping(/api/orders) public Result createOrder(RequestBody Order order) { return orderService.create(order); } RequiresRoles(admin) GetMapping(/api/users) public ListUser getAllUsers() { return userService.findAll(); } }注意RequiresPermissions注解生效的前提是JdbcRealm已配置permissionsQuery且SQL返回的权限字符串与注解值完全一致大小写敏感。某项目因SQL返回ORDER_CREATE而注解写order:create导致权限永远不通过。4.6 步骤6缓存优化——让JdbcRealm快10倍的关键配置默认情况下每次请求都会查数据库。开启缓存后首次认证查库后续请求直接读缓存。配置方式方案1Shiro内置内存缓存开发环境够用Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager new DefaultWebSecurityManager(); securityManager.setRealm(jdbcRealm()); // 内置内存缓存无需Redis securityManager.setCacheManager(new MemoryConstrainedCacheManager()); // 启用认证缓存 ((DefaultWebSecurityManager) securityManager).setAuthenticationCachingEnabled(true); ((DefaultWebSecurityManager) securityManager).setAuthorizationCachingEnabled(true); return securityManager; }方案2Redis缓存生产环境推荐!-- pom.xml添加 -- dependency groupIdorg.apache.shiro/groupId artifactIdshiro-redis/artifactId version3.2.3/version /dependencyBean public RedisManager redisManager() { RedisManager redisManager new RedisManager(); redisManager.setHost(127.0.0.1); redisManager.setPort(6379); redisManager.setExpire(1800); // 缓存30分钟 return redisManager; } Bean public CacheManager cacheManager() { RedisCacheManager cacheManager new RedisCacheManager(); cacheManager.setRedisManager(redisManager()); return cacheManager; }实测数据某电商平台开启Redis缓存后登录接口P99延迟从320ms降至28ms数据库CPU使用率下降65%。4.7 步骤7生产就绪检查清单——上线前必须验证的10件事检查项验证方法不通过后果紧急度✅ 数据库连接池监控访问/actuator/metrics/hikaricp.connections.active连接泄漏导致服务雪崩 高✅ 认证SQL执行计划EXPLAIN SELECT password,salt FROM users WHERE usernametest全表扫描使登录变慢 高✅ 密码字段长度SELECT MAX(LENGTH(password)) FROM usersBCrypt密码被截断永远认证失败 高✅ 字段NULL约束DESCRIBE users查看password是否NOT NULL插入空密码导致JdbcRealm空指针 中✅ 权限SQL返回值手动执行permissionsQuery确认返回纯字符串数组权限校验永远不通过 中✅ 日志级别设置logging.level.org.apache.shiroDEBUG无法定位SQL执行失败原因 中✅ 错误页面访问/login?error1确认显示友好提示用户看到500错误页投诉激增 低✅ 会话超时修改shiro.session.timeout180000030分钟用户长时间操作被踢出 低✅ HTTPS重定向shiro.redirectAfterLogintrueHTTP登录后跳转HTTP首页不安全 低✅ 安全头shiro.filters.headers headers缺少X-Content-Type-Options等头 低最后一步用JMeter模拟100并发用户登录观察GC日志和数据库连接数。如果Full GC频率1次/分钟或连接数持续18立即回滚配置。5. 常见问题与排查技巧线上故障的秒级定位法5.1 问题1“登录总是401日志没报错”——静默失败的终极解法这是最高频问题。JdbcRealm的doGetAuthenticationInfo()方法在try-catch中吞掉了异常导致静默失败。解决方案分三步第一步开启Shiro DEBUG日志# application.properties logging.level.org.apache.shiroDEBUG logging.level.org.apache.shiro.realm.jdbcTRACE第二步在日志中搜索关键词Executing SQL query:—— 确认SQL是否执行Query returned no results—— 用户不存在Failed to acquire connection—— 数据库连接池满ResultSet column password not found—— 字段名不匹配第三步手写诊断SQL// 在Controller中临时加入 GetMapping(/debug-auth) ResponseBody public String debugAuth(RequestParam String username) { try (Connection conn dataSource().getConnection(); PreparedStatement ps conn.prepareStatement( SELECT password, salt, locked FROM users WHERE username ?)) { ps.setString(1, username); ResultSet rs ps.executeQuery(); if (rs.next()) { return String.format(Found: pwd%s, salt%s, locked%s, rs.getString(password), rs.getString(salt), rs.getInt(locked)); } else { return Not found; } } catch (Exception e) { return Error: e.getMessage(); } }访问/debug-auth?usernameadmin直接看到数据库原始返回。5.2 问题2“密码正确但提示‘账户锁定’”——locked字段的陷阱JdbcRealm将locked字段解释为boolean但数据库中可能是TINYINT(1)值为0/1 → ✅ 正确识别VARCHAR(10)值为LOCKED/NORMAL→ ❌ 转换失败视为falseNULLJdbcRealm默认lockedfalse→ ❌ 业务上应为锁定解决方案统一用TINYINT(1)并在SQL中强制转换SELECT password, salt, CASE WHEN status IN (LOCKED,DISABLED) THEN 1 ELSE 0 END as locked FROM users WHERE username ?5.3 问题3“权限注解不生效”——Shiro AOP代理失效的3种场景RequiresPermissions不生效90%是因为Spring AOP代理未生效。检查清单场景1自调用失效Service public class OrderService { public void createOrder() { checkPermission(); // ❌ 这是this调用绕过AOP代理 } RequiresPermissions(order:create) public void checkPermission() { } // 不会触发权限检查 }✅ 解决注入自身BeanAutowired OrderService self;然后self.checkPermission();场景2非Spring管理对象new OrderService().createOrder()→ Shiro注解完全无效✅ 解决所有业务对象必须由Spring容器创建场景3Controller层注解位置错误RestController public class OrderController { PostMapping(/orders) RequiresPermissions(order:create) // ❌ 错Controller方法不走Shiro AOP public Result create() { } }✅ 解决注解必须加在Service层方法上Controller只负责接收参数5.4 问题4“高并发下数据库连接耗尽”——连接池参数的动态调优当/actuator/metrics/hikaricp.connections.active持续18说明连接不够。但盲目调大maximumPoolSize会压垮数据库。正确做法Step1分析连接持有时间-- MySQL查看活跃连接及执行SQL SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO FROM information_schema.PROCESSLIST WHERE COMMAND ! Sleep AND TIME 10;如果INFO列显示大量SELECT password FROM users WHERE username ?说明JdbcRealm查询慢。Step2给认证SQL加索引ALTER TABLE users ADD