简介:这份资源面向希望用 Basic4Android 开发安卓应用的初学者与进阶开发者,聚焦 B4A 与 MySQL 数据库交互这一常见需求,提供登录注册功能的完整实现参考。压缩包共 53 个文件,约 1.84MB,包含 29 个 class、8 个 java、3 个 xml 等编译与配置产物,以及 3 个 bas 源码、3 个 bal 布局文件和 b4a 工程文件、apk 成品,覆盖从界面布局到数据库访问的完整工程结构。已有 714 人学习下载,说明该主题在安卓入门与数据库实战中具有较高参考价值。读者可从中了解 JDBC 连接 MySQL 的参数配置与连接管理、登录查询与注册插入的 SQL 构造方式,以及密码哈希、SQL 注入防护和异常处理等安全要点,适合作为课程设计或小型项目原型的起步模板,对照源码快速理解 B4A 调用数据库的完整链路。
1. 用 B4A 直连 MySQL 做登录注册:一个被低估的安卓原生方案
安卓端做登录注册,多数人第一反应是 Retrofit 加后端接口,中间隔一层 HTTP 服务。但如果你手上只有一台 MySQL 服务器、没有后端团队、又要快速交付一个内部工具或课程设计,B4A 直连 MySQL 这条路其实相当务实。B4A(Basic4Android)用 Basic 语法编译出原生 APK,自带 JDBC 驱动支持,能直接在安卓里开数据库连接、跑 SQL、拿结果集。这份资源就是围绕「B4A 连接 MySQL 实现登录注册」展开的完整实现,包含连接配置、账号表设计、注册写入、登录校验四块核心逻辑。适合会一点 Basic、想绕开后端中间层快速验证业务闭环的开发者,也适合拿它当安卓数据库编程的入门样本。下面按「能干什么 → 怎么配 → 怎么防坑」的顺序拆开讲。
2. 连接层怎么搭:JDBC 驱动、连接串与线程模型
2.1 为什么 B4A 能直连 MySQL,代价是什么
B4A 编译产物是原生 Android 应用,运行时是 JVM 环境,所以它能加载标准 JDBC 驱动。资源里的做法是把 MySQL Connector/J 的 jar 包通过 B4A 的附加库机制打进 APK,然后在代码里用DriverManager拿Connection。这条路的技术前提有三个:一是目标 MySQL 允许远程连接,二是账号有对应库的权限,三是安卓端能访问到数据库端口(默认 3306)。
代价必须说清楚。直连意味着数据库账号密码打包在 APK 里,反编译就能拿到;意味着每个用户端都占一条数据库连接,并发一上来 MySQL 的max_connections先扛不住;还意味着所有 SQL 逻辑暴露在客户端,注入面变大。所以这个方案的正确使用边界是:内网工具、单机演示、课程设计、用户量可控的小系统。真要做面向公网的产品,中间加一层接口服务是迟早的事。资源本身没做这层抽象,它把连接逻辑封装在一个模块里,方便你后续替换成 HTTP 调用。
2.2 驱动引入与连接串参数
先把驱动 jar 放进 B4A 的附加库目录,在项目的 Libraries 里勾选。连接串的写法直接决定能不能连上,常见配置如下:
' 初始化 JDBC 驱动,只需执行一次 Dim MySQLDriver As JavaObject MySQLDriver.InitializeStatic("com.mysql.jdbc.Driver") ' 连接参数:库名、时区、字符集、超时都要显式给 Dim Host As String = "192.168.1.100" Dim Port As Int = 3306 Dim DbName As String = "app_user" Dim User As String = "app_rw" Dim Pass As String = "YourStrongPass_2024" Dim Url As String = "jdbc:mysql://" & Host & ":" & Port & "/" & DbName & _ "?useUnicode=true&characterEncoding=utf8" & _ "&serverTimezone=Asia/Shanghai" & _ "&connectTimeout=5000&socketTimeout=8000" & _ "&useSSL=false&allowPublicKeyRetrieval=true" Dim Conn As JavaObject Conn = MySQLDriver.RunMethod("connect", Array(Url, User, Pass))这段代码的关键点在连接串后半段。characterEncoding=utf8不写,中文昵称注册进去就是乱码;serverTimezone不写,MySQL 8 驱动会直接抛时区异常;connectTimeout和socketTimeout是保命的,网络抖动时不至于让 UI 线程卡死几十秒;useSSL=false在内网环境省掉证书配置,公网环境则应反过来开启。allowPublicKeyRetrieval=true是 MySQL 8 默认认证插件下的必要参数,缺了会报公钥检索失败。
2.3 线程模型:为什么必须把数据库操作踢出主线程
安卓的主线程负责 UI 渲染,任何超过约 5 秒的阻塞都会触发 ANR。JDBC 的connect和executeQuery都是同步阻塞调用,网络稍差就超时。资源里的处理方式是用 B4A 的Thread或Timer把数据库操作放到后台,结果通过CallSubDelayed回主线程更新界面。示意结构:
Sub btnLogin_Click ' 主线程只负责收集输入和禁用按钮 btnLogin.Enabled = False Dim task As Thread task.Initialize("LoginTask") task.Start(Me, "DoLogin", Array(txtUser.Text, txtPass.Text)) End Sub Sub DoLogin(User As String, Pass As String) ' 后台线程:建连接、查数据、关连接 Dim Result As Map = QueryUser(User, Pass) ' 回主线程处理 UI,不能在这里直接改控件 CallSubDelayed2(Me, "OnLoginResult", Result) End SubThread.Start的第二个参数是子过程名,第三个是参数数组。子过程里绝对不能碰任何 View 控件,否则会抛「only the original thread that created a view hierarchy can touch its views」。CallSubDelayed2把结果封成 Map 回传,主线程的OnLoginResult再根据Result.Get("ok")决定跳转还是提示。这套「后台查、主线程刷」的模式是这个资源能稳定跑起来的基础,跳过它直接在主线程里查库,模拟器上可能没事,真机弱网必翻车。
3. 账号表设计与注册写入:字段、约束与密码处理
3.1 表结构:最少字段与必要约束
登录注册的核心表不需要花哨,但几个约束不能省。资源对应的建表语句大致如下:
CREATE TABLE t_user ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(32) NOT NULL, password VARCHAR(128) NOT NULL, salt CHAR(16) NOT NULL, nickname VARCHAR(32) DEFAULT '', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, last_login DATETIME NULL, status TINYINT NOT NULL DEFAULT 1, UNIQUE KEY uk_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;username上的唯一索引是防重复注册的第一道闸,别指望应用层「先查再插」能挡住并发——两个请求同时查到不存在、同时插入,唯一索引才是最终裁决。password留 128 位是给哈希值留空间,明文密码最长也就几十位,但存哈希后长度会膨胀。salt单独一列存随机盐,status用于软禁用账号,last_login方便做活跃统计。字符集用utf8mb4而不是utf8,后者在 MySQL 里其实是三字节的,存 emoji 昵称会截断报错。
3.2 注册流程:查重、加盐、哈希、插入
注册不是一条 INSERT 就完事。资源里的顺序是:先做输入合法性校验,再查重,再生成盐和哈希,最后插入并处理唯一键冲突。密码绝不能明文入库,常见做法是 SHA-256 加盐,或者用 PBKDF2 做多轮迭代。B4A 里调 Java 的MessageDigest就能实现:
Sub MakeSalt As String Dim rnd As JavaObject rnd.InitializeNewInstance("java.security.SecureRandom", Null) Dim bytes(16) As Byte rnd.RunMethod("nextBytes", Array(bytes)) Return BytesToHex(bytes) End Sub Sub HashPassword(Pass As String, Salt As String) As String Dim md As JavaObject md.InitializeStatic("java.security.MessageDigest") Dim digest As JavaObject = md.RunMethod("getInstance", Array("SHA-256")) Dim input As String = Salt & Pass Dim raw() As Byte = digest.RunMethod("digest", Array(input.GetBytes("UTF-8"))) Return BytesToHex(raw) End SubSecureRandom而不是Random,是因为后者可预测,盐失去意义。哈希输入用「盐 + 密码」拼接,顺序固定即可,关键是登录校验时要用同样的拼接方式。BytesToHex是自定义的字节转十六进制函数,把二进制摘要转成可存库的字符串。注册的插入语句用参数化写法,别拼字符串:
Sub Register(User As String, Pass As String) As Boolean Dim Salt As String = MakeSalt Dim Hash As String = HashPassword(Pass, Salt) Dim ps As JavaObject ps = Conn.RunMethod("prepareStatement", _ Array("INSERT INTO t_user(username,password,salt) VALUES(?,?,?)")) ps.RunMethod("setString", Array(1, User)) ps.RunMethod("setString", Array(2, Hash)) ps.RunMethod("setString", Array(3, Salt)) Try ps.RunMethod("executeUpdate", Null) Return True Catch e As Exception ' 唯一键冲突会走到这里,返回 false 让上层提示"用户名已存在" Return False End Try End SubprepareStatement的占位符由驱动负责转义,用户名里带单引号也不会破坏 SQL 结构,这是防注入的基本功。executeUpdate返回受影响行数,插入成功是 1。捕获异常后统一返回 false,上层根据返回值区分「已存在」和「其他错误」时,可以进一步读异常信息里的错误码 1062 来判断是不是唯一键冲突。
3.3 登录校验:按用户名取盐再比对
登录的逻辑是注册的逆过程:按用户名查出salt和password,用同一套哈希函数算一遍,比对相等则通过。注意查询要用ResultSet逐行读,并且用完必须关:
Sub QueryUser(User As String, Pass As String) As Map Dim res As Map res.Initialize res.Put("ok", False) Dim ps As JavaObject ps = Conn.RunMethod("prepareStatement", _ Array("SELECT password,salt,status FROM t_user WHERE username=?")) ps.RunMethod("setString", Array(1, User)) Dim rs As JavaObject = ps.RunMethod("executeQuery", Null) If rs.RunMethod("next", Null) Then Dim dbHash As String = rs.RunMethod("getString", Array("password")) Dim dbSalt As String = rs.RunMethod("getString", Array("salt")) Dim st As Int = rs.RunMethod("getInt", Array("status")) If st = 1 And HashPassword(Pass, dbSalt) = dbHash Then res.Put("ok", True) End If End If rs.RunMethod("close", Null) ps.RunMethod("close", Null) Return res End Sub比对用普通字符串相等即可,因为两边都是定长哈希值,不存在时序侧信道的问题(真要较真可以用MessageDigest.isEqual)。status判断放在哈希比对之前或之后都行,但别漏,否则被禁用的账号照样能登。ResultSet和PreparedStatement的close必须成对出现,漏关的结果是连接池耗尽、MySQL 端Too many connections,这个坑在第 5 章还会展开。
4. 把登录注册串成完整流程:状态保持与界面联动
4.1 登录成功后的会话怎么存
直连方案没有服务端 session,登录态只能存在客户端。资源里的做法是登录成功后把username和一个随机 token 写进 B4A 的KeyValueStore,后续需要鉴权的操作带上这个 token 去数据库校验。简单场景下也可以只存用户名,但要注意这等于「客户端说了算」,安全性有限。更稳一点的做法是在t_user旁边加一张t_session表,登录时插入一条带过期时间的记录,退出时删除:
CREATE TABLE t_session ( token CHAR(32) NOT NULL PRIMARY KEY, username VARCHAR(32) NOT NULL, expire_at DATETIME NOT NULL, KEY idx_user (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;登录成功时生成 32 位随机 token 插入,客户端只存 token 不存密码;每次敏感操作先查t_session里 token 是否存在且未过期。这样即使 APK 被反编译,攻击者也拿不到长期有效的凭据,token 可以随时在服务端删掉。过期清理用 MySQL 事件或应用启动时顺手删过期行都行,量小的话后者更省事。
4.2 界面联动的三个状态
登录注册界面本质是三个状态机:空闲、请求中、结果反馈。请求中要禁用按钮防止重复提交,结果反馈要区分成功、账号密码错、网络异常三类。资源里的处理是把btnLogin.Enabled = False放在点击回调第一行,在OnLoginResult里恢复。网络异常和认证失败必须分开提示,否则用户会以为密码错了反复重试,实际是数据库连不上。判断依据是后台子过程里Try...Catch捕获的异常类型:连接超时抛的是SQLException且 message 含 timeout,认证失败则是查询返回空或哈希不等。
Sub OnLoginResult(res As Map) btnLogin.Enabled = True If res.Get("ok") = True Then kvs.PutString("token", res.Get("token")) StartActivity(MainPage) Else If res.Get("error") = "network" Then ToastMessageShow("网络异常,请检查连接", True) Else ToastMessageShow("用户名或密码错误", True) End If End Subres里除了ok还要带error字段区分错误类型,这个字段在后台子过程的Catch里根据异常 message 填充。界面层只做展示,不做判断逻辑,这样后续把直连换成 HTTP 接口时,界面代码几乎不用动。
5. 避坑清单:五个让直连方案当场翻车的现场
5.1 现象:模拟器能连,真机连不上
原因通常是网络环境差异。模拟器走的是宿主机网络,能访问到局域网里的 MySQL;真机如果连的是移动数据,根本到不了内网地址。另一个常见原因是 MySQL 的bind-address只监听了127.0.0.1,外部请求直接被拒。解决:确认手机和数据库在同一网段,检查 MySQL 配置里bind-address = 0.0.0.0,并在数据库侧给账号授权'app_rw'@'%'。授权语句GRANT SELECT,INSERT,UPDATE ON app_user.* TO 'app_rw'@'%',别图省事给ALL。
5.2 现象:注册中文昵称变问号或报错
原因是连接串没指定字符集,或者表用了utf8而非utf8mb4。驱动默认可能按 latin1 处理,中文进去就成了???。解决:连接串加useUnicode=true&characterEncoding=utf8,建表和库统一utf8mb4,插入前确认Conn的字符集设置生效。已经存坏的数据没法自动恢复,只能重录。
5.3 现象:跑一段时间后报 Too many connections
原因是每次操作都新建连接却忘了关,或者异常路径上close被跳过。JDBC 连接是稀缺资源,MySQL 默认max_connections也就一百多。解决:把close放进Try...Finally的Finally块,保证异常时也执行;更彻底的做法是引入一个简单的连接复用逻辑,或者干脆缩短连接生命周期、用完即关。排查时在 MySQL 端执行SHOW PROCESSLIST看连接来自哪个 IP、停留多久,一目了然。
5.4 现象:登录偶发失败,重试又好了
多半是socketTimeout设得太短,或者网络抖动时连接被中间设备掐断。表现是SQLException里带Communications link failure。解决:把connectTimeout和socketTimeout调到 8 到 10 秒,并在后台子过程里对超时做一次重试。重试要加次数上限,别写成死循环。另外确认数据库服务器没有开启wait_timeout过短的自动断连。
5.5 现象:APK 反编译后数据库密码裸奔
这是直连方案的固有缺陷,不是 bug。strings一把梭就能从 APK 里捞出连接串。缓解手段有限:给数据库账号最小权限(只给单库的增查改)、用独立低权限账号、定期轮换密码、把敏感配置放到首次启动时由用户输入而非硬编码。真要彻底解决,还是得加后端接口层。把这个方案用在内网或演示环境,是它最舒服的位置。
6. 进阶:把直连逻辑收进一个可替换的数据层
写到这儿,登录注册的闭环已经能跑。但如果你打算在这个基础上继续加功能,我建议第一件事不是加页面,而是把散落在各处的 JDBC 调用收进一个数据层模块。具体做法是定义一个UserRepo类,对外只暴露Login(user, pass)、Register(user, pass)、GetProfile(token)三个方法,内部管连接的建立与关闭,异常统一转成业务错误码。这样将来要把直连换成 HTTP 接口,只改UserRepo内部,界面和调用方一行不动。
验证数据层是否收干净,有个笨办法但很有效:全局搜索prepareStatement和DriverManager,如果只在UserRepo一个文件里出现,说明收住了;如果散在多个 Activity 里,就是没收干净。我一般还会在UserRepo里加一个开关常量USE_REMOTE_API,直连和接口两条路径并存,联调时切着用,比事后重构省事得多。
连接参数这块再补一个细节。serverTimezone写死Asia/Shanghai在单时区部署下没问题,但如果数据库服务器和客户端不在同一时区,created_at和last_login会出现偏移。稳妥做法是数据库统一存 UTC,展示时在客户端转本地时区,连接串里写serverTimezone=UTC。这个改动越早做越好,等表里攒了几万条时间错乱的数据再改,清洗成本高得离谱。
最后说个习惯。从那以后我每次做这类直连方案,都会在项目根目录放一个db_check.bas小模块,里面就一个SelfTest子过程:建连接、执行SELECT 1、读结果、关连接,全程打日志。上线前在目标机型上跑一遍,能连、能查、能关,三个都过才往下写业务。这个自检花不了十分钟,但能挡掉后面一大半「明明代码没问题却连不上」的玄学排查。希望帮到你。
本文还有配套的精品资源,点击获取