简介:这份资源面向希望用 Basic4Android 快速开发安卓应用的初学者与进阶开发者,聚焦 B4A 与 MySQL 数据库交互这一常见需求,提供登录注册功能的完整实现参考。包内共 53 个文件,以 29 个 class、8 个 java 源文件及 3 个 xml 配置为主,辅以 3 个 bas 与 3 个 bal 布局脚本、apk 与 dex 产物等,压缩包约 1.84MB,结构覆盖源码、资源与编译输出。已有 714 人学习下载,说明该主题在安卓入门与数据库实战中具有较高关注度。资源围绕数据库连接参数配置、SQL 查询与插入、界面输入验证、密码哈希与 SQL 注入防护等环节展开,读者可据此理解 B4A 调用 JDBC 的完整链路,并对照源码排查连接失败、查询无结果等常见问题,适合作为课程设计或小型项目原型的基础模板。
1. 用 B4A 直连 MySQL 做登录注册:先想清楚这条路能不能走
安卓端做登录注册,很多人第一反应是「客户端直接连数据库」。B4A(Basic4android)这个工具链里,确实能通过 JDBC 驱动连上 MySQL,写起来比搭一套 HTTP 接口快得多,一个 Activity 加几百行 Basic 语法就能跑通注册、登录、查重。但这条路能不能上生产,取决于你怎么处理连接、密码和并发。我见过不少课程设计和小型内部工具用它,跑得挺稳;也见过有人把 root 账号密码硬编码进 APK,反编译一看全露。这篇笔记拆的就是 B4A 连 MySQL 做登录注册这套方案:它由哪些部分组成、驱动怎么配、注册登录的 SQL 和参数怎么设、哪些坑必须提前绕开。适合手上有个 B4A 项目、需要快速落地账号体系,又不想临时学一套后端框架的开发者;如果你要做的是面向公网的产品,读完第 4 章你会知道该在哪一步停下来。
2. 环境与驱动:B4A 连 MySQL 的依赖链怎么搭
B4A 本身不带数据库驱动,连 MySQL 靠的是 JDBC。安卓上跑 JDBC 不是官方推荐路径,但可行,前提是把驱动包、网络权限、线程模型三件事配齐。这一章把依赖链拆开,每一步都落到具体文件和配置上。
2.1 驱动包的选择与放入位置
MySQL 官方 Connector/J 是 Java 库,B4A 通过 JavaObject 调用它。常见做法是选一个兼容低版本 Android 的 Connector/J 版本(5.1.x 系列在旧设备上兼容性更好,8.x 对 TLS 和 JDK 版本要求更高),把 jar 包放进项目的额外库目录,再在 B4A 的库管理里引用。具体操作是:在项目目录下建libs文件夹,把mysql-connector-java-5.1.49.jar放进去,然后在 B4A IDE 的 Project 菜单里选 Add Existing Jar,指向这个文件。B4A 编译时会把它打进 APK。
这里有个容易忽略的点:Connector/J 依赖java.sql包,安卓自带这部分,但不同 API 级别行为有差异。API 21 以下对 JDBC 4.0 支持不完整,如果目标设备很老,优先用 5.1.x 而不是 8.x。另外 jar 包体积不小,打进 APK 会让安装包涨几 MB,这是直连方案的固定成本,接受不了就得换 HTTP 接口。
2.2 Manifest 权限与网络配置
直连 MySQL 走的是 TCP 3306 端口,需要 INTERNET 权限。在 B4A 的 Manifest 编辑器里加:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />ACCESS_NETWORK_STATE 不是必须的,但用来在连接前判断网络是否可用,能少一次无谓的超时等待。安卓 9 以后默认禁止明文流量,MySQL 协议本身不是 HTTP,不受 cleartext 限制,但如果你的连接串里带了 SSL 参数,要注意证书校验行为。测试阶段常见做法是在连接串里关掉 SSL(useSSL=false),生产环境必须打开并配好证书,否则账号密码在链路上是裸的。
2.3 连接串参数逐个说清
连接串决定了连接行为,参数设错是后面大部分玄学问题的根源。一个能跑通的测试用连接串长这样:
Dim url As String = "jdbc:mysql://192.168.1.100:3306/appdb?useUnicode=true&characterEncoding=utf8&useSSL=false&connectTimeout=5000&socketTimeout=8000&autoReconnect=true"逐项说明:useUnicode=true加characterEncoding=utf8保证中文用户名不乱码,这两个必须成对出现;useSSL=false仅限内网测试;connectTimeout=5000是建立连接的超时,单位毫秒,设太小在弱网下频繁失败,设太大界面卡死;socketTimeout=8000是读超时,查询慢于这个值会抛异常;autoReconnect=true让驱动在连接断开后尝试重连,但注意它不保证事务安全,只适合这种无事务的简单读写。IP 地址不要写localhost,安卓设备上的 localhost 是设备自己,不是你的数据库服务器。
2.4 线程模型:为什么不能在主线程连库
B4A 的 UI 操作在主线程,网络 IO 必须放到后台。直连 MySQL 的每一次DriverManager.getConnection和executeQuery都是阻塞调用,放在主线程轻则界面卡顿,重则 ANR 弹窗。标准做法是用Thread或Timer包一层,查询结果通过CallSubDelayed回到主线程更新界面。这一点在注册登录场景里尤其关键,因为用户点按钮后如果界面没反应,会连点好几次,后台就并发起了多个连接,数据库连接数瞬间打满。后面第 4 章的坑里有一条就是连点导致的连接泄漏。
3. 注册与登录的落地实现:从建表到按钮回调
依赖配好之后,核心就是三件事:表结构设计、注册逻辑(查重加插入)、登录逻辑(查匹配加状态返回)。这一章给出可直接抄的 SQL 和 B4A 代码,并把每个参数为什么这么设讲清楚。
3.1 表结构:字段、索引与密码存储
先建表。用户名要唯一,密码不能明文存,这是底线。测试阶段很多人图省事存明文,但改起来成本很低,一开始就做对:
CREATE TABLE app_user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(32) NOT NULL, password_hash CHAR(64) NOT NULL, salt CHAR(16) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, last_login DATETIME NULL, status TINYINT NOT NULL DEFAULT 1, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;username上的唯一索引是注册查重的最后一道防线,应用层查重有并发窗口,唯一索引能兜住。password_hash存 SHA-256 十六进制串正好 64 字符,salt每个用户独立生成 16 字符。status用来做禁用账号,登录时判断。字符集用utf8mb4而不是utf8,后者在 MySQL 里是残缺的三字节实现,存 emoji 或部分生僻字会报错。created_at用数据库默认值,避免客户端时间不准。
密码处理在 B4A 侧做:生成随机 salt,把salt + 明文密码拼接后做 SHA-256。B4A 没有内置 SHA-256,常见做法是用 JavaObject 调java.security.MessageDigest:
Sub HashPassword(password As String, salt As String) As String Dim md As JavaObject md.InitializeNewInstance("java.security.MessageDigest", Null) Dim cls As JavaObject cls = md.GetField("class") Dim digest As JavaObject = cls.RunMethod("getInstance", Array As Object("SHA-256")) Dim input As String = salt & password Dim bytes() As Byte = input.GetBytes("UTF-8") digest.RunMethod("update", Array As Object(bytes)) Dim result() As Byte = digest.RunMethod("digest", Null) Return BytesToHex(result) End Sub这段的逻辑是:取 MessageDigest 实例,喂入 UTF-8 字节,输出摘要再转十六进制。salt & password的顺序固定即可,关键是注册和登录必须用同一顺序。GetBytes("UTF-8")不能省,否则中文密码在不同平台字节序列不同,登录会莫名失败。BytesToHex 自己写一个循环把每个字节转两位十六进制,注意负数要And 0xFF处理。
3.2 注册流程:查重、插入与唯一键冲突处理
注册的完整链路是:校验输入长度 → 查用户名是否存在 → 生成 salt 和 hash → 插入 → 根据结果返回提示。查重和插入之间有时间窗,所以插入语句要捕获唯一键冲突异常,不能只靠前面的查询。
Sub DoRegister(username As String, password As String) As Boolean If username.Length < 3 Or username.Length > 32 Then Return False If password.Length < 6 Then Return False Dim conn As JavaObject Try Dim cls As JavaObject cls.InitializeStatic("java.sql.DriverManager") conn = cls.RunMethod("getConnection", Array As Object(DB_URL, DB_USER, DB_PASS)) ' 先查重 Dim ps As JavaObject = conn.RunMethod("prepareStatement", _ Array As Object("SELECT COUNT(*) FROM app_user WHERE username=?")) ps.RunMethod("setString", Array As Object(1, username)) Dim rs As JavaObject = ps.RunMethod("executeQuery", Null) rs.RunMethod("next", Null) If rs.RunMethod("getInt", Array As Object(1)) > 0 Then CloseAll(rs, ps, conn) Return False End If ' 生成 salt 与 hash 后插入 Dim salt As String = GenerateSalt(16) Dim hash As String = HashPassword(password, salt) Dim ins As JavaObject = conn.RunMethod("prepareStatement", _ Array As Object("INSERT INTO app_user(username,password_hash,salt) VALUES(?,?,?)")) ins.RunMethod("setString", Array As Object(1, username)) ins.RunMethod("setString", Array As Object(2, hash)) ins.RunMethod("setString", Array As Object(3, salt)) ins.RunMethod("executeUpdate", Null) CloseAll(Null, ins, conn) Return True Catch Log("register error: " & LastException.Message) Return False End Try End Sub参数说明:prepareStatement加setString是防 SQL 注入的关键,用户名里带单引号也不会破坏语句结构,绝对不要用字符串拼接 SQL。getInt(1)取 COUNT 结果。CloseAll是自定义的清理子过程,按 rs、ps、conn 顺序关闭,任何一个为 Null 就跳过。Catch 里只记日志不抛给用户,界面层统一提示「注册失败,请稍后重试」,避免把数据库错误信息暴露出去。如果插入时撞上唯一键冲突(并发注册同名),会进 Catch,此时返回 False 也是合理结果。
3.3 登录流程:参数化查询与失败计数
登录比注册简单,但有两个细节:一是查询要按用户名取 salt 和 hash,在客户端算完再比对;二是失败次数要限制,否则就是暴力破解的靶子。
Sub DoLogin(username As String, password As String) As Int ' 返回 0 成功 1 用户不存在 2 密码错误 3 账号禁用 Dim conn As JavaObject Try Dim cls As JavaObject cls.InitializeStatic("java.sql.DriverManager") conn = cls.RunMethod("getConnection", Array As Object(DB_URL, DB_USER, DB_PASS)) Dim ps As JavaObject = conn.RunMethod("prepareStatement", _ Array As Object("SELECT password_hash,salt,status FROM app_user WHERE username=?")) ps.RunMethod("setString", Array As Object(1, username)) Dim rs As JavaObject = ps.RunMethod("executeQuery", Null) If Not(rs.RunMethod("next", Null)) Then CloseAll(rs, ps, conn) Return 1 End If Dim storedHash As String = rs.RunMethod("getString", Array As Object(1)) Dim salt As String = rs.RunMethod("getString", Array As Object(2)) Dim status As Int = rs.RunMethod("getInt", Array As Object(3)) CloseAll(rs, ps, conn) If status <> 1 Then Return 3 If HashPassword(password, salt) = storedHash Then UpdateLastLogin(username) Return 0 Else Return 2 End If Catch Log("login error: " & LastException.Message) Return 2 End Try End Sub逻辑说明:先按用户名取记录,取不到返回 1;取到后先判 status,禁用账号直接返回 3,不参与密码比对;密码比对用同一套 HashPassword,salt 从库里取,保证和注册时一致。UpdateLastLogin是另一条 UPDATE 语句,单独开连接或复用连接都行,注意别在同一个 ResultSet 未关闭时复用连接。失败计数建议放在应用层内存或单独的表里,连续 5 次失败锁定 10 分钟,这个逻辑不复杂但能挡掉大部分脚本撞库。返回值用整数而不是布尔,是为了让界面能给出「用户不存在」和「密码错误」的区分提示——虽然从安全角度统一提示更好,但内部工具场景下区分提示对用户更友好,自己权衡。
3.4 界面回调:把后台结果送回主线程
后台线程算完,必须回到主线程改 UI。B4A 的CallSubDelayed是标准做法:
Sub btnLogin_Click Dim u As String = edtUser.Text.Trim Dim p As String = edtPass.Text If u = "" Or p = "" Then ToastMessageShow("请输入用户名和密码", False) Return End If btnLogin.Enabled = False Dim t As Thread t.Start(Me, "LoginTask", u, p) End Sub Sub LoginTask(u As String, p As String) Dim code As Int = DoLogin(u, p) CallSubDelayed2(Me, "OnLoginResult", code) End Sub Sub OnLoginResult(code As Int) btnLogin.Enabled = True Select code Case 0 ToastMessageShow("登录成功", False) StartActivity(Main) Case 1 ToastMessageShow("用户不存在", False) Case 2 ToastMessageShow("密码错误", False) Case 3 ToastMessageShow("账号已被禁用", False) End Select End Sub关键点在btnLogin.Enabled = False:点击后立刻禁用按钮,防止连点并发起多个连接,这是第 4 章连接泄漏坑的直接对策。Thread.Start的参数按顺序传给 LoginTask,CallSubDelayed2把结果异步送回主线程,第二个参数是回调子过程名。整个链路里没有任何阻塞调用留在主线程,界面不会卡。
4. 直连方案的避坑清单:五条血泪经验
这一章是踩坑记录,每条按现象、原因、解决写。直连 MySQL 的问题大多集中在连接管理、编码、超时和权限上,提前知道能省很多调试时间。
4.1 现象:用几次之后提示连接数超限
原因:每次操作都getConnection却忘了close,或者异常分支里提前 return 跳过了关闭。MySQL 默认max_connections是 151,一个用户连点几次、几个用户同时用,很快就打满,之后所有连接都失败。更隐蔽的是 ResultSet 和 PreparedStatement 没关,它们持有连接引用,conn.close 也不一定立刻释放。
解决:所有数据库操作统一走 try-catch-finally 结构,关闭顺序固定为 ResultSet → PreparedStatement → Connection,每个关闭单独 try 包住,避免前一个抛异常导致后面关不掉。写一个CloseAll子过程集中处理,所有调用点都用它,不手写关闭。另外在按钮点击时禁用按钮,从源头减少并发连接。
4.2 现象:中文用户名注册成功,登录却查不到
原因:连接串没带useUnicode=true&characterEncoding=utf8,或者表字符集是utf8而客户端按utf8mb4发数据,两边编码不一致,存进去的是乱码或问号,按原用户名查自然查不到。还有一种情况是 B4A 侧GetBytes没指定 UTF-8,用了平台默认编码。
解决:连接串固定带useUnicode=true&characterEncoding=utf8,表统一utf8mb4,B4A 侧所有字符串转字节都显式GetBytes("UTF-8")。排查时直接在数据库里SELECT username, HEX(username)看存储的字节,和客户端发出的字节对比,一眼能看出是哪一层转错了。
4.3 现象:内网测试正常,换个网络就连接超时
原因:连接串里写的是数据库服务器的内网 IP,换到外网环境路由不通;或者防火墙只放行了特定来源 IP 的 3306 端口。也有connectTimeout设得过大(比如 30000),弱网下用户要等半分钟才看到失败提示。
解决:数据库地址做成可配置项,不要硬编码在代码里,至少放在一个常量区方便改。connectTimeout设 5000 左右,socketTimeout设 8000 左右,失败快速返回比长时间转圈体验好。上线前用手机流量和 WiFi 各测一遍,确认目标网络能到 3306。
4.4 现象:APK 反编译后数据库账号密码全暴露
原因:直连方案把数据库地址、账号、密码写在客户端代码里,不管怎么混淆,字符串常量都能被提取。用 root 账号更是灾难,攻击者拿到后可以拖库、删表。
解决:这是直连方案的结构性缺陷,没有完美解法。能做的是:给应用单独建一个数据库账号,只授予app_user表的 SELECT、INSERT、UPDATE 权限,不给 DROP、DELETE 和跨库权限;密码定期轮换;如果面向公网,老老实实加一层 HTTP 接口,把数据库藏在服务端。内部工具、课程设计场景下,最小权限账号加内网访问已经能挡住大部分风险,但心里要清楚这条线的位置。
4.5 现象:登录偶尔成功偶尔失败,日志里是通信异常
原因:autoReconnect=true在连接空闲被服务端断开后重连,但重连发生在语句执行中途时,驱动行为不确定;加上 MySQL 的wait_timeout默认 8 小时,连接池外的长连接很容易被回收。另一个常见原因是多个线程共用一个 Connection 对象,JDBC 连接不是线程安全的。
解决:不要跨线程共享 Connection,每次操作独立获取、用完即关,虽然开销大一点但行为确定。autoReconnect可以保留作为兜底,但不要依赖它保证正确性。如果操作频繁,考虑在应用内做一个极简的连接复用(单线程串行使用),但复杂度上升后不如直接上服务端接口。日志里把异常类型和消息都打出来,LastException.Message往往直接告诉你是不是连接被重置。
5. 进阶:把直连代码收敛成一个可复用的数据访问层
写到第 3 章,注册和登录各有一套连接、查询、关闭的代码,复制粘贴多了必然出现某处漏关连接。我的做法是把所有数据库操作收敛到一个模块里,对外只暴露QueryOne、Execute两个方法,连接管理和异常处理全在内部。这样第 4 章那些坑只需要在一个地方防住。
具体结构是:一个DB代码模块,内部持有连接串常量和一个私有的OpenConn子过程,QueryOne(sql, params)返回一行结果的字符串数组,Execute(sql, params)返回受影响行数。参数用List传,内部循环setString。这样注册的查重、插入,登录的查询、更新最后登录时间,全部变成一行调用。代码量减少一半,关闭逻辑只有一处,改连接串也只改一个常量。
验证这套封装是否可靠,我一般做三个检查:一是连续点登录按钮 20 次,看数据库SHOW PROCESSLIST里连接数是否稳定回落;二是注册一个带单引号和中文的用户名,确认不报错且能正常登录;三是把数据库服务停掉再点登录,确认界面在 5 秒左右给出失败提示而不是卡死。这三个检查覆盖了连接泄漏、注入与编码、超时三个高频问题。
还有一个值得做的技巧:把HashPassword的 salt 生成改成用java.security.SecureRandom而不是Random,前者是密码学安全的随机源,后者种子可预测。改动很小,但密码存储这块没有后悔药,一开始用对的比后面迁移省事。从那以后我每次写这类直连数据库的 Demo,都先把数据访问层封好再写业务,宁可多花半小时,也不在十几个调用点里逐个补 close。希望帮到你。
本文还有配套的精品资源,点击获取