wisetc / practice

Practice conclusion
5 stars 0 forks source link

fetch-client jwt 方案调研 #34

Open wisetc opened 4 years ago

wisetc commented 4 years ago

系统的各种授权方案中,userId 是贯穿始终的。

因此 token 中应该包含 userId 信息。

encode(userId) -> token;

decode(token) -> userId | null;

分支

dev-auth

技术

接口

使用中间件

中间件的名称:auth

实现

使用库 jsonwebtoken

https://www.npmjs.com/package/jsonwebtoken

var jwt = require('jsonwebtoken');
var token = jwt.sign({ foo: 'bar' }, 'shhhhh');

jwt.sign(
  {
    data: 'foobar',
  },
  'secret',
  { expiresIn: 60 * 60 }
);
// verify a token symmetric - synchronous
var decoded = jwt.verify(token, 'shhhhh');
console.log(decoded.foo); // bar

// invalid token - synchronous
try {
  var decoded = jwt.verify(token, 'wrong-secret');
} catch (err) {
  // err
}

代码

如下评论所示。

wisetc commented 4 years ago
// authentication.ts

import { ParameterizedContext } from 'koa';
import jwt from 'jsonwebtoken';

import { ErrorResponse } from '../shared/response';
import { HTTP_401_UNAUTHORIZED } from '../shared/status';

interface AuthenticateOptions {
    secret: string;
}

const authenticateOpts = {
    secret: 'shhhhh',
};

export const isRawUserId = (userId: string | undefined): boolean => {
    if (typeof userId === 'undefined') return false;
    return userId.length === 32;
};

export const getToken = (bearerStr: string | undefined): string | undefined => {
    const pattern = /^bearer (.+$)/;
    if (!pattern.exec(bearerStr)) return undefined;
    return bearerStr.replace(pattern, (v0, v1) => v1);
};

export const authenticate = (
    opts: AuthenticateOptions = authenticateOpts
) => async (ctx: ParameterizedContext, next: Function) => {
    const rawStr = ctx.headers.authentication;

    if (isRawUserId(rawStr)) {
        ctx.state.user = { userId: rawStr };
        await next();
        return;
    }

    const token = getToken(rawStr);
    if (typeof token === 'undefined') {
        ctx.body = new ErrorResponse('密钥无效。');
        ctx.status = HTTP_401_UNAUTHORIZED;
        return;
    }

    try {
        const decoded = jwt.verify(token, opts.secret);
        ctx.state.user = decoded;
        await next();
    } catch (err) {
        ctx.body = new ErrorResponse('密钥无效。');
        ctx.status = HTTP_401_UNAUTHORIZED;
        return;
    }
};
wisetc commented 4 years ago
// authentication.test.ts
import { createMockContext } from '@shopify/jest-koa-mocks';
import jwt from 'jsonwebtoken';

import { ErrorResponse } from '../shared/response';
import * as HTTP_STATUS from '../shared/status';
import { authenticate, getToken, isRawUserId } from './authentication';

describe('isRawUserId', () => {
    test('undefined', () => {
        expect(isRawUserId(undefined)).toBe(false);
    });

    test('abcd', () => {
        expect(isRawUserId('abcd')).toBe(false);
    });

    test('b774da99237047528293cad9fb19b6e2', () => {
        expect(isRawUserId('b774da99237047528293cad9fb19b6e2')).toBe(true);
    });
});

describe('getToken', () => {
    test('undefined', () => {
        expect(getToken(undefined)).toBe(undefined);
    });

    test('abc', () => {
        expect(getToken('abc')).toBe(undefined);
    });

    test('bearerabc', () => {
        expect(getToken('bearerabc')).toBe(undefined);
    });

    test('bearer abc', () => {
        expect(getToken('bearer abc')).toBe('abc');
    });
});

describe('authenticate', () => {
    interface RequestBody {}
    test('bearer token invalid', () => {
        const ctx = createMockContext<null, RequestBody>({
            method: 'GET',
            headers: {
                Authentication: 'bearer abc',
            },
        });
        const noop = jest.fn();

        authenticate()(ctx, noop);
        expect(ctx.status).toBe(HTTP_STATUS.HTTP_401_UNAUTHORIZED);
        expect(ctx.body).toEqual(new ErrorResponse('密钥无效。'));
        expect(noop).toBeCalledTimes(0);
    });

    test('bearer token valid', () => {
        const userId = 'abc';
        const secret = 'secret';
        const token = jwt.sign({ userId }, secret);
        const ctx = createMockContext<null, RequestBody>({
            method: 'GET',
            headers: {
                Authentication: 'bearer ' + token,
            },
        });
        const noop = jest.fn();

        authenticate({ secret })(ctx, noop);
        expect(noop).toBeCalledTimes(1);
        expect(ctx.state.user).toBeTruthy();
        expect(ctx.state.user.userId).toBe(userId);
    });

    test('raw userId', () => {
        const userId = 'b774da99237047528293cad9fb19b6e2';
        const secret = 'secret';
        const ctx = createMockContext<null, RequestBody>({
            method: 'GET',
            headers: {
                Authentication: userId,
            },
        });
        const noop = jest.fn();

        authenticate({ secret })(ctx, noop);
        expect(noop).toBeCalledTimes(1);
        expect(ctx.state.user).toBeTruthy();
        expect(ctx.state.user.userId).toBe(userId);
    });
});