Android Studio中通过PHP脚本在Apache服务器验证账号密码

Android Studio中通过PHP脚本在Apache服务器验证账号密码是否正确(在MySql中是否存在)

这个问题耗费了大量时间,踩了很多坑,下就解决过程进行记录

1.不使用服务器连接MySQL

1.1添加 MySQL 的 JDBC 驱动程序

1
2
3
dependencies {
implementation 'mysql:mysql-connector-java:8.0.27'
}

1.2验证账号密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import java.sql.DriverManager
import java.sql.SQLException
import java.sql.Statement

class LoginActivity : AppCompatActivity() {
private val DATABASE_URL = "jdbc:mysql://localhost:3306/my_database"
private val DATABASE_USER = "root"
private val DATABASE_PASSWORD = "password"

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)

val username = "input_username" // 替换为实际输入的用户名
val password = "input_password" // 替换为实际输入的密码

val connection = DriverManager.getConnection(DATABASE_URL, DATABASE_USER, DATABASE_PASSWORD)
val statement = connection.createStatement()
val query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'"
val resultSet = statement.executeQuery(query)

if (resultSet.next()) {
Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show()
// 在此处添加成功提示框的代码
} else {
Toast.makeText(this, "账号或密码错误", Toast.LENGTH_SHORT).show()
// 在此处添加错误提示框的代码
}

resultSet.close()
statement.close()
connection.close()
}
}

注释:onCreate() 方法中获取输入的用户名和密码,然后使用 DriverManager 类建立与数据库的连接。然后,我们创建了一个 Statement 对象并执行了查询语句。如果结果集中包含数据,则认为登录成功,并使用 Toast 显示成功提示框。

问题:安卓虚拟机中不能访问Localhost,应改为ip地址,这就要求程序联网,同时 ‘mysql-connector-java’, version: ‘8.0.27’中缺少所需的包,会导致程序运行错误;当程序联网时,不能在主线程实现,可以在协程或者创建子线程完成; 一个重要问题——DriverManager.getConnection(DATABASE_URL, DATABASE_USER, DATABASE_PASSWORD)一直报错导致程序闪退,降低mysql-connector-java版本后解决

解决:

1.导入依赖

1
2
3
4
//数据库连接
implementation group: 'mysql', name: 'mysql-connector-java', version: '5.1.49'
//协程
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'

2.JDBC_URL更换为ip地址

1
"jdbc:mysql://192.168.56.1:3306/test?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf-8",

更改后的连接数据库测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
CoroutineScope(Dispatchers.IO).launch {
var con: Connection
var sql: Statement
try {
Class.forName("com.mysql.jdbc.Driver"); //加载MYSQL JDBC驱动程序
println("成功加载Mysql驱动程序!");
} catch (e: Exception) {
print("加载Mysql驱动程序时出错!");
e.printStackTrace();
}
try {
con = DriverManager.getConnection(
"jdbc:mysql://192.168.56.1:3306/test?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf-8",
"root",
"123456"
);//连接数据库
println("成功连接Mysql服务器!");
sql=con.createStatement()
val query = "SELECT * FROM users"
val resultSet=sql.executeQuery(query)
while(resultSet.next()){
//val ss=resultSet.getString(2)
withContext(Dispatchers.Main){

}
}
} catch (e: Exception) {
print("连接数据库失败!");
e.printStackTrace();
}
}

控制台结果:

在完成这段代码中了解到:

SQL注入问题

概要:SQL 注入攻击是一种常见的网络攻击,它利用程序没有正确过滤用户输入的数据而向数据库中插入恶意代码。攻击者通过构造一些精心设计的输入数据,让程序将恶意代码当做正常的 SQL 语句来执行,从而获取非法的数据、破坏数据库的完整性,甚至控制整个系统。

示例:

查询语句

1
SELECT * FROM users WHERE username = '$username' AND password = '$password'

假设用户输入的用户名是 admin'--,密码是任意值。这时,构造出来的查询语句就变成了

1
SELECT * FROM users WHERE username = 'admin'--' AND password = '任意值'

后果:选取 users 表中用户名为 admin 的记录,后面的部分是注释掉的。这就意味着用户可以不需要知道正确的密码就能成功登录,从而进行非法的操作。

为了防止 SQL 注入攻击,程序应该正确地过滤用户输入的数据,避免将恶意代码当做正常的 SQL 语句来执行。在上面的示例中,可以使用参数化查询的方式来防止 SQL 注入攻击,例如:

