refactor: auth jwt

This commit is contained in:
Innei
2022-06-10 23:28:38 +08:00
committed by
parent 0d67b0be4a
commit c1282412c6
20 changed files with 113 additions and 200 deletions

View File

@@ -1,5 +1,3 @@
import jwtoken from 'jsonwebtoken'
import {
CanActivate,
ExecutionContext,
@@ -7,8 +5,10 @@ import {
UnauthorizedException,
} from '@nestjs/common'
import { __secret, isTest } from '~/global/env.global'
import { isTest } from '~/global/env.global'
import { mockUser1 } from '~/mock/user.mock'
import { ConfigsService } from '~/modules/configs/configs.service'
import { JWTService } from '~/processors/helper/helper.jwt.service'
import { getNestExecutionContextRequest } from '~/transformers/get-req.transformer'
/**
@@ -17,13 +17,13 @@ import { getNestExecutionContextRequest } from '~/transformers/get-req.transform
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
protected readonly jwtService: JWTService,
protected readonly configs: ConfigsService,
) {}
async canActivate(context: ExecutionContext): Promise<any> {
const request = this.getRequest(context)
if (typeof request.user !== 'undefined') {
return true
}
/// for e2e-test mock user
if (isTest) {
request.user = { ...mockUser1 }
@@ -31,18 +31,21 @@ export class AuthGuard implements CanActivate {
}
const query = request.query as any
const headers = request.headers
const Authorization =
const Authorization: string =
headers.authorization || headers.Authorization || query.token
if (!Authorization) {
throw new UnauthorizedException()
}
const jwt = Authorization.replace('Bearer ', '')
try {
const payload = jwtoken.verify(jwt, __secret)
} catch {
const jwt = Authorization.replace(/[Bb]earer /, '')
const ok = await this.jwtService.verify(jwt)
if (!ok) {
throw new UnauthorizedException()
}
request.user = await this.configs.getMaster()
request.token = jwt
return true
}
getRequest(context: ExecutionContext) {

View File

@@ -1,49 +1,35 @@
/*
* @Author: Innei
* @Date: 2020-11-24 16:20:37
* @LastEditTime: 2021-03-21 18:13:17
* @LastEditors: Innei
* @FilePath: /server/apps/server/src/auth/roles.guard.ts
* Mark: Coding with Love
*/
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
import { isTest } from '~/global/env.global'
import { AuthService } from '~/modules/auth/auth.service'
import { ConfigsService } from '~/modules/configs/configs.service'
import { JWTService } from '~/processors/helper/helper.jwt.service'
import { getNestExecutionContextRequest } from '~/transformers/get-req.transformer'
import { AuthGuard } from './auth.guard'
/**
* 区分游客和主人的守卫
*/
@Injectable()
export class RolesGuard extends AuthGuard('jwt') implements CanActivate {
constructor(private readonly authService: AuthService) {
super(authService)
export class RolesGuard extends AuthGuard implements CanActivate {
constructor(
protected readonly jwtService: JWTService,
protected readonly configs: ConfigsService,
) {
super(jwtService, configs)
}
async canActivate(context: ExecutionContext): Promise<boolean> {
let isMaster = false
const request = this.getRequest(context)
const authorization = request.headers.authorization
if (authorization) {
try {
isMaster = (await super.canActivate(context)) as boolean
} catch {}
// FIXME test env
if (!isMaster && !isTest) {
const [isValidToken, userModel] =
await this.authService.verifyCustomToken(authorization as string)
if (isValidToken) {
request.user = userModel!
isMaster = true
return true
}
}
}
let isMaster = false
try {
await super.canActivate(context)
isMaster = true
// eslint-disable-next-line no-empty
} catch {}
request.isGuest = !isMaster
request.isMaster = isMaster
return true
}

View File

@@ -1,32 +1,4 @@
import cluster from 'cluster'
import { machineIdSync } from 'node-machine-id'
import { CLUSTER, SECURITY } from '~/app.config'
export const isDev = process.env.NODE_ENV == 'development'
export const isTest = !!process.env.TEST
export const cwd = process.cwd()
const getMachineId = () => {
const id = machineIdSync()
if (isDev && cluster.isPrimary) {
console.log(id)
}
return id
}
export const __secret: any =
SECURITY.jwtSecret ||
Buffer.from(getMachineId()).toString('base64').slice(0, 15) ||
'asjhczxiucipoiopiqm2376'
if (isDev && cluster.isPrimary) {
console.log(__secret)
}
if (!CLUSTER.enable || cluster.isPrimary) {
console.log(
'JWT Secret start with :',
__secret.slice(0, 5) + '*'.repeat(__secret.length - 5),
)
}

View File

@@ -5,7 +5,7 @@ export const mockUser1: UserModel = {
name: 'John Doe',
mail: 'example@ee.com',
password: '**********',
authCode: '*****',
username: 'johndoe',
created: new Date('2021/1/1 10:00:11'),
}
@@ -15,7 +15,7 @@ export const mockUser2: UserModel = {
name: 'Shawn Carter',
mail: 'example@ee.com',
password: '**********',
authCode: '*****',
username: 'shawn',
created: new Date('2020/10/10 19:22:22'),
}

View File

@@ -1,6 +1,6 @@
import dayjs from 'dayjs'
import { Controller, Delete, Get, HttpCode, Query, Scope } from '@nestjs/common'
import { Controller, Delete, Get, HttpCode, Query } from '@nestjs/common'
import { Auth } from '~/common/decorator/auth.decorator'
import { Paginator } from '~/common/decorator/http.decorator'
@@ -14,7 +14,7 @@ import { getTodayEarly, getWeekStart } from '~/utils/time.util'
import { AnalyzeDto } from './analyze.dto'
import { AnalyzeService } from './analyze.service'
@Controller({ path: 'analyze', scope: Scope.REQUEST })
@Controller({ path: 'analyze' })
@ApiName
@Auth()
export class AnalyzeController {

View File

@@ -15,7 +15,6 @@ import {
NotFoundException,
Post,
Query,
Scope,
} from '@nestjs/common'
import { EventEmitter2 } from '@nestjs/event-emitter'
import { ApiBearerAuth, ApiOperation } from '@nestjs/swagger'
@@ -41,7 +40,6 @@ export class TokenDto {
@Controller({
path: 'auth',
scope: Scope.REQUEST,
})
@ApiName
export class AuthController {
@@ -66,7 +64,7 @@ export class AuthController {
if (typeof token === 'string') {
return await this.authService
.verifyCustomToken(token)
.then(([isValid, user]) => isValid)
.then(([isValid]) => isValid)
}
if (id && typeof id === 'string' && isMongoId(id)) {
return await this.authService.getTokenSecret(id)

View File

@@ -3,50 +3,28 @@ import { isDate, omit } from 'lodash'
import { customAlphabet } from 'nanoid/async'
import { Injectable } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { ReturnModelType } from '@typegoose/typegoose'
import { MasterLostException } from '~/common/exceptions/master-lost.exception'
import { alphabet } from '~/constants/other.constant'
import {
TokenModel,
UserModel as User,
UserDocument,
UserModel,
} from '~/modules/user/user.model'
import { JWTService } from '~/processors/helper/helper.jwt.service'
import { InjectModel } from '~/transformers/model.transformer'
import { TokenDto } from './auth.controller'
import { JwtPayload } from './interfaces/jwt-payload.interface'
@Injectable()
export class AuthService {
constructor(
@InjectModel(User) private readonly userModel: ReturnModelType<typeof User>,
private readonly jwtService: JwtService,
private readonly jwtService: JWTService,
) {}
async signToken(_id: string) {
const user = await this.userModel.findById(_id).select('authCode')
if (!user) {
throw new MasterLostException()
}
const authCode = user.authCode
const payload = {
_id,
authCode,
}
return this.jwtService.sign(payload)
}
async verifyPayload(payload: JwtPayload): Promise<UserDocument | null> {
const user = await this.userModel.findById(payload._id).select('+authCode')
if (!user) {
throw new MasterLostException()
}
return user && user.authCode === payload.authCode ? user : null
get jwtServicePublic() {
return this.jwtService
}
private async getAccessTokens() {

View File

@@ -1,4 +0,0 @@
export interface JwtPayload {
_id: string
authCode: string
}

View File

@@ -12,7 +12,6 @@ import {
Post,
Query,
Req,
Scope,
UnprocessableEntityException,
} from '@nestjs/common'
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger'
@@ -26,7 +25,7 @@ import { getMediumDateTime } from '~/utils'
import { BackupService } from './backup.service'
@Controller({ path: 'backups', scope: Scope.REQUEST })
@Controller({ path: 'backups' })
@ApiName
@Auth()
@BanInDemo

View File

@@ -12,7 +12,6 @@ import {
Post,
Query,
Res,
Scope,
UnprocessableEntityException,
} from '@nestjs/common'
import { Reflector } from '@nestjs/core'
@@ -34,7 +33,6 @@ import { LogQueryDto, LogTypeDto } from './health.dto'
@Controller({
path: 'health',
scope: Scope.REQUEST,
})
@Auth()
@ApiName

View File

@@ -1,4 +1,4 @@
import { Controller, Get, Scope } from '@nestjs/common'
import { Controller, Get } from '@nestjs/common'
import { Auth } from '~/common/decorator/auth.decorator'
import { ApiName } from '~/common/decorator/openapi.decorator'
@@ -7,7 +7,7 @@ import { PTYService } from './pty.service'
@ApiName
@Auth()
@Controller({ path: 'pty', scope: Scope.REQUEST })
@Controller({ path: 'pty' })
export class PTYController {
constructor(private readonly service: PTYService) {}

View File

@@ -3,7 +3,6 @@ import { nanoid } from 'nanoid'
import { IPty, spawn } from 'node-pty'
import { Socket } from 'socket.io'
import { JwtService } from '@nestjs/jwt'
import {
GatewayMetadata,
OnGatewayConnection,
@@ -20,6 +19,7 @@ import { AuthService } from '~/modules/auth/auth.service'
import { ConfigsService } from '~/modules/configs/configs.service'
import { CacheService } from '~/processors/cache/cache.service'
import { createAuthGateway } from '~/processors/gateway/shared/auth.gateway'
import { JWTService } from '~/processors/helper/helper.jwt.service'
import { getIp, getRedisKey } from '~/utils'
const AuthGateway = createAuthGateway({ namespace: 'pty', authway: 'jwt' })
@@ -29,7 +29,7 @@ export class PTYGateway
implements OnGatewayConnection, OnGatewayDisconnect
{
constructor(
protected readonly jwtService: JwtService,
protected readonly jwtService: JWTService,
protected readonly authService: AuthService,
protected readonly cacheService: CacheService,
protected readonly configService: ConfigsService,

View File

@@ -4,6 +4,7 @@ import { ApiOperation } from '@nestjs/swagger'
import { Auth } from '~/common/decorator/auth.decorator'
import { HttpCache } from '~/common/decorator/cache.decorator'
import { CurrentUser } from '~/common/decorator/current-user.decorator'
import { BanInDemo } from '~/common/decorator/demo.decorator'
import { IpLocation, IpRecord } from '~/common/decorator/ip.decorator'
import { ApiName } from '~/common/decorator/openapi.decorator'
import { IsMaster } from '~/common/decorator/role.decorator'
@@ -46,7 +47,7 @@ export class UserController {
const avatar = user.avatar ?? getAvatar(mail)
return {
token: await this.authService.signToken(user._id),
token: this.authService.jwtServicePublic.sign(user._id),
...footstep,
name,
username,
@@ -71,6 +72,7 @@ export class UserController {
@ApiOperation({ summary: '修改主人的信息' })
@Auth()
@HttpCache.disable
@BanInDemo
async patchMasterData(
@Body() body: UserPatchDto,
@CurrentUser() user: UserDocument,

View File

@@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { compareSync } from 'bcrypt'
import { nanoid } from 'nanoid'
import {
BadRequestException,
@@ -15,7 +14,7 @@ import { MasterLostException } from '~/common/exceptions/master-lost.exception'
import { RedisKeys } from '~/constants/cache.constant'
import { CacheService } from '~/processors/cache/cache.service'
import { InjectModel } from '~/transformers/model.transformer'
import { banInDemo, getAvatar, sleep } from '~/utils'
import { getAvatar, sleep } from '~/utils'
import { getRedisKey } from '~/utils/redis.util'
import { AuthService } from '../auth/auth.service'
@@ -82,7 +81,7 @@ export class UserService {
// @ts-ignore
const res = await this.userModel.create({ ...model })
const token = await this.authService.signToken(res._id)
const token = await this.authService.jwtServicePublic.sign(res._id)
return { token, username: res.username }
}
@@ -94,7 +93,6 @@ export class UserService {
* @param {Partial} data - 部分修改数据
*/
async patchUserData(user: UserDocument, data: Partial<UserModel>) {
banInDemo()
const { password } = data
const doc = { ...data }
if (password !== undefined) {
@@ -113,9 +111,8 @@ export class UserService {
throw new UnprocessableEntityException('密码可不能和原来的一样哦')
}
// 2. 认证码重新生成
const newCode = nanoid(10)
doc.authCode = newCode
// 2. 撤销所有 token
await this.authService.jwtServicePublic.invokeAll()
}
return await this.userModel.updateOne({ _id: user._id }, doc)
}

View File

@@ -1,7 +1,6 @@
import { resolve } from 'path'
import SocketIO, { Socket } from 'socket.io'
import { JwtService } from '@nestjs/jwt'
import {
GatewayMetadata,
OnGatewayConnection,
@@ -12,6 +11,7 @@ import {
import { LOG_DIR } from '~/constants/path.constant'
import { CacheService } from '~/processors/cache/cache.service'
import { JWTService } from '~/processors/helper/helper.jwt.service'
import { BusinessEvents } from '../../../constants/business-event.constant'
import { AuthService } from '../../../modules/auth/auth.service'
@@ -24,7 +24,7 @@ export class AdminEventsGateway
implements OnGatewayConnection, OnGatewayDisconnect
{
constructor(
protected readonly jwtService: JwtService,
protected readonly jwtService: JWTService,
protected readonly authService: AuthService,
private readonly cacheService: CacheService,
) {

View File

@@ -1,7 +1,6 @@
import { Namespace, Socket } from 'socket.io'
import { OnEvent } from '@nestjs/event-emitter'
import { JwtService } from '@nestjs/jwt'
import {
OnGatewayConnection,
OnGatewayDisconnect,
@@ -12,6 +11,7 @@ import { Emitter } from '@socket.io/redis-emitter'
import { EventBusEvents } from '~/constants/event-bus.constant'
import { AuthService } from '~/modules/auth/auth.service'
import { CacheService } from '~/processors/cache/cache.service'
import { JWTService } from '~/processors/helper/helper.jwt.service'
import { BusinessEvents } from '../../../constants/business-event.constant'
import { BoardcastBaseGateway } from '../base.gateway'
@@ -33,7 +33,7 @@ export const createAuthGateway = (
const { namespace, authway = 'all' } = options
class AuthGateway extends BoardcastBaseGateway implements IAuthGateway {
constructor(
protected readonly jwtService: JwtService,
protected readonly jwtService: JWTService,
protected readonly authService: AuthService,
private readonly cacheService: CacheService,
) {
@@ -66,9 +66,9 @@ export const createAuthGateway = (
const validJwt = async () => {
try {
const payload = this.jwtService.verify(token)
const user = await this.authService.verifyPayload(payload)
if (!user) {
const ok = await this.jwtService.verify(token)
if (!ok) {
return false
}
} catch {

View File

@@ -1,4 +1,3 @@
import { JwtService } from '@nestjs/jwt'
import {
GatewayMetadata,
OnGatewayConnection,
@@ -7,6 +6,7 @@ import {
} from '@nestjs/websockets'
import { CacheService } from '~/processors/cache/cache.service'
import { JWTService } from '~/processors/helper/helper.jwt.service'
import { AuthService } from '../../../modules/auth/auth.service'
import { createAuthGateway } from '../shared/auth.gateway'
@@ -22,7 +22,7 @@ export class SystemEventsGateway
implements OnGatewayConnection, OnGatewayDisconnect
{
constructor(
protected readonly jwtService: JwtService,
protected readonly jwtService: JWTService,
protected readonly authService: AuthService,
private readonly cacheService: CacheService,
) {

View File

@@ -10,51 +10,61 @@ import { getRedisKey, md5 } from '~/utils'
import { CacheService } from '../cache/cache.service'
const getMachineId = () => {
const id = machineIdSync()
if (isDev && cluster.isPrimary) {
console.log(id)
}
return id
}
const secret =
SECURITY.jwtSecret ||
Buffer.from(getMachineId()).toString('base64').slice(0, 15) ||
'asjhczxiucipoiopiqm2376'
if (isDev && cluster.isPrimary) {
console.log(secret)
}
if (!CLUSTER.enable || cluster.isPrimary) {
console.log(
'JWT Secret start with :',
secret.slice(0, 5) + '*'.repeat(secret.length - 5),
)
}
@Injectable()
export class JWTService {
private secret: string
constructor(private readonly cacheService: CacheService) {
this.init()
}
constructor(private readonly cacheService: CacheService) {}
private init() {
const getMachineId = () => {
const id = machineIdSync()
if (isDev && cluster.isPrimary) {
console.log(id)
}
return id
}
this.secret =
SECURITY.jwtSecret ||
Buffer.from(getMachineId()).toString('base64').slice(0, 15) ||
'asjhczxiucipoiopiqm2376'
if (isDev && cluster.isPrimary) {
console.log(this.secret)
}
if (!CLUSTER.enable || cluster.isPrimary) {
console.log(
'JWT Secret start with :',
this.secret.slice(0, 5) + '*'.repeat(this.secret.length - 5),
)
}
}
async verify(token: string) {
try {
verify(token, this.secret)
verify(token, secret)
return await this.isTokenInRedis(token)
} catch {
} catch (er) {
console.debug(er, token)
return false
}
}
async isTokenInRedis(id: string) {
async isTokenInRedis(token: string) {
const redis = this.cacheService.getClient()
const key = getRedisKey(RedisKeys.JWTStore)
const token = await redis.sismember(key, md5(id))
return !!token
const has = await redis.sismember(key, md5(token))
return !!has
}
async invokeToken(token: string) {
const redis = this.cacheService.getClient()
const key = getRedisKey(RedisKeys.JWTStore)
await redis.srem(key, md5(token))
}
async invokeAll() {
const redis = this.cacheService.getClient()
const key = getRedisKey(RedisKeys.JWTStore)
await redis.del(key)
}
async storeTokenInRedis(token: string) {
@@ -62,8 +72,8 @@ export class JWTService {
await redis.sadd(getRedisKey(RedisKeys.JWTStore), md5(token))
}
sign(id: string, options: SignOptions = { expiresIn: '7d' }): string {
const token = sign({ id }, this.secret, options)
sign(id: string, options: SignOptions = { expiresIn: '7d' }) {
const token = sign({ id }, secret, options)
this.storeTokenInRedis(token)
return token
}

View File

@@ -18,6 +18,7 @@ import { EmailService } from './helper.email.service'
import { EventManagerService } from './helper.event.service'
import { HttpService } from './helper.http.service'
import { ImageService } from './helper.image.service'
import { JWTService } from './helper.jwt.service'
import { TextMacroService } from './helper.macro.service'
import { TaskQueueService } from './helper.tq.service'
import { UploadService } from './helper.upload.service'
@@ -30,6 +31,7 @@ const providers: Provider<any>[] = [
EmailService,
EventManagerService,
HttpService,
JWTService,
ImageService,
TaskQueueService,
TextMacroService,

View File

@@ -1,11 +1,8 @@
import { JwtModule } from '@nestjs/jwt'
import { PassportModule } from '@nestjs/passport'
import { Test } from '@nestjs/testing'
import { getModelToken } from '~/transformers/model.transformer'
import { SECURITY } from '~/app.config'
import { AuthService } from '~/modules/auth/auth.service'
import { JwtStrategy } from '~/modules/auth/jwt.strategy'
import { UserModel } from '~/modules/user/user.model'
import { getModelToken } from '~/transformers/model.transformer'
describe('Test AuthService', () => {
let service: AuthService
@@ -18,24 +15,8 @@ describe('Test AuthService', () => {
authCode: 'authCode',
}
beforeAll(async () => {
const __secret: any = SECURITY.jwtSecret || 'asjhczxiucipoiopiqm2376'
const jwtModule = JwtModule.registerAsync({
useFactory() {
return {
secret: __secret,
signOptions: {
expiresIn: SECURITY.jwtExpire,
algorithm: 'HS256',
},
}
},
})
const moduleRef = Test.createTestingModule({
imports: [jwtModule, PassportModule],
providers: [
JwtStrategy,
AuthService,
{
provide: getModelToken(UserModel.name),
@@ -55,17 +36,8 @@ describe('Test AuthService', () => {
service = app.get(AuthService)
})
it('should sign token', async () => {
const _token = await service.signToken('1')
it('should sign token', () => {
const _token = service.jwtServicePublic.sign('1')
expect(_token).toBeDefined()
})
it('should verifyied', async () => {
const user = await service.verifyPayload({
_id: '1',
authCode: 'authCode',
})
expect(user).toBeDefined()
expect(user).toEqual(mockUser)
})
})