• Authentication (认证): 你是谁?
  • Authorization (授权): 你能干什么?

比如你去酒店开房,你需要拿身份证办理Check in,这是一个认证过程,身份证和密码的功效是一样的证明了你是谁,前台给你的房卡表示授权你开302房间,你不能用身份证去开别人房间吧,这就是认证和授权的区别。

整个授权过程有2个重要的概念:

  • role
  • scope (permission) 两个词汇同一概念,我们暂时用scope来表示

从零开始

我们写了两个api, 分别都需要认证,

let users = [
  { id: 1, name: 'foo' },
  { id: 2, name: 'bar' }
];

/**
 * List all users
 * auth: required
 */
router.get('/users', auth(), async ctx => {
  ctx.body = users;
});

/**
 * Update user
 * auth: required
 */
router.patch('/users/:id', auth(), async ctx => {
  let target = null;
  try {
    target = _.find(users, x => x.id === Number.parseInt(ctx.params.id));
    target.name = target.name.toUpperCase();
    ctx.body = target;
  } catch (error) {
    ctx.throw(404, error);
  }
});

这里mock了一个auth的middleware,完成用户foo的认证,

let auth = () => {
  return async (ctx, next) => {
    ctx.user = { name: 'foo' };
    await next();
  }
};

这个时候我们来了一个需求,普通用户可以查看所有用户,但只有管理员可以修改用户,给user加上role [user | admin], 然后在api中判断一下就搞定了,

router.patch('/users/:id', auth(), async ctx => {
  if (ctx.user.role !== 'admin') {
    ctx.throw(404);
  }
}

这个时候我们就把角色(role)做了硬编码,假设我们还有一个更新产品的功能,我也一样会判断角色是否是admin,但问题是用户可能需要自己管理角色,或者说他说管理员A负责用户管理,管理员B负责产品管理,这个时候你就懵b了吧,所以我们新建一层抽象,这个时候才引出了scope,我们基于scope做授权管理,而role和scope关系开放给用户去设定。想明白底层逻辑,这个时候你去看RBAC(Role based Access Control)就会比较好的理解。

ok, 我们来实现一下,首先建立user, role和scope关系,

let users = [
  { id: 1, name: 'foo', roles: ['user'] },
  { id: 2, name: 'bar', roles: ['user', 'admin'] }
];

let roles = [
  { name: 'user', scopes: ['user:read'] },
  { name: 'admin', scopes: ['user:read', 'user:write'] },
];

然后在auth中根据user的roles得到对应的scopes合集,

let auth = () => {
  return async (ctx, next) => {
    ctx.scopes = [];
    try {
      // ?name=foo
      // ?name=bar
      ctx.user = _.find(users, x => x.name === ctx.query.name);
      ctx.scopes = _
        .chain(ctx.user.roles)
        .map(roleName => _.find(roles, role => role.name === roleName).scopes)
        .flatten()
        .uniq()
        .value();
      debug(ctx.scopes);
    } catch (error) {
      ctx.throw(401);
    }
    await next();
  }
};

最后就是应用,

/**
 * Update user
 * auth: required
 * scope: user:write
 */
router.patch('/users/:id', auth(), async ctx => {
  if (!ctx.scopes.includes('user:write')) {
    ctx.throw(404);
  }

  let target = null;
  try {
    target = _.find(users, x => x.id === Number.parseInt(ctx.params.id));
    target.name = target.name.toUpperCase();
    ctx.body = target;
  } catch (error) {
    ctx.throw(404, error);
  }
});

测试一下,

➜  curl -XPATCH 'http://localhost:3000/users/1?name=foo' 
Not Found%                                                              
➜  curl -XPATCH 'http://localhost:3000/users/1?name=bar'
{
  "id": 1,
  "name": "FOO",
  "roles": [
    "user"
  ]
}

接着来考虑一个问题,普通用户随便不能随便改其用户信息,但他应该可以改自己的用户信息吧,那么普通用户是否需要user:write的scope呢?

这里假设修改本人信息的api为PATCH /user或者PATCH /users/me,那么在这个route需要scope吗?实际情况是,每个验证过身份的用户都可以修改自己的用户信息,所以就根本不需要scope,

/**
 * Update the authenticated user
 * auth: required
 * scope: /
 */
router.patch('/user', auth(), async ctx => {
  ctx.body = { name: ctx.user.name.toUpperCase() };
});

重构

我们通过auth这个middleware同时整合Authentication和Authorization两个过程。

改造后的调用更加简洁,

/**
 * Update user
 * auth: required
 * scope: user:write
 */
router.patch('/users/:id', auth('user:write', 'user'), async ctx => {
  try {
    let target = _.clone(_.find(users, x => x.id === Number.parseInt(ctx.params.id)));
    target.name = target.name.toUpperCase();
    ctx.body = target;
  } catch (error) {
    ctx.throw(404, error);
  }
});

对应的auth

let auth = (...scopes) => {
  return async (ctx, next) => {
    ctx.scopes = [];
    try {
      // ?name=foo
      // ?name=bar
      ctx.user = _.find(users, x => x.name === ctx.query.name);
      if (!ctx.user) { ctx.throw(401); }

      ctx.scopes = _
        .chain(ctx.user.roles)
        .map(roleName => _.find(roles, role => role.name === roleName).scopes)
        .flatten()
        .uniq()
        .value();
      // debug(ctx.scopes);

      if (scopes.length > 0) {
        // 检查授权
        if (_.intersection(scopes, ctx.scopes).length > 0) {
          await next();
        }
      } else {
        await next();
      }

    } catch (error) {
      ctx.throw(401);
    }
  }
};

补充话题: Decorator?

先来认识一下babel 6中的decorator, 也可以看阮一峰写的文章

@testable
class App {
  hi() {
    debug('hello world');
  }
}

function testable(target) {
  target.isTestable = true;
}

debug(App.isTestable);

运行这段代码,需要做点配置,

yarn add babel-cli babel-plugin-transform-decorators-legacy -D

.babelrc

{ "plugins": [ "transform-decorators-legacy" ] }

run
➜ npx babel decorator.js | node -

有了decorator的基础就可以了解一下gwuhaolin/koa-router-decorator: @route decorator for koa-router

@route('/monitor')
export default class MonitorCtrl{

  @route('/alive', HttpMethod.GET)
  async alive(ctx) {
    ctx.body = {
      data: true,
    };
  }
}

好不容易摆脱了Spring,没想到吧。