1
SELECT * FROM users WHERE username = ? AND password = ?

总结:在这种方式下,程序会将用户输入的数据作为参数传递给 SQL 查询语句,而不是直接将数据拼接到 SQL 查询语句中。这样,即使用户输入恶意代码,也不会对数据库造成任何危害。

故:最后更改上述代码为
1
2
3
4
5
6
// val statement = connection.createStatement()
// val query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'"
val query = "SELECT * FROM users WHERE username = ? AND password = ?"
val statement = connection.prepareStatement(query)
statement.setString(1, username)
statement.setString(2, password)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//补充了一下之前连接数据库并验证的代码,还是本地验证
Thread(Runnable {
val DATABASE_URL = "jdbc:mysql://192.168.56.1:3306/test?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf-8"
val DATABASE_USER = "root"
val DATABASE_PASSWORD = "123456"
val username = bd.user.text.toString() // 替换为实际输入的用户名
val password = bd.password.text.toString() // 替换为实际输入的密码
Class.forName("com.mysql.jdbc.Driver")
val connection: Connection =
DriverManager.getConnection(DATABASE_URL, DATABASE_USER, DATABASE_PASSWORD)
val statement = connection.createStatement()
val query = "SELECT * FROM users WHERE user = '$username' AND password = '$password'"
val resultSet = statement.executeQuery(query)
if (resultSet.next()) {
// 成功
} else {
// 失败
}
resultSet.close()
statement.close()
connection.close()

}).start()

附:使用命令行工具Windows PowerShell验证数据库连接 URL

在数据库一直连不上时,找到了这个办法,但是可能还是JDBC的版本问题,在尝试的时候报错了,在此记录一下该方法。

  1. 打开 Windows PowerShell。 Windows 键 + R 组合键:输入 “powershell” 。
  2. 输入以下命令,使用 JDBC 驱动程序测试数据库连接 URL:
1
java -cp <path/to/jdbc/driver.jar>;<path/to/your/code> TestConnection <database_url> <username> <password>

其中:

  • <path/to/jdbc/driver.jar> 是 JDBC 驱动程序的路径。
  • <path/to/your/code> 是你的代码所在的路径。
  • <database_url> 是数据库连接 URL。
  • <username> 是连接数据库所使用的用户名。
  • <password> 是连接数据库所使用的密码。

注:<path/to/your/code> 参数示例

  • 相对路径:.\app\src\main\java\com\example\myapp
  • 绝对路径:C:\AndroidProjects\MyApp\app\src\main\java\com\example\myapp

例如, JDBC 驱动程序位于 “C:\mysql-connector-java-8.0.28.jar”,代码位于 “C:\myproject\src”,数据库连接 URL 是 “jdbc:mysql://localhost:3306/mydatabase”,用户名是 “root”,密码是 “password”,则可以使用以下命令来测试数据库连接 URL:

1
java -cp C:\mysql-connector-java-8.0.28.jar;C:\myproject\src TestConnection jdbc:mysql://localhost:3306/mydatabase root password

如果连接成功,则将看到类似以下内容的输出:

1
Connected to database successfully.

否则,将看到与连接相关的错误信息。根据错误信息,进行修改。

验证Android 应用程序可以通过网络连接到 MySQL 服务器

Way:在 Android 应用程序中使用网络请求库(例如 OkHttp、Volley、Retrofit 等)向 MySQL 服务器发送 HTTP 请求。

将请求发送到服务器上的一个简单的 PHP 脚本,该脚本将连接到 MySQL 数据库并返回一些简单的数据(示例代码为“Hello World”)作为响应。以此验证 Android 应用程序可以通过网络连接到 MySQL 服务器。

PHP 脚本示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$hostname = "localhost";
$username = "root";
$password = "password";
$database = "mydatabase";

$conn = mysqli_connect($hostname, $username, $password, $database);

if (mysqli_connect_errno()) {
echo "Failed to connect to MySQL: " . mysqli_connect_error();
exit();
}

echo "Hello World";

mysqli_close($conn);
?>
  1. 将该脚本保存为 test.php 并将其放置在 Web 服务器的根目录下。
  2. 在 Android 应用程序中使用网络请求库向 http://<your_server_ip>/test.php 发送 GET 请求。
  3. 检查响应是否为 “Hello World”。判断 Android 应用程序能否通过网络连接到 MySQL 服务器。

