Android连接云MySQL正确姿势:HTTP API网关方案
简介本资源是一套完整的Android应用开发实战项目面向具备Java与Android Studio基础的开发者解决移动端连接云服务器MySQL数据库实现用户登录注册的核心需求。项目涵盖客户端Android应用、后端Spring Boot RESTful API及MySQL数据库交互全流程包含网络请求Retrofit/Volley、JSON解析、Token会话管理、密码加密存储、HTTPS安全传输等关键实践环节。压缩包共2000个文件主体为305个XML布局与配置文件、23个Java核心逻辑类、132个JSON接口示例、264个PNG资源图及38个JAR依赖库总大小102.05MB结构清晰模块分离明确。已有6269人学习下载提供可直接运行的完整工程代码、服务端API设计说明、异常处理策略及安全加固要点助开发者快速掌握跨平台数据交互与生产级移动应用后端集成能力。1. Android Studio 连接云服务器 MySQL 不是直连而是必须走 HTTP API 网关很多刚从本地 SQLite 迁移过来的开发者会下意识认为“Android 能连本地 MySQL那换台云服务器 IP 就行”——这是个危险误区。Android 系统从 API 28Android 9起默认禁止明文 HTTP 请求且原生不支持 JDBC 驱动直连远程 MySQLjava.sql.Driver在 Android RuntimeART中被移除com.mysql.cj.jdbc.Driver加载即崩溃Class.forName(com.mysql.cj.jdbc.Driver)抛ClassNotFoundException是必然结果。真正可行的路径只有一条Android 客户端通过 HTTP/HTTPS 向云服务器上的 Web 接口发起请求由后端服务完成 MySQL 操作并返回 JSON 响应。这意味着你必须同时掌控三端Android Studio 中的 Java/Kotlin 网络调用逻辑、云服务器上运行的 Web 服务如 Spring Boot、以及 MySQL 数据库的建表与权限配置。本方案适用于需要用户账户体系的中轻量级 App比如内部工具、校园应用或 MVP 产品原型若涉及高并发或金融级安全要求则需补全 OAuth2、JWT 刷新机制与数据库读写分离。下面从零开始把这三层链路打通。2. Android Studio 端用 Retrofit OkHttp 构建健壮的登录注册网络层Android 客户端不能碰 MySQL 驱动但能高效发起 REST 请求。Retrofit 是当前最主流的类型安全 HTTP 客户端配合 OkHttp 的连接池与拦截器比 Volley 更易维护、更易调试。关键不是“能不能发请求”而是如何让请求在真实网络环境下稳定、可测、可追溯。2.1 添加依赖与网络权限配置在app/build.gradle的dependencies块中添加// Retrofit 核心与转换器 implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 // OkHttp 日志拦截器仅 debug 环境 debugImplementation com.squareup.okhttp3:logging-interceptor:4.12.0 // AndroidX Lifecycle 支持避免内存泄漏 implementation androidx.lifecycle:lifecycle-viewmodel:2.7.0注意converter-gson用于自动将 JSON 响应映射为 Java Bean避免手写JSONObject解析logging-interceptor仅在debugImplementation中引入发布包不会包含日志代码符合安全规范。在AndroidManifest.xml的application外层同级添加网络权限uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE /2.2 定义 API 接口与数据模型创建api/LoginApiService.javaimport retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.POST; public interface LoginApiService { // 注册接口POST /api/register POST(api/register) CallApiResponse register(Body RegisterRequest request); // 登录接口POST /api/login POST(api/login) CallApiResponse login(Body LoginRequest request); }对应的数据模型model/RegisterRequest.javapublic class RegisterRequest { private String username; private String password; private String email; // 可选字段按实际需求增减 public RegisterRequest(String username, String password, String email) { this.username username; this.password password; this.email email; } // getter/setter 省略AS 快捷键 AltInsert 可自动生成 }ApiResponse.java统一响应结构适配后端返回格式public class ApiResponse { private int code; // 200 成功400 参数错误409 用户已存在等 private String message; // 提示信息 private String token; // 登录成功后返回的 JWT 或 session ID private long userId; // 可选用户主键 ID用于后续请求携带 // getter/setter }2.3 初始化 Retrofit 实例并处理网络异常在Application类或Activity的onCreate()中初始化// 基础 URL替换为你的云服务器公网 IP 或域名务必用 HTTPS String baseUrl https://your-server-domain.com/; // 构建 OkHttpClient启用日志、超时、证书校验 OkHttpClient.Builder clientBuilder new OkHttpClient.Builder(); if (BuildConfig.DEBUG) { HttpLoggingInterceptor logging new HttpLoggingInterceptor(); logging.setLevel(HttpLoggingInterceptor.Level.BODY); clientBuilder.addInterceptor(logging); } clientBuilder.connectTimeout(15, TimeUnit.SECONDS) .readTimeout(20, TimeUnit.SECONDS) .writeTimeout(20, TimeUnit.SECONDS); // 构建 Retrofit Retrofit retrofit new Retrofit.Builder() .baseUrl(baseUrl) .addConverterFactory(GsonConverterFactory.create()) .client(clientBuilder.build()) .build(); LoginApiService apiService retrofit.create(LoginApiService.class);逻辑说明baseUrl必须以/结尾否则POST(api/login)会拼接成https://domain.comapi/loginconnectTimeout控制建立 TCP 连接的最大等待时间readTimeout控制接收响应体的超时二者缺一不可——弱网环境下未设超时会导致 UI 冻结HttpLoggingInterceptor在 debug 模式下打印完整请求头、请求体、响应头、响应体是排查 404/400/500 的第一手证据。2.4 发起注册请求并处理 UI 状态在RegisterActivity.java中调用private void performRegister() { String username etUsername.getText().toString().trim(); String password etPassword.getText().toString(); String email etEmail.getText().toString().trim(); if (username.isEmpty() || password.isEmpty()) { Toast.makeText(this, 用户名和密码不能为空, Toast.LENGTH_SHORT).show(); return; } RegisterRequest request new RegisterRequest(username, password, email); CallApiResponse call apiService.register(request); call.enqueue(new CallbackApiResponse() { Override public void onResponse(CallApiResponse call, ResponseApiResponse response) { if (response.isSuccessful() response.body() ! null) { ApiResponse body response.body(); if (body.getCode() 200) { Toast.makeText(RegisterActivity.this, 注册成功, Toast.LENGTH_SHORT).show(); // 跳转到登录页或主界面 startActivity(new Intent(RegisterActivity.this, LoginActivity.class)); finish(); } else { Toast.makeText(RegisterActivity.this, 注册失败 body.getMessage(), Toast.LENGTH_LONG).show(); } } else { // HTTP 状态码非 2xx如 404、500 Toast.makeText(RegisterActivity.this, 服务器错误 response.code(), Toast.LENGTH_LONG).show(); } } Override public void onFailure(CallApiResponse call, Throwable t) { // 网络异常无网络、DNS 失败、SSL 握手失败等 if (t instanceof IOException) { Toast.makeText(RegisterActivity.this, 网络连接失败请检查网络设置, Toast.LENGTH_LONG).show(); } else { Toast.makeText(RegisterActivity.this, 请求异常 t.getMessage(), Toast.LENGTH_LONG).show(); } } }); }参数说明enqueue()是异步调用避免阻塞主线程onResponse()中response.isSuccessful()判断 HTTP 状态码是否在 200–399 范围response.body()才是反序列化后的 Java 对象onFailure()捕获的是 OkHttp 层的底层异常如SocketTimeoutException,UnknownHostException而非业务错误二者必须分开处理。3. 云服务器端用 Spring Boot 搭建轻量级 MySQL API 服务云服务器如阿里云 ECS、腾讯云 CVM上不能直接暴露 MySQL 的 3306 端口给 Android 客户端——这等于把数据库凭证扔在公网上。正确做法是部署一个 Web 服务作为中间层它负责验证、查询、写入并严格控制访问权限。3.1 环境准备与 MySQL 安全配置登录云服务器终端确认 MySQL 已安装并运行sudo systemctl status mysql # 若未运行sudo systemctl start mysql创建专用数据库与用户严禁 root 远程登录-- 登录 MySQL使用 root mysql -u root -p -- 创建数据库 CREATE DATABASE app_auth CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 创建专用用户替换 your_password CREATE USER app_userlocalhost IDENTIFIED BY your_password; -- 授予最小必要权限 GRANT SELECT, INSERT, UPDATE ON app_auth.* TO app_userlocalhost; -- 刷新权限 FLUSH PRIVILEGES; -- 退出 EXIT;提示app_userlocalhost表示该用户只能从本机即 Spring Boot 应用所在服务器连接 MySQL杜绝外网直连风险utf8mb4支持 emoji 和四字节 Unicode 字符避免中文乱码。3.2 Spring Boot 项目结构与核心依赖使用 Spring Initializr 生成基础项目勾选Spring WebSpring Data JPAMySQL DriverLombok简化 getter/setterpom.xml关键依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency3.3 配置application.yml连接云服务器本地 MySQLspring: datasource: url: jdbc:mysql://localhost:3306/app_auth?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse username: app_user password: your_password # 上一步创建的密码 driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update # 开发期自动建表生产环境务必改为 validate 或 none show-sql: true properties: hibernate: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect # 自定义 API 前缀 server: servlet: context-path: /api参数说明serverTimezoneAsia/Shanghai解决时间戳存取偏差allowPublicKeyRetrievaltrue是 MySQL 8.0 连接必需参数useSSLfalse仅限开发测试生产环境必须配置 SSL 证书并设为 trueddl-auto: update会根据 Entity 自动修改表结构上线前必须关闭否则可能误删数据。3.4 编写 User 实体与 Repositoryentity/User.javaimport lombok.Data; import javax.persistence.*; import java.time.LocalDateTime; Entity Table(name users) Data public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(unique true, nullable false, length 50) private String username; Column(nullable false, length 100) private String password; // 存储 BCrypt 加密后的哈希值 Column(unique true, length 100) private String email; Column(updatable false) private LocalDateTime createdAt LocalDateTime.now(); }repository/UserRepository.javaimport org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; Repository public interface UserRepository extends JpaRepositoryUser, Long { User findByUsername(String username); User findByEmail(String email); boolean existsByUsername(String username); boolean existsByEmail(String email); }3.5 实现注册与登录 Controllercontroller/AuthController.javaimport org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; RestController RequestMapping(/auth) public class AuthController { Autowired private UserRepository userRepository; Autowired private BCryptPasswordEncoder passwordEncoder; PostMapping(/register) public ResponseEntityMapString, Object register(RequestBody RegisterRequest request) { MapString, Object response new HashMap(); // 检查用户名/邮箱是否已存在 if (userRepository.existsByUsername(request.getUsername())) { response.put(code, 409); response.put(message, 用户名已存在); return ResponseEntity.badRequest().body(response); } if (request.getEmail() ! null !request.getEmail().isEmpty() userRepository.existsByEmail(request.getEmail())) { response.put(code, 409); response.put(message, 邮箱已被注册); return ResponseEntity.badRequest().body(response); } // 创建新用户 User user new User(); user.setUsername(request.getUsername()); user.setPassword(passwordEncoder.encode(request.getPassword())); // 强制加密 user.setEmail(request.getEmail()); userRepository.save(user); response.put(code, 200); response.put(message, 注册成功); return ResponseEntity.ok(response); } PostMapping(/login) public ResponseEntityMapString, Object login(RequestBody LoginRequest request) { MapString, Object response new HashMap(); User user userRepository.findByUsername(request.getUsername()); if (user null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) { response.put(code, 401); response.put(message, 用户名或密码错误); return ResponseEntity.status(401).body(response); } // 生成简单 Token生产环境请用 JWT String token token_ System.currentTimeMillis() _ user.getId(); response.put(code, 200); response.put(message, 登录成功); response.put(token, token); response.put(userId, user.getId()); return ResponseEntity.ok(response); } }安全说明BCryptPasswordEncoder是行业标准的密码哈希算法matches()方法安全比对避免时序攻击/auth/register和/auth/login路径与 Android 端POST(api/register)对应Retrofit 会自动拼接baseUrl api/ auth/registerResponseEntity显式控制 HTTP 状态码比ResponseBody更精准。4. 全链路联调与 HTTPS 强制实施当 Android 端和 Spring Boot 端都就绪必须进行端到端验证。常见失败点不在代码逻辑而在网络策略与协议合规性。4.1 云服务器防火墙与安全组开放 443 端口Spring Boot 默认监听 8080但 Android 要求 HTTPS。因此需配置反向代理Nginx或直接启用 Spring Boot HTTPS。推荐 Nginx 方案因其成熟稳定在云服务器安装 Nginxsudo apt update sudo apt install nginx -y sudo systemctl enable nginx配置/etc/nginx/sites-available/app-apiserver { listen 443 ssl http2; server_name your-server-domain.com; # 替换为你的域名 ssl_certificate /etc/letsencrypt/live/your-server-domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your-server-domain.com/privkey.pem; location /api/ { proxy_pass http://localhost:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } # HTTP 重定向到 HTTPS server { listen 80; server_name your-server-domain.com; return 301 https://$server_name$request_uri; }启用配置并申请免费 SSL 证书Certbotsudo ln -sf /etc/nginx/sites-available/app-api /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx sudo apt install certbot python3-certbot-nginx -y sudo certbot --nginx -d your-server-domain.com提示certbot会自动修改 Nginx 配置并续期证书proxy_pass后的/api/末尾斜杠至关重要否则路径会被错误拼接。4.2 Android 端强制 HTTPS 并绕过自签名证书仅测试若暂未配置正式 SSL 证书Android 9 会拒绝连接自签名 HTTPS。生产环境绝不可绕过仅用于本地调试在res/xml/network_security_config.xml?xml version1.0 encodingutf-8? network-security-config domain-config domain includeSubdomainstrueyour-server-domain.com/domain trust-anchors certificates srcsystem / certificates srcuser / !-- 允许用户安装的证书 -- /trust-anchors pin-set !-- 生产环境应配置证书固定 -- /pin-set /domain-config /network-security-config并在AndroidManifest.xml的application标签中添加android:networkSecurityConfigxml/network_security_config4.3 使用 Postman 验证 API 可用性在 Android 运行前先用 Postman 测试后端注册请求POST https://your-server-domain.com/api/auth/registerBody → raw → JSON{username:testuser,password:123456,email:testexample.com}预期响应{code:200,message:注册成功}登录请求POST https://your-server-domain.com/api/auth/loginBody → raw → JSON{username:testuser,password:123456}预期响应{code:200,message:登录成功,token:token_171...,userId:1}验证逻辑Postman 成功即证明云服务器网络可达、MySQL 可写、Spring Boot 服务正常若失败按顺序排查云服务器安全组443/80 开放、Nginx 进程状态sudo systemctl status nginx、Spring Boot 日志journalctl -u your-app.service -f、MySQL 连接日志sudo tail -f /var/log/mysql/error.log。5. 生产环境加固密码加密、Token 管理与防暴力破解完成基础功能只是起点。真实上线前必须堵住三个高危漏洞明文密码传输、Token 泄露、暴力撞库。5.1 Android 端密码绝不以明文提交虽然 HTTPS 加密了传输过程但客户端仍应做基础脱敏。在提交前对密码加盐哈希非替代服务端 BCrypt而是增加一层// 使用 SHA-256 固定 SaltSalt 应硬编码在 APK 中非敏感信息 private String hashPassword(String password) { String salt AppSalt2024; // 实际项目应混淆或动态生成 try { MessageDigest digest MessageDigest.getInstance(SHA-256); byte[] hash digest.digest((password salt).getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(hash); } catch (Exception e) { return password; // 失败时回退明文确保流程不中断 } }然后在RegisterRequest和LoginRequest中传入hashPassword(etPassword.getText().toString())。后端需同步修改验证逻辑对收到的哈希值再做一次 BCrypt 加密存储——形成双重哈希提升离线破解难度。5.2 服务端 Token 必须绑定设备与有效期当前示例中的token_ ts _ id是纯字符串无签名、无过期、无绑定。生产环境必须升级为 JWT添加依赖dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency生成 TokenAuthController.javaimport io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.security.Key; import javax.crypto.spec.SecretKeySpec; private String generateJwtToken(User user) { String secretKey YourSuperSecretKeyChangeInProd; // 生产环境从配置中心获取 Key key new SecretKeySpec(secretKey.getBytes(), SignatureAlgorithm.HS256.getJcaName()); return Jwts.builder() .setSubject(user.getUsername()) .claim(userId, user.getId()) .claim(ip, request.getRemoteAddr()) // 绑定登录 IP .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 24 * 60 * 60 * 1000)) // 24小时 .signWith(key, SignatureAlgorithm.HS256) .compact(); }参数说明setExpiration()设定过期时间避免长期有效 Token 被盗用claim(ip, ...)记录登录来源 IP后续请求可校验一致性SecretKeySpec的密钥长度必须 ≥256 bit32 字节否则 JWT 签名失败。5.3 后端接入 RateLimiting 防暴力破解在pom.xml中添加spring-boot-starter-aop和guavadependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependency dependency groupIdcom.google.guava/groupId artifactIdguava/artifactId version32.1.3-jre/version /dependency编写限流切面RateLimitAspect.javaimport org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import com.google.common.util.concurrent.RateLimiter; import java.util.concurrent.ConcurrentHashMap; Aspect Component public class RateLimitAspect { // 每 IP 每分钟最多 5 次登录尝试 private final ConcurrentHashMapString, RateLimiter limiters new ConcurrentHashMap(); Around(annotation(org.springframework.web.bind.annotation.PostMapping) execution(* com.example.controller.AuthController.login(..))) public Object limitLoginAttempts(ProceedingJoinPoint joinPoint) throws Throwable { ServletRequestAttributes attributes (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); String ip attributes.getRequest().getRemoteAddr(); RateLimiter limiter limiters.computeIfAbsent(ip, k - RateLimiter.create(5.0)); // 5 per minute if (!limiter.tryAcquire()) { throw new RuntimeException(请求过于频繁请稍后再试); } return joinPoint.proceed(); } }效果同一 IP 在 60 秒内发起第 6 次/auth/login请求时直接抛出异常返回 500 错误前端显示“请求过于频繁”。此机制无需数据库记录内存级实现性能无损。至此Android Studio 连接云服务器 MySQL 的登录注册链路已具备生产可用基础网络层健壮、服务层安全、数据库层隔离。下一步应将 Token 存储于 Android Keystore而非 SharedPreferences并为所有 API 请求添加 Authorization Header完成闭环。本文还有配套的精品资源点击获取