跪拜 Guibai
← Back to the summary

HTTP Is Stateless — That's Why You Need a Token After Login

In the previous article, we already got React to successfully request /api/login.

But there is still one key question:

After a user logs in successfully, how does the server know who this user is?

To understand this, we need to grasp a very important concept:

HTTP is stateless.


1. What does "stateless" mean?

You can simply think of two requests as two completely independent exchanges.

First time:

User:
Hello, I am admin, password is 123456.

Server:
OK, you have logged in successfully.

Request ends.

Then the second time:

User:
I want to access /api/repo.

Server:
Who are you?

The server will not automatically remember you forever just because it received a login request a moment ago.

Therefore:

After a user logs in successfully for the first time, they must obtain a "credential".

The next time they make a request, they bring this credential with them, and only then can the server determine:

Who sent this request?

This credential is:

Token.


2. After a successful login, the server returns a Token

We now modify /api/login.

First, install and import:

import jwt from 'jsonwebtoken';

Then prepare a server-side secret:

const secret = 'secret819!$';

You can simply understand this value as:

A key known only to the server.


3. Use jwt.sign() to generate a Token

After login verification passes:

const token = jwt.sign(
  {
    user: body.username,
    role: 'admin'
  },
  secret,
  {
    expiresIn: 86400
  }
)

The most important part here is:

{
  user: body.username,
  role: 'admin'
}

This is the identity information we want the server to record in the Token.

Then:

jwt.sign(...)

signs this identity object into a JWT.

So you can understand it like this:

Login successful
    ↓
Get user identity information
    ↓
jwt.sign()
    ↓
Generate JWT Token

4. What exactly is JWT?

JWT stands for:

JSON Web Token

For now, you don't need to memorize its structure by heart.

Just understand this first:

JWT is a scheme that puts user identity information into a Token and can be verified for authenticity by the server.

For example, we have:

{
  user: 'admin',
  role: 'admin'
}

After the server signs it, we get a string:

eyJhbGciOiJIUzI1Ni...

Later, when the user comes back with this Token string to make a request,

the server can verify it through:

jwt.verify()

5. Return the Token to the frontend

After a successful login:

return {
  code: 0,
  user: {
    username: body.username
  },
  token
}

Now the browser receives:

{
  "code": 0,
  "user": {
    "username": "admin"
  },
  "token": "eyJ..."
}

The login flow becomes:

User fills in username and password
        ↓
Send /api/login
        ↓
Server verifies username and password
        ↓
jwt.sign() generates Token
        ↓
Token returned to frontend

6. Token is not "the login state itself"

This is a point that is easily confused.

After a successful login, the server gives you a Token.

The essence of a Token is:

A credential used to prove your identity when accessing the server later.

So:

Login

and:

Authentication

are actually two stages.

Login:

Who are you? Is the password correct?

Authentication:

Have you logged in before? Is this Token legitimate?


7. How to send the Token on the next request?

HTTP has a dedicated request Header:

Authorization

Usually written as:

Authorization: Bearer eyJ...

Here:

Bearer

can be simply understood as:

The string that follows is my Token for identity verification.

So when the server receives:

Authorization: Bearer eyJ...

it can extract the Token and verify it.


8. How does the server verify?

After /api/repo receives the request:

const authorization = req.headers?.authorization;

It gets:

Bearer eyJ...

First, check the format:

if (!authorization?.startsWith('Bearer ')) {
  return {
    code: 401,
    message: 'Missing authorization token'
  };
}

Then extract the actual Token:

const token = authorization.slice('Bearer '.length);

Finally:

try {
  const decoded = jwt.verify(token, secret);

  return {
    code: 0,
    data: decoded.user
  };
} catch {
  return {
    code: 401,
    message: 'Invalid token'
  };
}

This is:

Receive Authorization
        ↓
Extract the Token after Bearer
        ↓
jwt.verify()
        ↓
Verify Token
        ↓
Success: Allow access
Failure: Return 401

9. Why is using the same secret important?

When generating the Token:

jwt.sign(payload, secret)

When verifying the Token:

jwt.verify(token, secret)

Both sides must use the same secret.

Otherwise:

sign uses secret A
verify uses secret B

The server will think:

This Token was not issued by me.

And then return:

Invalid token

This is also one of the problems you encountered during debugging earlier.


10. Now the core of JWT is clear

The whole logic really consists of only two actions:

jwt.sign()

Responsible for:

The server issues a Token.

jwt.verify()

Responsible for:

The server verifies a Token.

So don't overcomplicate JWT.

In this demo, it revolves entirely around these two actions.