服务器上传JSON数据,验证账号密码

  1. 在 Android 应用程序中创建一个包含账号和密码的 JSON 对象:

    1
    2
    3
    4
    val json = JSONObject().apply {
    put("username", "<your_username>")
    put("password", "<your_password>")
    }
  2. 在 Android 应用程序中创建一个子线程,使用 HttpURLConnection 或 OkHttp 等库来将 JSON 数据上传到服务器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    val url = URL("<your_server_url>")
    val connection = url.openConnection() as HttpURLConnection
    connection.requestMethod = "POST"
    connection.doOutput = true
    connection.setRequestProperty("Content-Type", "application/json")
    connection.setRequestProperty("Accept", "application/json") //设置的是处理数据格式

    OutputStreamWriter(connection.outputStream).apply {
    write(json.toString())
    flush()
    }

    if (connection.responseCode == HttpURLConnection.HTTP_OK) {
    // 上传成功
    // 在这里处理服务器返回的 JSON 数据,并验证账号密码是否正确
    } else {
    // 上传失败
    }
  3. 在 Android 应用程序中解析服务器返回的 JSON 数据,并验证账号密码是否正确:

    1
    2
    3
    4
    5
    6
    7
    8
    val response = connection.inputStream.bufferedReader().use { it.readText() }
    val jsonResponse = JSONObject(response)

    if (jsonResponse.getBoolean("success")) {
    // 账号密码正确
    } else {
    // 账号密码错误
    }
  4. 在 Android 应用程序中将上述代码放在一个按钮的点击事件中,在用户点击按钮后上传 JSON 数据并验证账号密码是否正确:

    1
    2
    3
    4
    5
    6
    val submitBtn = findViewById<Button>(R.id.submitBtn)
    submitBtn.setOnClickListener {
    Thread(Runnable {
    // 将上述代码放在这里
    }).start()
    }

    val jsonResponse = JSONObject(response)报错

    原因:response 不是一个有效的 JSON 字符串。

在使用 JSONObject 构造函数时,输入的参数必须是一个有效的 JSON 字符串。如果输入的字符串格式不正确或不完整,则可能会导致解析错误。

解决:将服务器返回的响应数据打印出来,检查它是否是一个有效的 JSON 字符串。

1
Log.d("Response", response)

这样在Logcat中就会出现:

同一个局域网(LAN)下,远程计算机访问 MySQL 数据库[失败了]

另一台计算机开启 MySQL 数据库并且设置网络权限,使用该计算机的 IP 地址或主机名来访问该数据库。

在连接字符串中指定该计算机的 IP 地址或主机名,如下所示:

1
jdbc:mysql://<host>:<port>/<database_name>
  • <host> :要访问的计算机的 IP 地址或主机名
  • <port> : MySQL 数据库的端口号(通常是 3306)
  • <database_name> :要连接的数据库的名称

注:从远程计算机访问 MySQL 数据库,该计算机必须启用 MySQL 服务器,并且已经在 MySQL 服务器中为 IP 地址或主机名授予了正确的权限。

MySQL 服务器配置步骤:

1.编辑 MySQL 服务器的配置文件 my.cnf/my.ini,将 bind-address 参数设置为服务器的 IP 地址或 0.0.0.0(表示接受所有远程连接)。————通过查看安装目录,命令行,隐藏文件等方法,都没能找到这个文件,下面步骤配置成功了,但是远程连接还是失败

1
bind-address = 0.0.0.0

2.在 MySQL 服务器中创建一个新用户,并为该用户授予远程访问的权限。以下命令将创建一个名为 remoteuser 的新用户,密码为 password,并为该用户授予访问名为 mydatabase 的数据库的权限:

1
2
CREATE USER 'remoteuser'@'%' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON mydatabase.* TO 'remoteuser'@'%';

'%' 表示该用户可以从任何 IP 地址访问 MySQL 服务器【不安全,应设定为特定的 IP 地址或主机名】

3.重启 MySQL 服务器

Mysql更改从localhost访问到任意IP访问

mysql安装好后默认只能在localhost访问,重新设定后才能用192.***这种ip地址访问数据库

1.在mysql安装路径 bin 下,进入控制台
1
bin>mysql -u root -p
2.把localhost用%代替
1
2
3
4
mysql> use mysql
mysql> update user set host = '%' where user ='root';
mysql> flush privileges;
mysql> select 'host','user' from user where user='root';

同样:用“%”不安全,最好设置为特定ip地址[数据库中没有重要数据,就没有特别设置]

