滑动过期机制——延长 Token有效期

发布于:2025-04-21 ⋅ 阅读:(19) ⋅ 点赞:(0)


前端使用 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 为实际的安全密钥。

网站公告

今日签到

点亮在社区的每一天
去签到