當然不是說JWT token 不如 redis token實作方案好, 具體看使用的場景,這裡我們並不討論二者孰優孰劣,只是提供一種實現方案,讓大家知道如何實現。
計數器的應用基本上和排行榜系統一樣,都是多數網站的普遍需求,如視頻網站的播放計數,電器網站的瀏覽數等等,但這些數量一般比較龐大,如果存到關係型資料庫,對MySQL或其他關係型資料庫的挑戰還是很大的,而Redis基本上可以說是天然支援計數器應用。
與SQL型資料不同,redis沒有提供新建資料庫的操作,因為它自帶了16(0 -15)個資料庫(預設使用0庫)。在同一個庫中,key是唯一存在的、不允許重複的,它就像一把“密鑰”,只能打開一把“鎖”。鍵值儲存的本質就是使用key來標識value,當想要檢索value時,必須使用與value對應的key進行查找.
##函式庫 | 版本 |
Nest.js | V8.1.2 |
專案是基於Nest.js 8.x
版本,與Nest.js 9.x
版本使用有所不同, 後面的文章專門整理了兩個版本使用不同點的說明, 以及如何從V8
升級到V9
, 這裡就不過多討論。
首先,我們在Nest.js專案中連接Redis, 連接Redis需要的參數:
REDIS_HOST:Redis 域名
REDIS_PORT:Redis 端口号
REDIS_DB: Redis 数据库
REDIS_PASSPORT:Redis 设置的密码
將參數寫入.env
與.env. prod
設定檔中:
使用Nest官方推薦的方法,只需要簡單的3個步驟:
1、引入依賴檔
npm install cache-manager --save
npm install cache-manager-redis-store --save
npm install @types/cache-manager -D
Nest
為各種快取存儲提供統一的API,內建的是記憶體中的資料存儲,但是也可使用cache-manager
來使用其他方案,例如使用Redis
來快取。
為了啟用緩存,導入ConfigModule
, 並呼叫register()
或registerAsync()
傳入回應的設定參數。
2、建立module檔案src/db/redis-cache.module.ts
, 實作如下:
import { ConfigModule, ConfigService } from '@nestjs/config';
import { RedisCacheService } from './redis-cache.service';
import { CacheModule, Module, Global } from '@nestjs/common';
import * as redisStore from 'cache-manager-redis-store';
@Module({
imports: [
CacheModule.registerAsync({
isGlobal: true,
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
return {
store: redisStore,
host: configService.get('REDIS_HOST'),
port: configService.get('REDIS_PORT'),
db: 0, //目标库,
auth_pass: configService.get('REDIS_PASSPORT') // 密码,没有可以不写
};
},
}),
],
providers: [RedisCacheService],
exports: [RedisCacheService],
})
export class RedisCacheModule {}
-
CacheModule
的registerAsync
方法採用Redis Store 設定進行通訊
-
store
屬性值redisStore
,表示'cache-manager-redis-store' 函式庫
-
isGlobal
屬性設定為true
來將其宣告為全域模組,當我們將RedisCacheModule
在AppModule
中匯入時, 其他模組就可以直接使用,不需要再次導入
- 由於Redis 資訊寫在設定檔中,所以採用
registerAsync()
方法來處理異步數據,如果是靜態數據, 可以使用register
3、新建redis-cache.service.ts
檔, 在service實作快取的讀寫
import { Injectable, Inject, CACHE_MANAGER } from '@nestjs/common';
import { Cache } from 'cache-manager';
@Injectable()
export class RedisCacheService {
constructor(
@Inject(CACHE_MANAGER)
private cacheManager: Cache,
) {}
cacheSet(key: string, value: string, ttl: number) {
this.cacheManager.set(key, value, { ttl }, (err) => {
if (err) throw err;
});
}
async cacheGet(key: string): Promise<any> {
return this.cacheManager.get(key);
}
}
接下來,在app.module.ts
中導入RedisCacheModule
即可。
調整 token 簽發及驗證流程
我們借助redis來實現token過期處理、token自動續期、以及用戶唯一登入。
- 過期處理:把使用者資訊及token放進redis,並設定過期時間
- token自動續期:token的過期時間為30分鐘,如果在這30分鐘內沒有操作,則重新登錄,如果30分鐘內有操作,就給token自動續一個新的時間,防止使用時斷線。
- 戶唯一登入:相同的帳號,不同電腦登錄,先登入的用戶會被後登入的擠下線
##token 過期處理在登錄時,將jwt產生的token,存入redis,並設定有效期限為30分鐘。存入redis的key由使用者資訊組成, value是token值。 // auth.service.ts
async login(user: Partial<User>) {
const token = this.createToken({
id: user.id,
username: user.username,
role: user.role,
});
+ await this.redisCacheService.cacheSet(
+ `${user.id}&${user.username}&${user.role}`,
+ token,
+ 1800,
+ );
return { token };
}
在驗證token時, 從redis中取token,如果取不到token,可能是token已過期。 // jwt.strategy.ts
+ import { RedisCacheService } from './../core/db/redis-cache.service';
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
private readonly authService: AuthService,
private readonly configService: ConfigService,
+ private readonly redisCacheService: RedisCacheService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get('SECRET'),
+ passReqToCallback: true,
} as StrategyOptions);
}
async validate(req, user: User) {
+ const token = ExtractJwt.fromAuthHeaderAsBearerToken()(req);
+ const cacheToken = await this.redisCacheService.cacheGet(
+ `${user.id}&${user.username}&${user.role}`,
+ );
+ if (!cacheToken) {
+ throw new UnauthorizedException('token 已过期');
+ }
const existUser = await this.authService.getUser(user);
if (!existUser) {
throw new UnauthorizedException('token不正确');
}
return existUser;
}
}
用戶唯一登入
當使用者登入時,每次簽發的新的token,會覆蓋先前的token, 判斷redis中的token與請求傳入的token是否相同, 不相同時, 可能是其他地方已登錄, 提示token錯誤。 // jwt.strategy.ts
async validate(req, user: User) {
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(req);
const cacheToken = await this.redisCacheService.cacheGet(
`${user.id}&${user.username}&${user.role}`,
);
if (!cacheToken) {
throw new UnauthorizedException('token 已过期');
}
+ if (token != cacheToken) {
+ throw new UnauthorizedException('token不正确');
+ }
const existUser = await this.authService.getUser(user);
if (!existUser) {
throw new UnauthorizedException('token不正确');
}
return existUser;
}
token自動續期
實作方案有多種,可以後台jwt產生access_token(jwt有效期30分鐘)和
refresh_token,
refresh_token有效期比
access_token有效期長,客戶端快取此兩種token, 當
access_token過期時, 客戶端再攜帶
refresh_token取得新的
access_token。這種方案需要介面呼叫的開發人員配合。
我這裡主要介紹一下,純後端實作的token自動續期實作流程:
①:jwt產生token時,有效期限設定為用不過期- ②:redis 快取token時設定有效期限30分鐘
- ③:使用者攜帶token請求時, 若key存在,且value相同,則重新設定有效期限為30分鐘
-
設定jwt產生的token, 用不過期, 這部分程式碼是在auth.module.ts檔中, 不了解的可以看文章
Nest.js 實戰系列第二篇-實作註冊、掃碼登陸、jwt認證
// auth.module.ts
const jwtModule = JwtModule.registerAsync({
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
return {
secret: configService.get('SECRET', 'test123456'),
- signOptions: { expiresIn: '4h' }, // 取消有效期设置
};
},
});
然後再token認證通過後,重新設定過期時間, 因為使用的cache-manager沒有透過直接更新有效期限方法,透過重新設定來實現:
// jwt.strategy.ts
async validate(req, user: User) {
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(req);
const cacheToken = await this.redisCacheService.cacheGet(
`${user.id}&${user.username}&${user.role}`,
);
if (!cacheToken) {
throw new UnauthorizedException('token 已过期');
}
if (token != cacheToken) {
throw new UnauthorizedException('token不正确');
}
const existUser = await this.authService.getUser(user);
if (!existUser) {
throw new UnauthorizedException('token不正确');
}
+ this.redisCacheService.cacheSet(
+ `${user.id}&${user.username}&${user.role}`,
+ token,
+ 1800,
+ );
return existUser;
}
到此,在Nest中實現token過期處理、token自動續期、以及用戶唯一登入都完成了, 登出登入時移除token比較簡單就不在這裡一一上代碼了。 在Nest中除了使用官方推薦的這種方式外, 還可以使用nestjs-redis來實現,如果你存token時, 希望存
hash結構,使用
cache-manager-redis-store時,會發現沒有提供
hash值存取放方法(需要花點心思去發現)。
注意:如果使用nest-redis
來實現redis緩存, 在Nest.js 8 版本下會報錯, 小夥伴可以使用@chenjm/nestjs-redis
來代替, 或參考issue上的解決方案:Nest 8 redis bug。
總結
原始碼位址:https://github.com/koala-coding/nest-blog
#更多程式相關知識,請訪問:程式設計教學! !