UNPKG

tomjs

Version:
205 lines (178 loc) 7.97 kB
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;