Nest项目(五)-登录并实现接口鉴权
前言
继续整理一下当前项目的情况,已满足:
- 启动一个服务,端口为 3000
- 可以处理get、post请求,操作数据库并返回自定义数据结构
- 可以通过接口访问经过 ejs 编译后的 html,并按照 ejs 的规则实现数据渲染
- 已经完成了一个前后端分离的项目
紧接着,就是开始搞接口鉴权了。
大部分的项目中都会用到接口鉴权,尤其是B端项目更是需要,nest 推荐的鉴权方法也是使用 jwt 进行登录验证。本篇就基于 jwt 实现了登录接口,并在登录成功以后返回token和用户信息。
配置流程
准备:模拟登录场景
在rest文件中增加如下接口请求:
@host = http://localhost
@port = 3000
@contentType = application/json
###
# 登录
POST {{host}}:{{port}}/login HTTP/1.1
content-type: {{contentType}}
{
"username": "zhangsan",
"password": "123456"
}
如上所示即可使用 restClient 插件发起一个post接口并携带 json 实现 模拟登录请求了。
login 接口开发
注意,访问接口地址为 /login ,所以 要在 app 模块上进行开发:
// app.controller.ts
{
// ...
@Post('login')
postLogin(loginDto: { username: string; password: string }): any {
return this.appService.postLogin(loginDto);
}
// ...
}
// app.service.ts
{
// ...
// 记得把表加载进来
async postLogin(loginDto: { username: string; password: string }) {
const { username, password } = loginDto;
const loginUser = await this.userRepository
.createQueryBuilder('user')
.where('user.username = :username', { username })
.getOne();
if (!loginUser) {
return {
__code: 201,
__message: '用户名不存在!',
};
}
if (password !== loginUser.password) {
return {
__code: 201,
__message: '用户名或密码错误!',
};
}
return loginUser;
}
// ...
}
// app.module.ts
// ...
imports: [
TypeOrmModule.forRoot(CONF_MYSQL) /* 使用 typeorm 链接数据库 */,
TypeOrmModule.forFeature([User]) /* 安装数据模型 */,
UserModule,
],
// ...
如上所示:增加login接口,如果用户名和密码验证通过则返回用户信息。千万记得在module中增加数据挂载数据模型。
数据如下:
在rest文件中发起请求验证一遍:
用户不存在:
密码错误:
登录成功:
如此,登录接口开发完成。
但是常规情况下,接口鉴权肯定是大多数接口需要用到的,如何在需要鉴权的接口上自动实现鉴权?又如何从鉴权结果中拿到用户信息呢?
这就用到了 JWT 技术。
使用 jwt 实现接口鉴权并加密
JWT(JSON Web Token)是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。该 Token 被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该 Token 也可直接被用于认证,也可被加密。
安装:
$ pnpm add passport passport-jwt passport-local @nestjs/passport @nestjs/jwt -S
总体说一下具体配置流程:
- 抽离的方式单独开发jwt配置
- 在app中挂载
1. 准备一个变量用来保存常量
// src/config/index.ts
export const jwtKey = {
secret: 'mentalPublic' /* 密码盐 */
}
2. 编写 jwt 逻辑
// src/common/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { jwtKey } from 'src/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtKey.secret,
});
}
async validate(payload) {
// console.log(
// '🚀 ~ file: jwt.strategy.ts ~ line 17 ~ JwtStrategy ~ validate ~ payload',
// payload,
// );
// 这里的内容接下来可以在接口中获取到
return {
id: payload.id,
username: payload.username,
};
}
}
3. 挂载在 modlue 中
// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CONF_MYSQL } from './config/database';
import { UserModule } from './modules/user/user.module';
import { User } from './modules/user/user.entity';
import { jwtKey } from './config/index';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './common/jwt.strategy';
@Module({
imports: [
TypeOrmModule.forRoot(CONF_MYSQL) /* 使用 typeorm 链接数据库 */,
TypeOrmModule.forFeature([User]) /* 安装数据模型 */,
// 配置jwt
JwtModule.register({
...jwtKey,
signOptions: { expiresIn: '24h' }, /* 过期时间 */
}),
UserModule,
],
controllers: [AppController],
providers: [AppService, JwtStrategy], /* 这里别忘了 */
})
export class AppModule {}
4. 在 service 中使用
import { Injectable } from '@nestjs/common';
import { CONF_MYAQL2 } from './config/database';
import mysql from 'mysql2';
import { User } from './modules/user/user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AppService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
private readonly jwtService: JwtService,
) {}
// 加密方法,生成token,并将 paload 信息压缩进 token 中
async certificate(admin: Admin) {
const payload = {
id: admin.id,
username: admin.username,
}
const token = this.jwtService.sign(payload)
return token
}
getHello(): string {
return 'Hello World!';
}
async postLogin(loginDto: { username: string; password: string }) {
const { username, password } = loginDto;
const loginUser = await this.userRepository
.createQueryBuilder('user')
.where({ username })
.andWhere({ isDelete: false })
.getOne();
if (!loginUser) {
return {
__code: 201,
__message: '用户名不存在!',
};
}
if (password !== loginUser.password) {
return {
__code: 201,
__message: '用户名或密码错误!',
};
}
// 获取token并重新返回前端数据
const token = await this.certificate(loginUser);
return {
token,
info: { username: loginUser.username, sex: loginUser.sex },
};
}
}
如注释所示,现在返回前端的信息已经发生了改变,将 loginUser 结构后,将用来展示的数据返回到前端。
5. 阻挡未登录的请求
现在已经满足鉴权功能了,如果想阻挡未登录的请求,则需要用到 守卫(Guard)。
使用方法很简单,我们改造 user 模块下的接口 getList ,即可验证:
// user.controller.ts
// ...
@UseGuards(AuthGuard('jwt'))
@Get('list')
getList() {
return this.userService.getList();
}
// ...
请求验证:
改造请求体,加上token,token 即登录成功以后返回的 token:
@host = http://localhost
@port = 3000
@contentType = application/json
@auth = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MjEsInVzZXJuYW1lIjoiMTIzMTIzIiwiaWF0IjoxNjcwMzE5MjY1LCJleHAiOjE2NzA0MDU2NjV9.WInB3cTudMrmlJ8yyqV9p5X3i2I1Uu_04W7MRyHb-Dw
###
# 获取列表
GET {{host}}:{{port}}/user/list HTTP/1.1
content-type: {{contentType}}
Authorization: {{auth}}
再次发起请求:
从接口中获取用户信息
这个场景很常见,主要用在一些接口需要将操作者写入表中,或者打印日志时使用。
还是在 getList 中验证:
// user.controller.ts
// ...
@UseGuards(AuthGuard('jwt'))
@Get('list')
getList(@Req() req: Request | any) { /* ts 提示 req 上没有 user 属性,所以加了个 any */
console.log('当前操作用户:', req.user);
return this.userService.getList();
}
// ...
再次请求:
如此就可以从 jwt 中获取到 用户信息了。
下一步计划
至此,从接口层面完成了登录和鉴权。
但是有个问题,既然有了鉴权,那么大部分的接口应该是都需要鉴权的,这样一来大部分的接口都需要写 @UseGuards(AuthGuard('jwt'))
来声明这个接口需要登录以后才能访问。所以下一步就要反过来,让大部分接口默认需要 登录,只有少部分的接口不需要登录的增加一个别的修饰符 Public
来声明这个接口不要登录也可以。
下一步,优化鉴权并增加角色判断,同时为用户增加角色属性。
参考文档
转载自:https://juejin.cn/post/7173981542346129445