6 个启动连环坑:从 MySQL 用户到前端 401 的完整链路打通

导读

编译跑绿后,启动链路又冒出 6 个连环坑:MySQL 没 building_* 用户、property-service 写死内网 IP、admin 种子用户密码是 SHA512×2、is_enable=1 居然是禁用、gateway 报 JWT NPE、前端访问报 401。本篇一次性梳理这 6 个坑的根因、修复方案、排查顺序,以及哪些是”反直觉的项目约定”。

🎧 文章导读

🎵 背景音乐

前言

环境编译通过只是开始。真正启动 admin-service 时,才发现问题像洋葱一样一层层剥开:连不上 MySQL → admin 用户密码哈希算法不对 → is_enable 字段语义反过来 → 启动后 NPE → 前端 401。

这一篇把 6 个修复合并在一起,因为它们是一条链上的——前一个修不完后一个根本看不到。

6 个连环坑链路示意
图1:从 MySQL 到 401 的启动链路

坑 1:MySQL 没有 building_* 用户

现象

1
2
3
CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; 
nested exception is java.sql.SQLException:
Access denied for user 'building_admin'@'localhost' (using password: YES)

根因

bootstrap-dev.yml 写死了 username: building_admin / password: building_admin@123,但 MySQL 里只有 root、travel、系统用户——从来没人建过 building_* 系列用户。

修复

8 个用户 + 授权 SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE USER IF NOT EXISTS 'building_admin'@'localhost'   IDENTIFIED BY 'building_admin@123';
CREATE USER IF NOT EXISTS 'building_bpm'@'localhost' IDENTIFIED BY 'building_bpm@123';
CREATE USER IF NOT EXISTS 'building_env'@'localhost' IDENTIFIED BY 'building_env@123';
CREATE USER IF NOT EXISTS 'building_biz'@'localhost' IDENTIFIED BY 'building_biz@123';
CREATE USER IF NOT EXISTS 'building_smart'@'localhost' IDENTIFIED BY 'building_smart@123';
CREATE USER IF NOT EXISTS 'building_through'@'localhost' IDENTIFIED BY 'building_through@123';
CREATE USER IF NOT EXISTS 'building_jobs'@'localhost' IDENTIFIED BY 'building_jobs@123';
CREATE USER IF NOT EXISTS 'building_nacos'@'localhost' IDENTIFIED BY 'building_nacos@123';

GRANT ALL PRIVILEGES ON building_admin.* TO 'building_admin'@'localhost';
GRANT ALL PRIVILEGES ON building_bpm.* TO 'building_bpm'@'localhost';
-- ... 共 8 个 GRANT

FLUSH PRIVILEGES;

[!warning] 关键发现
项目记录说”11 个数据库”,实际只有 8 个 unique

  • admin + aided 共用 building_admin
  • through + security 共用 building_through
  • property + warning 共用 building_biz

反直觉的项目约定——服务 ≠ 库

坑 2:property-service 写死内网 IP

property-service/src/main/resources/bootstrap-dev.yml 3 处 IP 全部指向 10.152.238.245(堡垒机内网):

字段 改前 改后
5 NACOS_SERVER 10.152.238.245:8848 127.0.0.1:8848
10 MYSQL_SERVER 10.152.238.245:3306 127.0.0.1:3306
14 Redis host 10.152.238.245 127.0.0.1

估计是某次开发连堡垒机调测忘了改回来。其他服务都是 127.0.0.1,唯独 property 是内网

property 内网 IP 残留配置
图2:property 写死内网 IP

坑 3:admin 种子用户密码算法

admin 起来后登录报 用户名或密码错误。一开始以为是 hash 算法不对——确实不对,但不是 BCrypt,是 SHA512 × 2 次 + 盐

