use cookie to send refreshToken
This commit is contained in:
parent
9be9a1a2b3
commit
699897b071
@ -39,6 +39,8 @@
|
|||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
|
"ms": "3.0.0-canary.1",
|
||||||
"nestjs-prisma": "^0.20.0",
|
"nestjs-prisma": "^0.20.0",
|
||||||
"nodemailer": "^6.9.1",
|
"nodemailer": "^6.9.1",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
@ -50,6 +52,7 @@
|
|||||||
"@nestjs/cli": "^9.0.0",
|
"@nestjs/cli": "^9.0.0",
|
||||||
"@nestjs/schematics": "^9.0.0",
|
"@nestjs/schematics": "^9.0.0",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
|
"@types/cookie-parser": "^1.4.3",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/node": "18.11.18",
|
"@types/node": "18.11.18",
|
||||||
"@types/passport-jwt": "^3.0.8",
|
"@types/passport-jwt": "^3.0.8",
|
||||||
|
@ -14,6 +14,7 @@ specifiers:
|
|||||||
'@nestjs/swagger': ^6.2.1
|
'@nestjs/swagger': ^6.2.1
|
||||||
'@prisma/client': ^4.10.1
|
'@prisma/client': ^4.10.1
|
||||||
'@types/bcrypt': ^5.0.0
|
'@types/bcrypt': ^5.0.0
|
||||||
|
'@types/cookie-parser': ^1.4.3
|
||||||
'@types/express': ^4.17.13
|
'@types/express': ^4.17.13
|
||||||
'@types/jsonwebtoken': ^9.0.1
|
'@types/jsonwebtoken': ^9.0.1
|
||||||
'@types/node': 18.11.18
|
'@types/node': 18.11.18
|
||||||
@ -23,11 +24,13 @@ specifiers:
|
|||||||
bcrypt: ^5.1.0
|
bcrypt: ^5.1.0
|
||||||
class-transformer: ^0.5.1
|
class-transformer: ^0.5.1
|
||||||
class-validator: ^0.14.0
|
class-validator: ^0.14.0
|
||||||
|
cookie-parser: ^1.4.6
|
||||||
eslint: ^8.0.1
|
eslint: ^8.0.1
|
||||||
eslint-config-prettier: ^8.3.0
|
eslint-config-prettier: ^8.3.0
|
||||||
eslint-plugin-prettier: ^4.0.0
|
eslint-plugin-prettier: ^4.0.0
|
||||||
husky: ^8.0.0
|
husky: ^8.0.0
|
||||||
lint-staged: ^13.1.2
|
lint-staged: ^13.1.2
|
||||||
|
ms: 3.0.0-canary.1
|
||||||
nestjs-prisma: ^0.20.0
|
nestjs-prisma: ^0.20.0
|
||||||
nodemailer: ^6.9.1
|
nodemailer: ^6.9.1
|
||||||
passport: ^0.6.0
|
passport: ^0.6.0
|
||||||
@ -58,6 +61,8 @@ dependencies:
|
|||||||
bcrypt: 5.1.0
|
bcrypt: 5.1.0
|
||||||
class-transformer: 0.5.1
|
class-transformer: 0.5.1
|
||||||
class-validator: 0.14.0
|
class-validator: 0.14.0
|
||||||
|
cookie-parser: 1.4.6
|
||||||
|
ms: 3.0.0-canary.1
|
||||||
nestjs-prisma: 0.20.0_uhhmeuf5jto6tk72f36tv2cdfe
|
nestjs-prisma: 0.20.0_uhhmeuf5jto6tk72f36tv2cdfe
|
||||||
nodemailer: 6.9.1
|
nodemailer: 6.9.1
|
||||||
passport: 0.6.0
|
passport: 0.6.0
|
||||||
@ -69,6 +74,7 @@ devDependencies:
|
|||||||
'@nestjs/cli': 9.2.0
|
'@nestjs/cli': 9.2.0
|
||||||
'@nestjs/schematics': 9.0.4_typescript@4.9.5
|
'@nestjs/schematics': 9.0.4_typescript@4.9.5
|
||||||
'@types/bcrypt': 5.0.0
|
'@types/bcrypt': 5.0.0
|
||||||
|
'@types/cookie-parser': 1.4.3
|
||||||
'@types/express': 4.17.17
|
'@types/express': 4.17.17
|
||||||
'@types/node': 18.11.18
|
'@types/node': 18.11.18
|
||||||
'@types/passport-jwt': 3.0.8
|
'@types/passport-jwt': 3.0.8
|
||||||
@ -756,6 +762,12 @@ packages:
|
|||||||
'@types/node': 18.11.18
|
'@types/node': 18.11.18
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/cookie-parser/1.4.3:
|
||||||
|
resolution: {integrity: sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==}
|
||||||
|
dependencies:
|
||||||
|
'@types/express': 4.17.17
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/ejs/3.1.2:
|
/@types/ejs/3.1.2:
|
||||||
resolution: {integrity: sha512-ZmiaE3wglXVWBM9fyVC17aGPkLo/UgaOjEiI2FXQfyczrCefORPxIe+2dVmnmk3zkVIbizjrlQzmPGhSYGXG5g==}
|
resolution: {integrity: sha512-ZmiaE3wglXVWBM9fyVC17aGPkLo/UgaOjEiI2FXQfyczrCefORPxIe+2dVmnmk3zkVIbizjrlQzmPGhSYGXG5g==}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
@ -1754,10 +1766,23 @@ packages:
|
|||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/cookie-parser/1.4.6:
|
||||||
|
resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==}
|
||||||
|
engines: {node: '>= 0.8.0'}
|
||||||
|
dependencies:
|
||||||
|
cookie: 0.4.1
|
||||||
|
cookie-signature: 1.0.6
|
||||||
|
dev: false
|
||||||
|
|
||||||
/cookie-signature/1.0.6:
|
/cookie-signature/1.0.6:
|
||||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/cookie/0.4.1:
|
||||||
|
resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/cookie/0.5.0:
|
/cookie/0.5.0:
|
||||||
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
|
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@ -3989,6 +4014,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/ms/3.0.0-canary.1:
|
||||||
|
resolution: {integrity: sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g==}
|
||||||
|
engines: {node: '>=12.13'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/multer/1.4.4-lts.1:
|
/multer/1.4.4-lts.1:
|
||||||
resolution: {integrity: sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==}
|
resolution: {integrity: sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==}
|
||||||
engines: {node: '>= 6.0.0'}
|
engines: {node: '>= 6.0.0'}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { registerAs, ConfigType } from '@nestjs/config'
|
import { registerAs, ConfigType } from '@nestjs/config'
|
||||||
|
import ms, { StringValue } from 'ms'
|
||||||
|
|
||||||
export const securityConfig = registerAs('security', () => ({
|
export const securityConfig = registerAs('security', () => ({
|
||||||
jwt_access_secret: process.env.JWT_ACCESS_SECRET || 'JWT_ACCESS_SECRET',
|
jwt_access_secret: process.env.JWT_ACCESS_SECRET || 'JWT_ACCESS_SECRET',
|
||||||
jwt_refresh_secret: process.env.JWT_REFRESH_SECRET || 'JWT_REFRESH_SECRET',
|
jwt_refresh_secret: process.env.JWT_REFRESH_SECRET || 'JWT_REFRESH_SECRET',
|
||||||
expiresIn: process.env.expiresIn || '15m',
|
expiresIn: ms((process.env.expiresIn || '15m') as StringValue),
|
||||||
refreshIn: process.env.refreshIn || '7d',
|
refreshIn: ms((process.env.refreshIn || '7d') as StringValue),
|
||||||
bcryptSaltOrRound: Number(process.env.bcryptSaltOrRound) || 10,
|
bcryptSaltOrRound: Number(process.env.bcryptSaltOrRound) || 10,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
9
src/common/decorators/cookies.decorator.ts
Normal file
9
src/common/decorators/cookies.decorator.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common'
|
||||||
|
|
||||||
|
export const Cookies = createParamDecorator(
|
||||||
|
(data: string, ctx: ExecutionContext) => {
|
||||||
|
const request = ctx.switchToHttp().getRequest()
|
||||||
|
|
||||||
|
return data ? request.cookies?.[data] : request.cookies
|
||||||
|
},
|
||||||
|
)
|
@ -3,6 +3,7 @@ import { ValidationPipe } from '@nestjs/common'
|
|||||||
import { ConfigService } from '@nestjs/config'
|
import { ConfigService } from '@nestjs/config'
|
||||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
|
||||||
import { PrismaClientExceptionFilter, PrismaService } from 'nestjs-prisma'
|
import { PrismaClientExceptionFilter, PrismaService } from 'nestjs-prisma'
|
||||||
|
import * as CookieParser from 'cookie-parser'
|
||||||
import { JwtExceptionsFilter } from './common/filters/jwt-exceptions.filter'
|
import { JwtExceptionsFilter } from './common/filters/jwt-exceptions.filter'
|
||||||
import { AppModule } from './app.module'
|
import { AppModule } from './app.module'
|
||||||
|
|
||||||
@ -21,9 +22,13 @@ async function bootstrap() {
|
|||||||
app.useGlobalFilters(new PrismaClientExceptionFilter(httpAdapter))
|
app.useGlobalFilters(new PrismaClientExceptionFilter(httpAdapter))
|
||||||
app.useGlobalFilters(new JwtExceptionsFilter(httpAdapter))
|
app.useGlobalFilters(new JwtExceptionsFilter(httpAdapter))
|
||||||
|
|
||||||
|
// CookieParser
|
||||||
|
app.use(CookieParser())
|
||||||
|
|
||||||
// Swagger Api
|
// Swagger Api
|
||||||
const options = new DocumentBuilder()
|
const options = new DocumentBuilder()
|
||||||
.addBearerAuth()
|
.addBearerAuth()
|
||||||
|
.addCookieAuth('refreshToken')
|
||||||
.setTitle('Nest Project')
|
.setTitle('Nest Project')
|
||||||
.setDescription('The Nest-Project API description')
|
.setDescription('The Nest-Project API description')
|
||||||
.setVersion('1.0')
|
.setVersion('1.0')
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import '@nestjs/mapped-types'
|
import '@nestjs/mapped-types'
|
||||||
import { ApiProperty } from '@nestjs/swagger'
|
import { ApiProperty } from '@nestjs/swagger'
|
||||||
import { IsString } from 'class-validator'
|
|
||||||
|
|
||||||
export class Token {
|
export class Token {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@ -9,11 +8,6 @@ export class Token {
|
|||||||
refreshToken: string
|
refreshToken: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TokenRefreshPayload {
|
|
||||||
@IsString()
|
|
||||||
refreshToken: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TokenPayload {
|
export class TokenPayload {
|
||||||
userId: string
|
userId: string
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,14 @@ import {
|
|||||||
Patch,
|
Patch,
|
||||||
Put,
|
Put,
|
||||||
Body,
|
Body,
|
||||||
|
Res,
|
||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
Query,
|
Query,
|
||||||
|
UnauthorizedException,
|
||||||
} from '@nestjs/common'
|
} from '@nestjs/common'
|
||||||
|
import type { Response } from 'express'
|
||||||
|
import { Cookies } from 'src/common/decorators/cookies.decorator'
|
||||||
import { MeService } from './me.service'
|
import { MeService } from './me.service'
|
||||||
import { ApiTags, ApiOperation, ApiUnauthorizedResponse } from '@nestjs/swagger'
|
import { ApiTags, ApiOperation, ApiUnauthorizedResponse } from '@nestjs/swagger'
|
||||||
import { User } from 'src/common/decorators/user.decorator'
|
import { User } from 'src/common/decorators/user.decorator'
|
||||||
@ -19,7 +23,6 @@ import { UserEntity } from './entities/user.entity'
|
|||||||
import { DeleteUserDto } from './dto/delete-user.dto'
|
import { DeleteUserDto } from './dto/delete-user.dto'
|
||||||
import { ChangePassword } from './dto/change-password.dto'
|
import { ChangePassword } from './dto/change-password.dto'
|
||||||
import { UpdateUserDto } from './dto/update-user.dto'
|
import { UpdateUserDto } from './dto/update-user.dto'
|
||||||
import { TokenRefreshPayload } from './dto/token.dto'
|
|
||||||
import { ChangeEmailDto } from './dto/change-email.dto'
|
import { ChangeEmailDto } from './dto/change-email.dto'
|
||||||
|
|
||||||
@Controller('api/users/me')
|
@Controller('api/users/me')
|
||||||
@ -93,7 +96,18 @@ export class MeController {
|
|||||||
@Put('token')
|
@Put('token')
|
||||||
@ApiOperation({ summary: '刷新token' })
|
@ApiOperation({ summary: '刷新token' })
|
||||||
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
|
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
|
||||||
async updateAccessToken(@Body() payload: TokenRefreshPayload) {
|
async updateAccessToken(
|
||||||
return this.meService.updateAccessToken(payload.refreshToken)
|
@Res({ passthrough: true }) res: Response,
|
||||||
|
@Cookies('refreshToken') refreshToken: string,
|
||||||
|
) {
|
||||||
|
if (!refreshToken) {
|
||||||
|
throw new UnauthorizedException('no refresh token')
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return this.meService.updateAccessToken(refreshToken)
|
||||||
|
} catch (err) {
|
||||||
|
res.clearCookie('refreshToken')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,17 +77,13 @@ export class MeService {
|
|||||||
if (iat * 1000 < user.updatedAt.getTime()) {
|
if (iat * 1000 < user.updatedAt.getTime()) {
|
||||||
throw new UnauthorizedException('token失效,请重新登录')
|
throw new UnauthorizedException('token失效,请重新登录')
|
||||||
}
|
}
|
||||||
return {
|
return this.jwtService.sign(
|
||||||
userId,
|
|
||||||
refreshToken: refreshToken,
|
|
||||||
accessToken: this.jwtService.sign(
|
|
||||||
{ userId },
|
{ userId },
|
||||||
{
|
{
|
||||||
secret: this.secureConfig.jwt_refresh_secret,
|
secret: this.secureConfig.jwt_access_secret,
|
||||||
expiresIn: this.secureConfig.refreshIn,
|
expiresIn: this.secureConfig.refreshIn,
|
||||||
},
|
},
|
||||||
),
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkPassword(pwd: string, hashPwd: string) {
|
private async checkPassword(pwd: string, hashPwd: string) {
|
||||||
|
@ -1,31 +1,64 @@
|
|||||||
import { Controller, Post, Patch, Body, Param } from '@nestjs/common'
|
import { Controller, Inject, Post, Patch, Body, Res } from '@nestjs/common'
|
||||||
|
import type { Response, CookieOptions } from 'express'
|
||||||
import { UsersService } from './users.service'
|
import { UsersService } from './users.service'
|
||||||
import { ApiTags, ApiOperation } from '@nestjs/swagger'
|
import { ApiTags, ApiOperation } from '@nestjs/swagger'
|
||||||
import { CreateUserDto } from './dto/create-user.dto'
|
import { CreateUserDto } from './dto/create-user.dto'
|
||||||
import { LoginInputDto } from './dto/login-input.dto'
|
import { LoginInputDto } from './dto/login-input.dto'
|
||||||
import { ResetPassword } from './dto/reset-password.dto'
|
import { ResetPassword } from './dto/reset-password.dto'
|
||||||
|
import { securityConfig, SecurityConfig } from 'src/common/configs'
|
||||||
|
|
||||||
@Controller('api/users')
|
@Controller('api/users')
|
||||||
@ApiTags('Users')
|
@ApiTags('Users')
|
||||||
export class UsersController {
|
export class UsersController {
|
||||||
constructor(private readonly usersService: UsersService) {}
|
private refreshTokenCookieConfig: CookieOptions = {
|
||||||
|
path: '/api/users/me/token',
|
||||||
|
secure: true,
|
||||||
|
maxAge: this.secureConfig.refreshIn / 1000,
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'lax',
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly usersService: UsersService,
|
||||||
|
@Inject(securityConfig.KEY) private secureConfig: SecurityConfig,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({ summary: '邮箱注册' })
|
@ApiOperation({ summary: '邮箱注册' })
|
||||||
async registerByEmail(@Body() userData: CreateUserDto) {
|
async registerByEmail(
|
||||||
return this.usersService.registerByEmail(userData)
|
@Body() userData: CreateUserDto,
|
||||||
|
@Res({ passthrough: true }) res: Response,
|
||||||
|
) {
|
||||||
|
const { refreshToken, accessToken } =
|
||||||
|
await this.usersService.registerByEmail(userData)
|
||||||
|
res.cookie('refreshToken', refreshToken, this.refreshTokenCookieConfig)
|
||||||
|
return accessToken
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 限制调用频率,避免暴力破解
|
// TODO: 限制调用频率,避免暴力破解
|
||||||
@Post('token')
|
@Post('token')
|
||||||
@ApiOperation({ summary: '邮箱登录' })
|
@ApiOperation({ summary: '邮箱登录' })
|
||||||
async loginByEmail(@Body() user: LoginInputDto) {
|
async loginByEmail(
|
||||||
return this.usersService.loginByEmail(user.email, user.password)
|
@Body() user: LoginInputDto,
|
||||||
|
@Res({ passthrough: true }) res: Response,
|
||||||
|
) {
|
||||||
|
const { refreshToken, accessToken } = await this.usersService.loginByEmail(
|
||||||
|
user.email,
|
||||||
|
user.password,
|
||||||
|
)
|
||||||
|
res.cookie('refreshToken', refreshToken, this.refreshTokenCookieConfig)
|
||||||
|
return accessToken
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('password')
|
@Patch('password')
|
||||||
@ApiOperation({ summary: '找回密码' })
|
@ApiOperation({ summary: '找回密码' })
|
||||||
async forgetPassword(@Body() payload: ResetPassword) {
|
async forgetPassword(
|
||||||
return this.usersService.resetPasswordByEmail(payload)
|
@Body() payload: ResetPassword,
|
||||||
|
@Res({ passthrough: true }) res: Response,
|
||||||
|
) {
|
||||||
|
const { refreshToken, accessToken } =
|
||||||
|
await this.usersService.resetPasswordByEmail(payload)
|
||||||
|
res.cookie('refreshToken', refreshToken, this.refreshTokenCookieConfig)
|
||||||
|
return accessToken
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user