Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

认证和授权有什么区别? #3

Open
nonocast opened this issue Mar 1, 2020 · 0 comments
Open

认证和授权有什么区别? #3

nonocast opened this issue Mar 1, 2020 · 0 comments

Comments

@nonocast
Copy link
Owner

@nonocast nonocast commented Mar 1, 2020

  • 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,没想到吧。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
1 participant
You can’t perform that action at this time.