php文件用Apache上传到服务器,从另一台计算机远程访问

注意两点
  1. 确认防火墙没有阻止访问:如果服务器上的防火墙已启用并阻止了 HTTP 请求,需要允许 HTTP 请求通过防火墙。
  2. 确认 Wi-Fi 网络连接正常:确保计算机已连接到同一 Wi-Fi 网络。

防火墙访问设置: Windows Defender 防火墙中 - 高级设置 - 入站规则

Android 应用程序中上传 JSON 数据并验证账号密码

将验证的结果转换为 JSON 数据,然后上传到服务器。服务器可以通过接收到的 JSON 数据进行相应的处理,例如将其存储到数据库中或者验证 JSON 数据中的账号密码是否正确。

将 MySQL 数据库中的数据转换成 JSON 格式,可以使用 MySQL 自带的 JSON 函数,如 JSON_OBJECTJSON_ARRAY,将数据库中的数据转换为 JSON 对象或者 JSON 数组。然后将 JSON 数据上传到服务器进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
// 获取 POST 请求中的 JSON 数据
$json = file_get_contents('php://input');

// 将 JSON 数据解析为关联数组
$data = json_decode($json, true);

// 在此处验证账号密码是否正确


// 返回 JSON 响应
$response = array('success' => true, 'message' => '验证成功');
echo json_encode($response);
?>

注意点:$json = file_get_contents('php://input');这是上传JSON代码,也就是Android中POST的结果,我在处理数据的时候,一直当做Param/Form-data处理,在POST请求服务器测试的时候,Form-data可以,但是Param不行,就以为AS中失败的原因是没有处理Param参数,费了很长时间解决了这个问题[解决方法很简单],在到AS中测试,失败了,崩溃!!!

下面先列出正确执行的步骤:

1.php脚本中验证账号密码[使用 MySQLi(MySQL Improved Extension)扩展库连接 MySQL]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php
$json = file_get_contents('php://input');
$data = json_decode($json, true);
$user = $data['user'];
$password = $data['password'];
//echo "user = $user, password = $password\n";
// 使用解析后的 $user 和 $password 进行后续操作


// 获取请求参数
//$user = isset($_REQUEST["user"]) ? $_REQUEST["user"] : "";
//$password = isset($_REQUEST["password"]) ? $_REQUEST["password"] : "";
//echo "user = $user, password = $password\n";
// 调试语句,查看参数是否正确接收
//echo "user = $user, password = $password\n";
//echo "user = ".$_GET["user"]."\n";

// 数据库连接信息
$servername = "localhost";
$userdbname = "root";
$passdbword = "123456";
$dbname = "mydatabase";

// 创建连接
$conn = new mysqli($servername, $userdbname, $passdbword, $dbname);

// 检查连接是否成功
if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}

// 设置字符集
$conn->set_charset("utf8");

// 获取请求参数
//$user = $user;
//$password = $password;

// 执行查询
$sql = "SELECT * FROM users WHERE user='$user' AND password='$password'";
//echo "user = $user, password = $password\n";
//$sql = "SELECT * FROM users WHERE user='" . $_GET["user"] . "' AND password='" . $_GET["password"] . "'";
$result = $conn->query($sql);

// 检查结果是否存在
if ($result->num_rows > 0) {
// 验证成功
$response["user_exists"] = true;
$response["password_matches"] = true;
} else {
// 验证失败
$response["user_exists"] = false;
$response["password_matches"] = false;
}

// 返回 JSON 格式响应
echo json_encode($response);

// 关闭连接
$conn->close();

?>

细节:

从POST过来的参数,有两种格式(暂只讨论这两种):from-data,param;

. $_GET[“user”] .获得的是param数据

. $_POST[“user”] .获得的是from-data数据

代码块

1
2
3
// 获取请求参数
//$user = isset($_REQUEST["user"]) ? $_REQUEST["user"] : "";
//$password = isset($_REQUEST["password"]) ? $_REQUEST["password"] : "";

作用是获得:POST,GET,COOKIE里的所有参数数值

PHP代码中未正确处理POST请求中的参数,会报错:Undefined array key

确保在客户端中使用的键与PHP代码中的键匹配。如在Postman中使用键“user”和“password”,则确保PHP代码中使用的是相同的键名。

使用var_dump($_POST)在PHP代码中打印出POST请求中的内容,以便检查请求中传递的键和值是否正确。