加密逻辑(service-util/.../LoginPwdCryptoUtil.java

1
2
3
4
5
6
public static final int HASH_INTERATIONS = 2;

public static final String enCryptPassword(String password, String salt) {
return Hex.encodeHexString(
hash(StringUtils.getBytesUtf8(password), StringUtils.getBytesUtf8(salt), HASH_INTERATIONS));
}

算法总结:

1
2
password_hash = SHA512( SHA512(salt + password) )   转 hex 字符串
salt = Base64(32 字节随机)

Python 复现(已验证)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import hashlib, base64, os

password = "123456"
salt_bytes = os.urandom(32)
salt = base64.b64encode(salt_bytes).decode('utf-8')

h = hashlib.sha512()
h.update(salt_bytes)
h.update(password.encode('utf-8'))
hashed = h.digest() # 第一次

h = hashlib.sha512()
h.update(hashed)
hashed = h.digest() # 第二次

password_hash = hashed.hex() # 转 hex

插 admin 种子用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
INSERT INTO sys_user (
user_id, user_name, password, password_salt,
user_status, real_name, is_enable, tenant_id,
delete_flag, face_pic_status, face_pic_audit_status,
create_id, create_name, create_time,
update_id, update_name, update_time
) VALUES (
'1', 'admin',
'<上面算的 hash>', '<上面算的 salt>',
'1', 'admin', 0, '000000', -- is_enable=0 才是启用
'0', '0', '0',
'admin', 'admin', NOW(),
'admin', 'admin', NOW()
);

[!warning] 实战坑
real_name 填”超级管理员”被 MySQL 拒(Data too long),改 admin 才过——这列虽然 varchar(64) 但表可能是 utf8 而非 utf8mb4遇到这种错就缩短内容

坑 4:is_enable=1 居然是禁用

修完密码又报错 账号已被锁定。看源码(UserController.java:195):

1
2
3
if (sysUser.getIsEnable() != null && DictConstants.DISABLE_FLAG.equals(sysUser.getIsEnable())) {
return BaseResult.fail(null, "账号已被锁定,请联系管理员启用");
}

DictConstants.DISABLE_FLAG = "1"——**is_enable=1 居然是禁用的意思**。完全违反 Spring/Java 命名直觉。

字段 我以为 项目实际
is_enable = 0 禁用 启用
is_enable = 1 启用 禁用

[!warning] 锁定陷阱
密码错 5 次自动设 is_enable = "1" 锁定。调试期不要乱试密码——一旦锁了,要直接 SQL 改 is_enable=0,不然怎么试都”已被锁定”。

is_enable 反向语义示意
图3:is_enable=1 居然是禁用

坑 5:gateway 启动 3 个 NPE

现象

# Bean 触发原因
1 devServicRouteLocator CommonProperties.deployments 为 null
2 dynamicRouteNacosListener NacosConfigProperties bean 不存在(enabled: false 干掉了)
3 jwtTokenService JwtAuthProperties.jwts 为 null

根因

Nacos 拒绝带连字符的 group 名(控制台报”不允许非法字符”),但项目代码默认是 smart-building网关问 Nacos 拿 smart-building 组 → 404 → 配置全空

修复

bootstrap.yml 5 处 ${NACOS_GROUP:smart-building} 全部改成 smart_building

1
2
3
spring.cloud.nacos.discovery.group: smart_building
spring.cloud.nacos.config.group: smart_building
# ... shared-configs[0..2].group

JWT NPE 怎么挖出来的

第 3 个 NPE 的堆栈只指到 JwtTokenService.<init>:54,源码在 com.xxkj:common-auth jar 里。诊断路径

1
2
3
4
# 反编译 jar
javap -p -classpath common-auth-2.6.0-SNAPSHOT.jar com.xxkj.auth.service.JwtTokenService
javap -p -classpath ... com.xxkj.auth.properties.JwtAuthProperties
javap -v -classpath ... com.xxkj.auth.model.JwtAuthInfo # 找 @ConfigurationProperties 的 prefix

反编译结果:JwtAuthProperties 的 prefix = icfg.auth,字段 jwts 继承 BaseAuthInfo(含 secretaccessTokenExpirationrefreshTokenExpiration 等)。

Nacos 完整配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
icfg:
admin:
service-id: admin
auth:
jwts:
secret: smart-park-default-jwt-secret-please-change-in-production
accessTokenExpiration: 3600
refreshTokenExpiration: 86400
autoRefresh: true
singleLogin: false
issuer: smart-park
common:
deployments:
- serviceId: through-service
deployPort: 18134
# ... 共 11 个服务
feign-url:
aided: http://127.0.0.1:18100
admin: http://127.0.0.1:18200
through: http://127.0.0.1:18134
security: http://127.0.0.1:18135
iot: http://127.0.0.1:18350

坑 6:前端 401 不一定是 token 错

现象

前端 npm run dev 起来后点登录:

1
2
3
4
{
"status": 500,
"message": "Connection refused: no further information: /127.0.0.1:18200"
}

VUE_APP_GATEWAY 改成 http://localhost:10001(本地网关)后,报 401

根因

JwtAuthProperties.publicApiPatterns(反编译 com.xxkj:common-auth)是白名单——只有路径在这个白名单里,网关的 AccessControlFilter 才放行到下游。没配的话默认是空集 → 任何 /admin/** 请求都被网关直接 401。

修复

Nacos application.ymlpublicApiPatterns

1
2
3
4
5
6
icfg:
auth:
publicApiPatterns:
- /admin/user/login
- /admin/user/logout
- /admin/captcha/image

前端两处配套改

E:\nanwang\zhyq-admin - no module\.env.development 第 10 行:

1
2
- VUE_APP_GATEWAY = 'http://10.182.5.212:8080'
+ VUE_APP_GATEWAY = 'http://localhost:10001'

vue.config.js 第 39-41 行:

1
2
3
4
5
6
7
  '/gateway': {
target: process.env.VUE_APP_GATEWAY,
secure: false,
changeOrigin: true,
- // pathRewrite: { '^/gateway': '' }
+ pathRewrite: { '^/gateway': '' }
}

[!warning] 三种白名单的区别

  • loginRelaApis — Gateway 拦截这些路径,加 token 再返回
  • logoutRelaApis — Gateway 拦截这些路径,清除 token
  • publicApiPatterns免鉴权路径(不校验 token 就能访问)

登录接口要同时配 loginRelaApis + publicApiPatterns 才能正确工作。

6 个坑的链路总结

1
2
3
4
5
6
✅ MySQL 用户(坑1) ─→ ✅ property IP(坑2) ─→ ✅ admin 启动

❌ 密码错(坑3) ─→ ❌ is_enable=1 锁定(坑4) ─→ ❌ JWT NPE(坑5)


❌ 前端 401(坑6)

经验总结

排查顺序的智慧

6 个坑看似随机,但顺序排查效率最高:先连得上(用户、IP),再起得来(启动参数、JWT),最后访问得通(白名单、路由)。不要跳着来——后面的问题依赖前面的解决状态。

反直觉约定的来源

  • is_enable=1 是禁用:Blade 框架通用约定,但违反 Spring/Java 直觉
  • 密码是 SHA512×2:Blade 框架的旧版加密,未切到 BCrypt
  • 8 个库不是 11 个:项目历史变更

DictConstants.X 这种常量时,永远看常量、不要猜

401 ≠ token 失效

看到 401 第一反应是”token 失效”,但更常见的是白名单没配——尤其是登录接口本身被 401,是经典”白名单缺登录路径”症状。白名单问题优先排查

500 = 网关放行了

看到 500 + Connection refused: 18200是好消息——说明网关这层完全 OK,问题只在下游服务没起。别去改网关

javap 是 jar 反编译必杀器

JDK 自带 javap,反编译私有 jar 不需要额外装。常用命令:

1
2
3
javap -p ClassName              # 看方法签名
javap -v ClassName # 看字节码(含注解)
javap -classpath xxx.jar ClassName # 看 jar 里某个类

下一步

启动链路打通了,但 admin 启动后菜单还是空——sys_role / sys_resource 都没数据。下一篇讲怎么补 token、登录后权限、以及 980.sql 数据导入。