🙆
Strategy & Factoryパターンで、複数authメソッドを簡潔に
サーバーに必要な要件例
- clientからのアクセスに対して、jwt tokenを確認。access tokenからuser idを取ってreqに詰め込む
- 外部サーバーからのwebhookによるアクセスは、指定されたドメインからのreqの場合チェックなしで通す
- ローカル開発中は、もともと指定していたuser idを取ってreqに詰め込む
これらの条件分岐を全てmiddlewareの内部のif文とかで捌くと、複雑になる。
strategy & factoryパターンを導入するメリット:
- 責務を分割して見やすくできる
- 新しいauthのハンドリング要件が追加されても柔軟に対応できる
strategy & factoryの説明
strategy: interfaceを実装して、それを呼び出す側が具体の実装から独立する
factory: interfaceを実装する複数のインスタンスからどれを返すかを判定する
strategy & factory無しの場合
(tsrynge使ってdependency injectionしてる)
@injectable()
class Auth {
constructor(
@inject("Config") private config Config,
@inject("IVerifyTokenUC") private verifyTokenUC IVerifyTokenUC,
) {}
public execute = async (req Request, res Response): Promise<void> => {
if (this.config.env === Env.DEV) {
req.userId = this.config.userIdForDev;
return;
}
const { authorization } = req.headers;
if (!authorization) {
throw new Error("No token provided");
}
if (req.path.startWith("/webhooks") && authorization === this.config.accessToken) {
return;
}
const token = authorization.split(" ")[1];
const userId = await this.verifyTokenUC.execute(token); // might throw an error
req.userId = userId
}
}
これでも良いっちゃ良い。
ただ、これだと、次の両方の責務をexecute
メソッドが兼任してしまう。
- どのauthロジックを使用するかの判定
- 全authロジックの実行
これをstrategy & factoryパターンを使うと、綺麗に切り分けられる
strategy & factoryありの場合
export interface IAuthStrategy {
authenticate(req: Request): Promise<void>;
}
@injectable()
class AuthStrategyFactory {
constructor(
@inject("JwtAuthStrategy") private readonly jwtAuthStrategy: JwtAuthStrategy,
@inject("LocalAuthStrategy") private readonly localAuthStrategy: LocalAuthStrategy,
@inject("ApiKeyAuthStrategy") private readonly apiKeyAuthStrategy: ApiKeyAuthStrategy,
@inject("Config") private readonly config: Config,
) { }
public getStrategy = (req: Request): IAuthStrategy => {
if (this.config.env === Env.DEV) {
return this.localAuthStrategy;
}
if (req.path.startsWith("/webhooks")) {
return this.apiKeyAuthStrategy;
}
return this.jwtAuthStrategy;
}
}
@injectable()
class ApiKeyAuthStrategy implements IAuthStrategy {
constructor(
@inject("Config") private readonly config: Config,
) { }
authenticate = async (req: Request): Promise<void> => {
const { authorization } = req.headers;
if (!authorization) {
throw new Error("No token provided");
}
if (authorization !== this.config.revenueCatApiKey) {
throw new Error("Invalid token");
}
}
}
@injectable()
class JwtAuthStrategy implements IAuthStrategy {
constructor(
@inject("IVerifyTokenUC") private readonly verifyTokenUC: IVerifyTokenUC,
) { }
authenticate = async (req: Request): Promise<void> => {
const { authorization } = req.headers;
if (!authorization) {
throw new Error("No token provided");
}
const token = authorization.split(" ")[1];
const cmd: VerifyTokenCmd = {
token,
};
const res = await this.verifyTokenUC.execute(cmd);
req.userId = res.userId;
}
}
@injectable()
class LocalAuthStrategy implements IAuthStrategy {
constructor(
@inject("Config") private readonly config: Config,
) { }
authenticate = async (req: Request): Promise<void> => {
req.userId = this.config.devUserId;
}
}
長くなるし冗長やけど、それぞれのクラスが超具体的な単一の責務を持つ。
SOLID則にも沿う。
学習コストや実装コストこそかかれど、安全で分かりやすいコードになる。
俺はこっちの方が良いと思う。
Discussion