用户登录实践 | 青训营

189 阅读5分钟

确定需求和目标

需求

通过用户名和密码进行登录,登录成功后返回用户 id 和权限 token

请求参数

参数类型备注
usernamestring登录用户名
passwordstring登录密码

返回响应

参数类型备注
status_codeinteger状态码,0-成功,其他值-失败
status_msgstring返回状态描述
user_idinteger用户id
tokenstring用户鉴权token

通过观察需求和简单分析我们不难发现,实际上登录和注册的大多数代码是可以复用的。因此,在后面的编写过程中省了不少力气。具体参考用户注册实践 | 青训营 - 掘金 (juejin.cn)

数据库

由于在编写注册接口时已经有了数据库我们可以直接拿来使用。

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `username` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  `token` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `name_password_index` (username,`password`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表\n';

编写接口代码

  1. 连接数据库

    db, err := data.DbConnect(writer)
    

    DbConnector.go:

    package data
    
    import (
        "database/sql"
        "net/http"
    )
    
    func DbConnect(writer http.ResponseWriter) (db *sql.DB, err error) {
        dsn := "root:123456@tcp(localhost:3306)/tiktok" // 要改成自己的数据源
        // 连接数据库
        db, err = sql.Open("mysql", dsn)
        if err != nil {
           http.Error(writer, err.Error(), http.StatusInternalServerError)
           return db, err
        }
    
        // 检查是否连接成功
        err = db.Ping()
        if err != nil {
           http.Error(writer, err.Error(), http.StatusInternalServerError)
           return db, err
        }
        return db, err
    }
    

    连接数据库只需要写:db := service.DbConnect(writer)其中writer是网络响应,在这里用来返回错误状态码

  2. 获取请求体数据

    requestData := handler.HandleRequest(writer, request)
    

    userRequest.go:

    package handler
    
    import (
        "TikTok/model"
        "encoding/json"
        "io/ioutil"
        "net/http"
    )
    
    func HandleRequest(writer http.ResponseWriter, request *http.Request) model.UserRequest {
        var requestData model.UserRequest
        // 检查请求方法是否为POST
        if request.Method != http.MethodPost {
           http.Error(writer, "Method Not Allowed", http.StatusMethodNotAllowed)
           return requestData
        }
    
        // 读取请求体数据
        requestBody, err := ioutil.ReadAll(request.Body)
        defer request.Body.Close()
        if err != nil {
           http.Error(writer, err.Error(), http.StatusInternalServerError)
           return requestData
        }
    
        // 解析JSON数据
        err = json.Unmarshal(requestBody, &requestData)
        if err != nil {
           http.Error(writer, err.Error(), http.StatusBadRequest)
           return requestData
        }
    
        return requestData
    }
    

    首先,创建一个空的 model.UserRequest 对象 requestData 用于存储请求数据。

    接下来,代码检查请求方法是否为 POST。如果请求方法不是 POST,则使用 http.Error 函数向客户端返回一个 "Method Not Allowed" 的错误,并返回空的 requestData

    然后,代码通过使用 ioutil.ReadAll 函数从请求体中读取数据,并将其保存到 requestBody 变量中。之后通过调用 defer request.Body.Close() 来确保在函数结束时关闭请求体。

    如果读取请求体时出现错误,代码使用 http.Error 函数向客户端返回一个 HTTP 500 错误,并返回空的 requestData

    接下来,代码尝试解析 requestBody 中的 JSON 数据到 requestData 变量中,通过调用 json.Unmarshal 函数实现此功能。如果解析过程出现错误,代码使用 http.Error 函数向客户端返回一个 HTTP 400 错误,并返回空的 requestData

    最后,函数返回解析后得到的 requestData 对象。

  3. 执行查询操作,验证用户名和密码

    query := `
        SELECT id, token FROM user WHERE username = ? AND password = ?
    `
    var userID int64
    var token string
    err = db.QueryRow(query, requestData.Username, requestData.Password).Scan(&userID, &token)
    if err != nil {
        http.Error(writer, "用户名或密码错误", http.StatusUnauthorized)
        return
    }
    

    首先,代码定义了一个 SQL 查询语句并将其赋值给 query 变量。该查询语句用于从名为 "user" 的表中选取 "id" 和 "token" 字段,条件是用户名(requestData.Username)和密码(requestData.Password)匹配。

    接下来,代码定义了两个变量 userID(类型为 int64)和 token(类型为 string)用于存储查询结果。

    然后,代码使用 db.QueryRow 函数执行查询,并通过调用 Scan 方法将查询结果扫描并赋值给 userIDtoken 变量。Scan 方法以查询结果的列顺序作为参数,并将查询结果的值依次赋值给对应的变量。如果查询时发生错误,则将错误赋值给 err 变量。

    最后,代码检查 err 变量是否为空。如果不为空,则说明查询过程中出现了错误,代码使用 http.Error 函数向客户端返回一个 HTTP 401 错误(未授权),并返回。此处的错误信息是 "用户名或密码错误"。

  4. 发送响应数据

    handler.HandleResponse(userID, token, writer, "登录成功!")
    

    userResponse.go:

    package handler
    
    import (
        "TikTok/model"
        "encoding/json"
        "net/http"
    )
    
    func HandleResponse(userID int64, token string, writer http.ResponseWriter, StatusMsg string) {
        // 构造响应数据
        responseData := model.UserResponse{
           StatusCode: http.StatusOK,
           StatusMsg:  StatusMsg,
           UserID:     userID,
           Token:      token,
        }
    
        // 将响应转换为JSON并写入响应体中
        responseJSON, err := json.Marshal(responseData)
        if err != nil {
           http.Error(writer, err.Error(), http.StatusInternalServerError)
           return
        }
    
        // 设置响应头部
        writer.Header().Set("Content-Type", "application/json")
    
        // 发送响应数据
        writer.WriteHeader(http.StatusOK)
        writer.Write(responseJSON)
    }
    

    首先,代码创建了一个 model.UserResponse 类型的变量 responseData,并使用传入的参数填充其中的字段。StatusCode 字段被设置为 http.StatusOKStatusMsg 字段被设置为传入的 StatusMsg 参数,UserID 字段被设置为传入的 userID 参数,Token 字段被设置为传入的 token 参数。

    接下来,代码使用 json.Marshal 函数将 responseData 对象转换为 JSON 格式的字节数组 responseJSON。如果转换过程中发生错误,则使用 http.Error 函数向客户端返回一个 HTTP 500 错误,并返回。

    然后,代码使用 writer.Header().Set 方法设置响应头部的 "Content-Type" 字段为 "application/json",以指示响应数据的格式为 JSON。

    接着,代码使用 writer.WriteHeader 方法将响应的状态码设置为 http.StatusOK(200),表示请求成功。

    最后,代码使用 writer.Write 方法将 JSON 格式的响应数据 responseJSON 写入响应体中,将其发送给客户端。

测试和调试

使用单元测试发送模拟请求

func TestLoginHandler(t *testing.T) {
	// 构造测试请求的JSON数据
	jsonData := `{"username": "test_username", "password": "test_password"}`

	// 创建请求体
	requestBody := strings.NewReader(jsonData)

	// 创建模拟请求
	request := httptest.NewRequest(http.MethodPost, "/douyin/user/login/", requestBody)

	// 创建模拟响应写入器
	responseWriter := httptest.NewRecorder()

	// 调用被测试的处理函数
	service.LoginHandler(responseWriter, request)

	// 获取响应
	response := responseWriter.Result()

	// 检查状态码
	if response.StatusCode != http.StatusOK {
		t.Errorf("Expected status code %d, but got %d", http.StatusOK, response.StatusCode)
	}
}

经过测试,很顺利地通过,登录成功。