用户登录之后,会返回一个用户的标识,之后带上这个标识请求别的接口,就能识别出该用户。

标识登录状态的方案有两种: session 和 jwt。

session 是通过 cookie 返回一个 id,关联服务端内存里保存的 session 对象,请求时服务端取出 cookie 里 id 对应的 session 对象,就可以拿到用户信息。

jwt 不在服务端存储,会直接把用户信息放到 token 里返回,每次请求带上这个 token,服务端就能从中取出用户信息。

这个 token 一般是放在一个叫 authorization 的 header 里。

这两种方案一个服务端存储,通过 cookie 携带标识,一个在客户端存储,通过 header 携带标识。

session 的方案默认不支持分布式,因为是保存在一台服务器的内存的,另一台服务器没有。

jwt 的方案天然支持分布式,因为信息保存在 token 里,只要从中取出来就行。

所以 jwt 的方案用的还是很多的。

服务端把用户信息放入 token 里,设置一个过期时间,客户端请求的时候通过 authorization 的 header 携带 token,服务端验证通过,就可以从中取到用户信息。

但是这样有个问题:

token 是有过期时间的,比如 3 天,那过期后再访问就需要重新登录了。

这样体验并不好。

想想你在用某个 app 的时候,用着用着突然跳到登录页了,告诉你需要重新登录了。

是不是体验很差?

所以要加上续签机制,也就是延长 token 过期时间。

主流的方案是通过双 token,一个 access_token、一个 refresh_token。

登录成功之后,返回这两个 token:

访问接口时带上 access_token 访问:

当 access_token 过期时,通过 refresh_token 来刷新,拿到新的 access_token 和 refresh_token

这里的 access_token 就是我们之前的 token。

为什么多了个 refresh_token 就能简化呢?

因为如果你重新登录,是不是需要再填一遍用户名密码?而有了 refresh_token 之后,只要带上这个 token 就能标识用户,不需要传用户名密码就能拿到新 token。

而 access_token 一般过期时间设置的比较短,比如 30 分钟,refresh_token 设置的过期时间比较长,比如 7 天。

这样,只要你 7 天内访问一次,就能刷新 token,再续 7 天,一直不需要登录。

但如果你超过 7 天没访问,那 refresh_token 也过期了,就需要重新登录了。

想想你常用的 APP,是不是没再重新登录过?

而不常用的 APP,再次打开是不是就又要重新登录了?

这种一般都是双 token 做的。

知道了什么是双 token,以及它解决的问题,我们来实现一下。

新建个 nest 项目:

 npx nest new token-test

进入项目,把它跑起来:

npm run start:dev

访问 http://localhost:3000 可以看到 hello world,代表服务跑成功了:

在 AppController 添加一个 login 的 post 接口:

@Post('login')

login(@Body() userDto: UserDto) {

    console.log(userDto);

    return 'success';

}

这里通过 @Body 取出请求体的内容,设置到 dto 中。

dto 是 data transfer object,数据传输对象,用来保存参数的。

我们创建 src/user.dto.ts

export class UserDto {

    username: string;

    password: string;

}

在 postman 里访问下这个接口:

返回了 success,服务端也打印了收到的参数:

然后我们实现下登录逻辑:

这里我们就不连接数据库了,就是内置几个用户,匹配下信息。

const users = [

  { username: 'guang', password: '111111', email: 'xxx@xxx.com'},

  { username: 'dong', password: '222222', email: 'yyy@yyy.com'},

]

@Post('login')

login(@Body() userDto: UserDto) {

    const user = users.find(item => item.username === userDto.username);

    if(!user) {

      throw new BadRequestException('用户不存在');

    }

    if(user.password !== userDto.password) {

      throw new BadRequestException("密码错误");

    }

    return {

      userInfo: {

        username: user.username,

        email: user.email

      },

      accessToken: 'xxx',

      refreshToken: 'yyy'

    };

}

如果没找到,就返回用户不存在。

找到了但是密码不对,就返回密码错误。

否则返回用户信息和 token。

测试下:

当 username 不存在时:

当 password 不对时:

登录成功时:

然后我们引入 jwt 模块来生成 token:

npm install @nestjs/jwt

在 AppModule 里注册下这个模块:

JwtModule.register({

  secret: 'guang'

})

然后在 AppController 里就可以注入 JwtService 来用了:

@Inject(JwtService)

private jwtService: JwtService

这个是 nest 的依赖注入功能。

然后用这个 jwtService 生成 access_token 和 refresh_token:

const accessToken = this.jwtService.sign({

  username: user.username,

  email: user.email

}, {

  expiresIn: '0.5h'

});

const refreshToken = this.jwtService.sign({

  username: user.username

}, {

  expiresIn: '7d'

})

access_token 过期时间半小时,refresh_token 过期时间 7 天。

测试下:

登录之后,访问别的接口只要带上这个 access_token 就好了。

前面讲过,jwt 是通过 authorization 的 header 携带 token,格式是 Bearer xxxx

也就是这样:

我们再定义个需要登录访问的接口:

@Get('aaa')

aaa(@Req() req: Request) {

    const authorization = req.headers['authorization'];

    if(!authorization) {

      throw new UnauthorizedException('用户未登录');

    }

    try{

      const token = authorization.split(' ')[1];

      const data = this.jwtService.verify(token);

      console.log(data);

    } catch(e) {

      throw new UnauthorizedException('token 失效,请重新登录');

    }

}

接口里取出 authorization 的 header,如果没有,说明没登录。

然后从中取出 token,用 jwtService.verify 校验下。

如果校验失败,返回 token 失效的错误,否则打印其中的信息。

试一下:

带上 token 访问这个接口:

服务端打印了 token 中的信息,这就是我们登录时放到里面的:

试一下错误的 token:

然后我们实现刷新 token 的接口:

@Get('refresh')

refresh(@Query('token') token: string) {

    try{

      const data = this.jwtService.verify(token);

      const user = users.find(item => item.username === data.username);

      const accessToken = this.jwtService.sign({

        username: user.username,

        email: user.email

      }, {

        expiresIn: '0.5h'

      });

      const refreshToken = this.jwtService.sign({

        username: user.username

      }, {

        expiresIn: '7d'

      })

      return {

        accessToken,

        refreshToken

      };

    } catch(e) {

      throw new UnauthorizedException('token 失效,请重新登录');

    }

}

定义了个 get 接口,参数是 refresh_token。

从 token 中取出 username,然后查询对应的 user 信息,再重新生成双 token 返回。

测试下:

登录之后拿到 refreshToken:

然后带上这个 token 访问刷新接口:

返回了新的 token,这种方式也叫做无感刷新。

那在前端项目里怎么用呢?

我们新建个 react 项目试试:

npx create-react-app --template=typescript token-test-frontend

把它跑起来:

npm run start

因为 3000 端口被占用了,这里跑在了 3001 端口。

成功跑起来了。

我们改下 App.tsx

import { useCallback, useState } from "react";

interface User {

  username: string;

  email?: string;

}

function App() {

  const [user, setUser] = useState();

  const login = useCallback(() => {

    setUser({username: 'guang', email: 'xx@xx.com'});

  }, []);

  return (

    

      {

        user?.username

          ? `当前登录用户: ${ user?.username }`

          : 登录

      }