Open wisetc opened 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;
}
};
// 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);
});
});
系统的各种授权方案中,userId 是贯穿始终的。
因此 token 中应该包含 userId 信息。
encode(userId) -> token;
decode(token) -> userId | null;
分支
dev-auth
技术
接口
使用中间件
中间件的名称:auth
实现
使用库 jsonwebtoken
https://www.npmjs.com/package/jsonwebtoken
代码
如下评论所示。