tomjs
Version:
tomjs framework
205 lines (178 loc) • 7.97 kB
JavaScript
const require2 = require('tomjs/handlers/require2');
const BaseApiError = require2('tomjs/error/base_api_error');
const BaseUser = require2('tomjs/controllers/base_user');
const { isObject } = require2('tomjs/handlers/tools');
const Password = require2('tomjs/password');
const auth_cfg = require2('tomjs/configs')().auth;
//const log4js = require2('tomjs/handlers/log4js');
// let authLog = console;
// if (typeof(auth_cfg.log4js_category) && (auth_cfg.log4js_category.length > 0)) {
// authLog = log4js.getLogger(auth_cfg.log4js_category);
// }
//提供事件:
//login_ok 事件 参数: ctx(上下文),user(用户信息),token(生成Toekn)
//login_error 事件 参数: ctx(上下文),in_where(生成Toekn)
//login_by_id 事件 参数: ctx(上下文),user(用户信息),token(生成Toekn)
//login_by_id_error 事件 参数: ctx(上下文),user id
//logout 事件 参数: ctx(上下文),user id
class BaseLogin extends BaseUser {
field_name() {
return 'name';
}
field_password() {
return 'password';
}
post_password() {
return 'password';
}
/**
* 读取并检查允许用于登录的用户字段。
*
* 字段列表仍然由项目配置或 login 方法参数动态决定,以保留框架对用户名、邮箱、手机号及嵌套字段的支持。
* 这里只拒绝 MongoDB 操作符、空路径和可能修改对象原型的字段,防止错误配置重新形成查询注入入口。
*
* @param {String} name_fields 逗号分隔的登录字段。
* @returns {Array|Boolean} 合法字段数组,配置不合法时返回 false。
*/
getLoginUsernameFields(name_fields) {
if (typeof name_fields != 'string') {
return false;
}
const fields = name_fields.split(',').map(field => field.trim());
const reserved_fields = ['__proto__', 'prototype', 'constructor'];
if (fields.length == 0 || fields.some(field => {
const field_parts = field.split('.');
return field_parts.some(field_part => {
return !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field_part)
|| reserved_fields.indexOf(field_part) >= 0;
});
})) {
return false;
}
return Array.from(new Set(fields));
}
/**
* 根据服务端允许的字段构造 HTTP 登录查询。
*
* 客户端只负责提交一个普通字符串登录名,不能直接决定 MongoDB 查询结构。
* 单字段返回普通等值查询,多字段返回由框架生成的 $or,因此动态登录能力和查询安全可以同时保留。
*
* @param {Object} request_body HTTP 登录请求数据。
* @param {String} name_fields 逗号分隔的登录字段。
* @returns {Object|Boolean} 可交给 loginByWhere 的查询,输入不合法时返回 false。
*/
buildLoginWhere(request_body, name_fields) {
if (!isObject(request_body)) {
return false;
}
const fields = this.getLoginUsernameFields(name_fields);
const field_name = this.field_name();
let value = request_body[field_name];
if (fields === false || typeof value != 'string') {
return false;
}
if (auth_cfg.register_name_email_mobile_lower_case) {
value = value.toLowerCase();
}
if (fields.length == 1) {
return { [fields[0]]: value };
}
return {
$or: fields.map(field => ({ [field]: value })),
};
}
async login(ctx, name_fields) {
if (!name_fields) {
name_fields = auth_cfg.login_username_fields;
}
const request_body = ctx.request.body;
const login_where = this.buildLoginWhere(request_body, name_fields);
let user = false;
if (login_where !== false) {
login_where[this.post_password()] = request_body[this.post_password()];
if (Object.prototype.hasOwnProperty.call(request_body, auth_cfg.expiresin_long)) {
login_where[auth_cfg.expiresin_long] = request_body[auth_cfg.expiresin_long];
}
user = await this.loginByWhere(ctx, login_where, true);
}
else {
// 非法登录名或错误字段配置不访问数据库,但仍发送失败事件供项目统一记录安全日志。
this.emitter.emit('login_error', { ctx, where: {} });
}
if (user === false) {
throw new BaseApiError(BaseApiError.LOGIN_ERROR, { message: ctx.state.__('name or password error') });
}
let tokenInfo = this.decodeToken(ctx.state[auth_cfg.jwt_tokenkey]);
ctx.body = {
id: user.id,
name: user.name,
userid: user.id,
token: ctx.state[auth_cfg.jwt_tokenkey],
exp: tokenInfo.exp,
exp_is_long: tokenInfo.exp_is_long,
}
if (user[auth_cfg.jwt_key_status] !== undefined) {
ctx.body[auth_cfg.jwt_key_status] = user[auth_cfg.jwt_key_status];
}
return user;
}
async loginByWhere(ctx, in_where, check_password = true) {
if (!isObject(in_where)) { return false; }
let where = Object.assign({}, in_where);
let expiresin_long = false;
if (where[auth_cfg.expiresin_long] == 1) {
expiresin_long = true;
}
if (Object.prototype.hasOwnProperty.call(where, auth_cfg.expiresin_long)) {
delete where[auth_cfg.expiresin_long];
}
let passwd = '';
if (check_password) {
if (Object.prototype.hasOwnProperty.call(where, this.post_password())) {
passwd = where[this.post_password()];
delete where[this.post_password()];
}
}
let user = undefined;
try { user = await this.users.findOne(where) } catch (e) {
throw new BaseApiError(BaseApiError.DB_ERROR, { message: e.message });
}
if (user === null || user === undefined || (check_password && (!await Password.compare(passwd, user[this.field_password()])))) {
// 失败事件只传递实际执行的查询,不暴露明文密码和长时登录控制字段。
this.emitter.emit('login_error', { ctx, where });
return false;
}
let token = await this.BuildToken(ctx, user, expiresin_long);
this.emitter.emit('login_ok', { ctx, user, token, expiresin_long });
return user;
}
async loginByID(ctx, id, expiresin_long) {
let user = undefined
try { user = await this.users.findById(id) } catch (e) {
throw new BaseApiError(BaseApiError.DB_ERROR, { message: e.message });
}
if (user) {
let token = await this.BuildToken(ctx, user, expiresin_long);
this.emitter.emit('login_by_id', { ctx, user, token });
return user;
} else {
this.emitter.emit('login_by_id_error', { ctx, id });
return false;
}
}
async logout(ctx) {
ctx.cookies.set(auth_cfg.jwt_cookie, '', { path: '/', maxAge: 0, }); //清除cookie
let inc = {};
inc[auth_cfg.jwt_key_token_version] = 1;
if (ctx.state[auth_cfg.jwt_key] && ctx.state[auth_cfg.jwt_key][auth_cfg.jwt_key_id]) {
try { await this.users.updateOne({ _id: ctx.state[auth_cfg.jwt_key][auth_cfg.jwt_key_id] }, { $inc: inc }); } catch (e) {
throw new BaseApiError(BaseApiError.DB_ERROR, { message: e.message });
}
this.emitter.emit('logout', { ctx, id: ctx.state[auth_cfg.jwt_key][auth_cfg.jwt_key_id] });
ctx.body = {};
} else {
throw new BaseApiError(BaseApiError.JWT_ERROR);
}
}
}
module.exports = BaseLogin;