1
2
3
4
5
<?php 
var_dump($_POST);
?>
//没有的话就提示array(0) { }
// Warning: Undefined array key "***"

以下代码检查是否有 POST 数据被成功提交:

1
2
3
4
5
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
echo "POST data received";
} else {
echo "No POST data received";
}

Postman测试:

当 Postman 发送一个 POST 请求时,需要确保请求的数据是使用 “form-data” 类型发送的,而不是 “x-www-form-urlencoded” 类型。如果使用 “x-www-form-urlencoded” 类型,服务器可能无法正确解析数据,导致无法获取 POST 参数。

即:

AS处理代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
 val submitBtn = findViewById<Button>(R.id.button)
submitBtn.setOnClickListener {
val username = bd.user.text.toString() // 替换为实际输入的用户名
val password = bd.password.text.toString() // 替换为实际输入的密码
//1.创建一个新的线程来执行网络请求,以避免在主线程中进行网络操作而导致 UI 界面卡顿。
Thread {
//2.创建一个 URL 对象,并使用该对象打开一个 HttpURLConnection 连接对象。
val url = URL("http://192.168.56.1/checkp.php")
val connection = url.openConnection() as HttpURLConnection
//3.设置请求方法为 POST,并设置连接属性。
connection.requestMethod = "POST"
connection.doOutput = true
connection.setRequestProperty("Content-Type", "application/json")
/* 如果处理form-data:
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW")
// 创建包含form-data数据的请求体
val multiPart = FormDataMultiPart()
multiPart.field("user", "admin")
multiPart.field("password", "123456")
// 将请求体添加到HttpURLConnection对象中
val outputWriter = OutputStreamWriter(connection.outputStream)
multiPart.writeTo(outputWriter, Charset.defaultCharset()) */
//4.创建一个 JSONObject 对象,并向其中添加要上传的数据。
val jsonObject = JSONObject()
jsonObject.put("user", username)
jsonObject.put("password", password)
//5.将 JSONObject 对象转换成字符串,并写入连接对象的输出流中。
val outputWriter = OutputStreamWriter(connection.outputStream)
outputWriter.write(jsonObject.toString())
outputWriter.flush()
outputWriter.close()
//6.获取服务器的响应状态码,如果状态码为 HttpURLConnection.HTTP_OK(200),则说明请求成功。
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
//7.从连接对象的输入流中读取服务器的响应数据,并将其转换成字符串。
val inputStream = connection.inputStream
val bufferedReader = BufferedReader(InputStreamReader(inputStream))
val response = StringBuilder()
var inputLine: String?
while (bufferedReader.readLine().also { inputLine = it } != null) {
response.append(inputLine)
}
bufferedReader.close()
Log.d("MainActivity", "Response received: $response")
//8.将响应数据转换成 JSONObject 对象,并获取需要的数据。
val jsonResponse = JSONObject(response.toString())
val usernameExists = jsonResponse.getBoolean("user_exists")
val passwordMatches = jsonResponse.getBoolean("password_matches")
//9.根据服务器返回的数据进行相应的处理。
if (usernameExists && passwordMatches) {
// 账号密码正确,执行下一步操作
Looper.prepare();
Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show() //Toast不能直接在子线程中使用
Looper.loop();
} else {
// 账号密码错误,给出提示信息
Looper.prepare();
Toast.makeText(this, "登录失败", Toast.LENGTH_SHORT).show()
Looper.loop();
}
}
}.start()
}

注:outputWriter.close() 不会清空数据,作用是关闭输出流并释放资源,以确保数据被正确地发送到目标服务器。如果在写入完数据之后不关闭输出流,可能会导致数据发送不完整或丢失。关闭输出流后,连接将保持打开状态,以等待来自服务器的响应。如果需要清空数据,可以重新实例化 jsonObject 对象。

connection.setRequestProperty("Content-Type", "application/json") :设置HTTP请求头,设置请求的Content-Type为application/json。告诉服务器发送的数据是JSON格式的,让服务器能够正确解析请求数据。

Postman测试服务器能否正常处理JSON数据:Body-row ,设置为JSON,POST提交验证

附解决 Toast不能直接在子线程中使用 问题链接:——浅析Android中的消息机制

http://t.csdn.cn/ntFKC


Android Studio中通过PHP脚本在Apache服务器验证账号密码
http://example.com/2023/03/21/Android-Studio中通过PHP脚本在Apache服务器验证账号密码/
作者
zhanghao
发布于
2023年3月21日
许可协议