前言

双因子认证:双因子认证(2FA)是指结合密码以及实物(信用卡、SMS手机、令牌或指纹等生物标志)两种条件对用户进行认证的方法。--百度百科

跟我一样"老"的网瘾少年想必一定见过买点卡后上面送的密保(类似但不完全一样),还有"将军令",以及网银的网盾,是一种二次验证的机制;它通常是6位的数字,每次使用后(HOTP)或者一定时间后(TOTP)都将会刷新,大大加大了用户的安全性,OTP(One-Time Password)分为HOTP(HMAC-based One-Time Password)和TOTP(Time-based One-Time Password)。

HOTP是基于 HMAC 算法加密的一次性密码,以事件同步机制,把事件次序(counter)及相同的密钥(secret)作为输入,通过 HASH 算法运算出一致的密码。

TOTP是基于时间戳算法的一次性密码,基于客户端的时间和服务器的时间及相同的密钥(secret)作为输入,产生数字进行对比,这就需要客户端的时间和服务器的时间保持相对的一致性。

Odoo12集成双因子认证

为了让odoo12的登录也可以使用双因子认证以提高安全性,我们需要:

、实现OTP验证逻辑
、为ODOO用户界面展示二维码
、为管理员用户提供OTP开关
、在登录界面增加对OTP的验证

我们需要依赖的包:

pip install pyotp
pip install pyqrcode
pip install pypng

实现OTP验证逻辑

首先,我们需要对res.users用户进行重写,添加OTP验证逻辑

# -*- coding: utf-8 -*-
import base64
import pyotp
import pyqrcode
import io from odoo import models, fields, api, _, tools
from odoo.http import request
from odoo.exceptions import AccessDenied import logging _logger = logging.getLogger(__name__) class ResUsers(models.Model):
_inherit = 'res.users' otp_type = fields.Selection(selection=[('time', _('Time based')), ('count', _('Counter based'))], default='time',
string="Type",
help="Type of 2FA, time = new code for each period, counter = new code for each login")
otp_secret = fields.Char(string="Secret", size=16, help='16 character base32 secret',
default=lambda self: pyotp.random_base32())
otp_counter = fields.Integer(string="Counter", default=0)
otp_digits = fields.Integer(string="Digits", default=6, help="Length of the code")
otp_period = fields.Integer(string="Period", default=30, help="Seconds to update code")
otp_qrcode = fields.Binary(compute="_compute_otp_qrcode") otp_uri = fields.Char(compute='_compute_otp_uri', string="URI") # 生成二维码
@api.model
def create_qr_code(self, uri):
buffer = io.BytesIO()
qr = pyqrcode.create(uri)
qr.png(buffer, scale=3)
return base64.b64encode(buffer.getvalue()).decode() # 将二维码的值赋给otp_qrcode变量
@api.depends('otp_uri')
def _compute_otp_qrcode(self):
self.ensure_one()
self.otp_qrcode = self.create_qr_code(self.otp_uri) # 计算otp_uri
@api.depends('otp_type', 'otp_period', 'otp_digits', 'otp_secret', 'company_id', 'otp_counter')
def _compute_otp_uri(self):
self.ensure_one()
if self.otp_type == 'time':
self.otp_uri = pyotp.utils.build_uri(secret=self.otp_secret, name=self.login,
issuer_name=self.company_id.name, period=self.otp_period)
else:
self.otp_uri = pyotp.utils.build_uri(secret=self.otp_secret, name=self.login,
initial_count=self.otp_counter, issuer_name=self.company_id.name,
digits=self.otp_digits) # 验证otp验证码是否正确
@api.model
def check_otp(self, otp_code):
res_user = self.env['res.users'].browse(self.env.uid)
if res_user.otp_type == 'time':
totp = pyotp.TOTP(res_user.otp_secret)
return totp.verify(otp_code)
elif res_user.otp_type == 'count':
hotp = pyotp.HOTP(res_user.otp_secret)
# 允许用户不小心多点20次,但是已经用过的码则无法再次使用
for count in range(res_user.otp_counter, res_user.otp_counter + 20):
if count > 0 and hotp.verify(otp_code, count):
res_user.otp_counter = count + 1
return True
return False # 覆盖原生_check_credentials,增加双因子验证
def _check_credentials(self, password):
super(ResUsers, self)._check_credentials(password)
# 判断是否打开双因子验证并校验验证码
if self.company_id.is_open_2fa and not self.check_otp(request.params.get('tfa_code')):
# pass
raise AccessDenied(_('Validation Code Error!'))

在这里,我们继承了res.users,添加了如下方法:

   _compute_otp_uri: 计算otp_uri
create_qr_code: 通过计算的otp_uri生成二维码
_compute_otp_qrcode: 调用create_qr_code生成二维码,赋值给otp_qrcode变量
check_otp: 用于验证otp验证码是否正确
_check_credentials: 覆盖原生_check_credentials,判断双因子的开关,调用check_otp进行双因子验证

_check_credentials方法中,我们判断了双因子的开关,而双因子开关是以公司为单位的,因此我们还需要对res.company进行继承添加字段:

# -*- coding: utf-8 -*-

from odoo import models, api, fields

class ResCompany(models.Model):
_inherit = "res.company" is_open_2fa = fields.Boolean(string="Open 2FA", default=False)

为ODOO用户界面展示二维码

我们写好逻辑后,需要在用户界面中将二维码以及配置展示出来:

<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- 设置->用户&公司->用户界面-->
<record id="view_users_form" model="ir.ui.view">
<field name="name">res.users.form</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form"/>
<field name="arch" type="xml">
<notebook colspan="4" position="inside">
<page string="2FA">
<group col="4" colspan="4">
<field name="otp_secret"/>
<field name="otp_type"/>
<field name="otp_counter"
attrs="{'invisible':[('otp_type', '==', 'time')], 'readonly': True}"/>
<field name="otp_digits" attrs="{'invisible':[('otp_type', '==', 'time')]}"/>
<field name="otp_period" attrs="{'invisible':[('otp_type', '==', 'count')]}"/>
</group>
<div class="row" style="display: block;text-align: center;">
<field name="otp_qrcode" widget="image" nolabel="1"/>
</div>
<div class="row" style="display: block;text-align: center;">
<label for="otp_uri"/>:
<field name="otp_uri"/>
</div>
</page>
</notebook>
</field>
</record>
<!-- 右上角首选项界面-->
<record id="view_users_form_simple_modif" model="ir.ui.view">
<field name="name">res.users.preferences.form.otp</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form_simple_modif"/>
<field name="arch" type="xml">
<footer position="before">
<div class="o_horizontal_separator">OTP</div>
<div class="row" style="display:block;text-align:center">
<field name="otp_qrcode" widget="image" nolabel="1"/>
</div>
<div class="row" style="display:block;text-align:center">
<field name="otp_uri" nolabel="1"/>
</div>
</footer>
</field>
</record>
</data>
</odoo>

效果如下:

为管理员用户提供OTP开关

我们需要让OTP可以为管理员配置,我们将它加入到res.config.settings的常规设置中:

首先,继承模型添加关联字段,is_open_2fa与company_id里的is_open_2fa关联:

# -*- coding: utf-8 -*-

from odoo import fields, models

class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings' is_open_2fa = fields.Boolean(related='company_id.is_open_2fa', string="Open 2FA", readonly=False)

然后,我们将它展示到常规设置->用户当中

<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.base.setup</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="100"/>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='user_default_rights']" position="inside">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="is_open_2fa"/>
</div>
<div class="o_setting_right_pane">
<label for="is_open_2fa"/>
<div class="text-muted">
The Switch to open 2FA
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>

效果如下:

在登录界面增加对OTP的验证

最后,我们修改登录界面,在页面中增加对otp的验证。

首先,我们需要新增输入页面:

<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="auth_2FA.2fa_auth" name="TFA_auth">
<t t-call="web.login_layout">
<form class="oe_login_form" role="form" t-attf-action="/web/login/2fa_auth{{ '?debug' if debug else '' }}"
method="post" onsubmit="this.action = this.action + location.hash">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/> <div class="form-group field-login">
<label for="tfa_code">Validation Code</label>
<input type="text" placeholder="Please input 2FA digits number" name="tfa_code" t-att-value="tfa_code" id="tfa_code"
t-attf-class="form-control #{'form-control-sm' if form_small else ''}" required="required"
autofocus="autofocus" autocapitalize="off"/>
</div> <p class="alert alert-danger" t-if="error" role="alert">
<t t-esc="error"/>
</p>
<p class="alert alert-success" t-if="message" role="status">
<t t-esc="message"/>
</p> <div t-attf-class="clearfix oe_login_buttons text-center mb-1 {{'pt-2' if form_small else 'pt-3'}}">
<button type="submit" class="btn btn-primary btn-block">Log in</button>
<button type="button" class="btn btn-primary btn-block" onclick="window.location.href='/web/login'">Return</button>
<div class="o_login_auth"/>
</div> <input type="hidden" name="login" t-att-value="login"/>
<input type="hidden" name="password" t-att-value="password"/>
<input type="hidden" name="redirect" t-att-value="redirect"/>
</form>
</t>
</template>
</odoo>

然后,我们需要对/web/login路由进行修改,更改它的跳转逻辑和验证逻辑,在controller中添加main.py:

# -*- coding: utf-8 -*-
import odoo
import logging
from odoo import http, _
from odoo.addons.web.controllers.main import ensure_db, Home
from passlib.context import CryptContext
from odoo.http import request default_crypt_context = CryptContext(
['pbkdf2_sha512', 'md5_crypt'],
deprecated=['md5_crypt'],
) _logger = logging.getLogger(__name__) class WebHome(odoo.addons.web.controllers.main.Home):
# Override by misterling
@http.route('/web/login', type='http', auth="none", sitemap=False)
def web_login(self, redirect=None, **kw):
ensure_db()
request.params['login_success'] = False
if request.httprequest.method == 'GET' and redirect and request.session.uid:
return http.redirect_with_hash(redirect) if not request.uid:
request.uid = odoo.SUPERUSER_ID values = request.params.copy()
try:
values['databases'] = http.db_list()
except odoo.exceptions.AccessDenied:
values['databases'] = None if request.httprequest.method == 'POST':
old_uid = request.uid
try:
request.env.cr.execute(
"SELECT COALESCE(company_id, NULL), COALESCE(password, '') FROM res_users WHERE login=%s",
[request.params['login']]
)
res = request.env.cr.fetchone()
if not res:
raise odoo.exceptions.AccessDenied(_('Wrong login account'))
[company_id, hashed] = res
if company_id and request.env['res.company'].browse(company_id).is_open_2fa:
# 验证密码正确性
valid, replacement = default_crypt_context.verify_and_update(request.params['password'], hashed)
if replacement is not None:
self._set_encrypted_password(self.env.user.id, replacement)
if valid:
response = request.render('auth_2FA.2fa_auth', values)
response.headers['X-Frame-Options'] = 'DENY'
return response
else:
raise odoo.exceptions.AccessDenied()
# 没有打开双因子验证
uid = request.session.authenticate(request.session.db, request.params['login'],
request.params['password'])
request.params['login_success'] = True
return http.redirect_with_hash(self._login_redirect(uid, redirect=redirect))
except odoo.exceptions.AccessDenied as e:
request.uid = old_uid
if e.args == odoo.exceptions.AccessDenied().args:
values['error'] = _("Wrong login/password")
else:
values['error'] = e.args[0]
else:
if 'error' in request.params and request.params.get('error') == 'access':
values['error'] = _('Only employee can access this database. Please contact the administrator.') if 'login' not in values and request.session.get('auth_login'):
values['login'] = request.session.get('auth_login') if not odoo.tools.config['list_db']:
values['disable_database_manager'] = True # otherwise no real way to test debug mode in template as ?debug =>
# values['debug'] = '' but that's also the fallback value when
# missing variables in qweb
if 'debug' in values:
values['debug'] = True response = request.render('web.login', values)
response.headers['X-Frame-Options'] = 'DENY'
return response @http.route('/web/login/2fa_auth', type='http', auth="none")
def web_login_2fa_auth(self, redirect=None, **kw):
ensure_db()
request.params['login_success'] = False
if not request.uid:
request.uid = odoo.SUPERUSER_ID values = request.params.copy()
try:
values['databases'] = http.db_list()
except odoo.exceptions.AccessDenied:
values['databases'] = None
old_uid = request.uid
try:
uid = request.session.authenticate(request.session.db, request.params['login'],
request.params['password'])
request.params['login_success'] = True
return http.redirect_with_hash(self._login_redirect(uid, redirect=redirect))
except odoo.exceptions.AccessDenied as e:
request.uid = old_uid
if e.args == odoo.exceptions.AccessDenied().args:
values['error'] = _("Wrong login/password")
else:
values['error'] = e.args[0]
if not odoo.tools.config['list_db']:
values['disable_database_manager'] = True if 'login' not in values and request.session.get('auth_login'):
values['login'] = request.session.get('auth_login') if 'debug' in values:
values['debug'] = True response = request.render('auth_2FA.2fa_auth', values)
response.headers['X-Frame-Options'] = 'DENY'
return response

我们新增了otp验证路由,将登录逻辑增加到otp验证路由中,然后更改login路由,增加以下逻辑:

request.env.cr.execute(
"SELECT COALESCE(company_id, NULL), COALESCE(password, '') FROM res_users WHERE login=%s",
[request.params['login']]
)
res = request.env.cr.fetchone()
if not res:
raise odoo.exceptions.AccessDenied(_('Wrong login account'))
[company_id, hashed] = res
if company_id and request.env['res.company'].browse(company_id).is_open_2fa:
# 验证密码正确性
valid, replacement = default_crypt_context.verify_and_update(request.params['password'], hashed)
if replacement is not None:
self._set_encrypted_password(self.env.user.id, replacement)
if valid:
response = request.render('auth_2FA.2fa_auth', values)
response.headers['X-Frame-Options'] = 'DENY'
return response
else:
raise odoo.exceptions.AccessDenied()

这段代码的作用是判断otp是否开启并进行密码验证(不登录,不生成session),通过密码验证跳转到otp验证页面。效果如下:

增加语言支持

由于使用的是英文,我们需要增加中文翻译支持。

Tip:需要中文翻译的语句在非字段描述中需要使用_进行包裹,如:
_("Wrong login/password")

打开开发者模式,设置->翻译->导出翻译->选择简体中文,PO文件,要导出的应用为two factor authentication。新建auth_2FA/i18n目录,将导出的文件复制到i18n目录下,修改里面英文内容对应的中文内容后,重启服务器即可生效。

如果没有生效,请在设置->翻译->加载翻译中重新加载。

TODO

对于第一次使用的用户,在用户登录时,在登录界面中展示二维码。实现方案:为res.users增加"是否第一次使用"字段,在第一次登录后展示二维码,并为其赋值为True。具体的逻辑有兴趣的朋友可以先行尝试实现。

APP下载

功能需要配套app使用,请自己手机搜索"谷歌验证器"下载使用,或使用其他可用otp软件替代。

代码地址

从github中下载:

git clone https://github.com/lingjiawen/auth_2FA.git

从odoo官方app中下载:https://apps.odoo.com/apps/modules/12.0/auth_2FA/

声明

原文来自于博客园(https://www.cnblogs.com/ljwTiey/p/11505473.html)

转载请注明文章出处,文章如有任何版权问题,请联系作者删除。

代码仅供学习使用,未经作者允许,禁止使用于商业用途。

合作或问题反馈,联系邮箱:26476395@qq.com

---------------------------------以下为9月12号更新----------------------------------

新增对于第一次使用的用户,在用户登录时,在登录界面中展示二维码。实现方案:为res.users增加"是否第一次使用"字段,在第一次登录后展示二维码,并为其赋值为True,已更新至github和app store,效果如下:

odoo12之应用:一、双因子验证(Two-factor authentication, 2FA)(HOTP,TOTP)附源码的更多相关文章

  1. Remote验证及其改进(附源码)

    Remote验证及其改进(附源码) 表单中的输入项,有些是固定的,不变的验证规则,比如字符长度,必填等.但有些是动态的,比如注册用户名是否存在这样的检查,这个需要访问服务器后台才能解决.这篇文章将会介 ...

  2. 强制禁用gitlab的双因子认证:Two-Factor Authentication

    (一)问题描述: 此博客解决如下问题:禁用gitlab的双因子认证 禁用前,如图(此时,你在gitlab中什么也干不了) (二)思路分析: 百度了很多方法,都不可靠(如不可靠的说明:https://s ...

  3. Java钉钉开发_02_免登授权(身份验证)(附源码)

    源码已上传GitHub: https://github.com/shirayner/DingTalk_Demo 一.本节要点 1.免登授权的流程 (1)签名校验 (2)获取code,并传到后台 (3) ...

  4. JAVA WEB项目中生成验证码及验证实例(附源码及目录结构)

    [我是一个初学者,自己总结和网上搜索资料,代码是自己敲了一遍,亲测有效,现将所有的目录结构和代码贴出来分享给像我一样的初学者] 作用 验证码为全自动区分计算机和人类的图灵测试的缩写,是一种区分用户是计 ...

  5. Asp.net MVC验证哪些事(3)-- Remote验证及其改进(附源码)

    表单中的输入项,有些是固定的,不变的验证规则,比如字符长度,必填等.但有些是动态的,比如注册用户名是否存在这样的检查,这个需要访问服务器后台才能解决.这篇文章将会介绍MVC中如何使用[RemoteAt ...

  6. 利用js编写一个简单的html表单验证,验证通过时提交数据(附源码)

    <!DOCTYPE html> <html lang="en"> <head>     <meta charset="UTF-8 ...

  7. ASP.NET Core & 双因素验证2FA 实战经验分享

    必读 本文源码核心逻辑使用AspNetCore.Totp,为什么不使用AspNetCore.Totp而是使用源码封装后面将会说明. 为了防止不提供原网址的转载,特在这里加上原文链接: https:// ...

  8. (诊断)解决GitHub使用双因子身份认证“Two-Factor Athentication”后无法git push 代码的“fatal: Authentication failed for ...”错误

    在GitHub上采取双因子身份认证后,在git push 的时候将会要求填写用户的用户名和密码,用户名就是用户在GitHub上申请的用户名,但是密码不是普通登录GitHub的密码. 一旦采取双因子身份 ...

  9. 双因子方差分析|adjusted R|强度|SSA|SSE|SST|

    应用统计学 方差分析的基本假设: 组间组平均与总平均的不同是由treatment引发的.单个值与组平均的不同是由组内error引发的. 如果没有处理误差SSA=SSE,所以右尾假设如果F>1则处 ...

随机推荐

  1. 十分钟带你看一遍ES6新特性

    let , const关键字 var 看习惯了java, 看js真的是忍不住想笑,比如说这个var,它太自由了,自由到{}根本限制不住它的生命周期 js的var关键字,无论在何处声明,都会被视为声明在 ...

  2. 【算法】【排序】【插入类】希尔排序 ShellSort

    #include<stdio.h> #include <time.h> #include<stdlib.h> int main(){ ]; //设立随机数 sran ...

  3. java并发编程(五)----(JUC)ReentrantLock

    上一节我们了解了Lock接口的一些简单的说明,知道Lock锁的常用形式,那么这节我们正式开始进入JUC锁(java.util.concurrent包下的锁,简称JUC锁).下面我们来看一下Lock最常 ...

  4. quick-cocos2dx在eclipse下的lua调试

    文中大部分内容来自http://cn.quick-x.com/?p=253,绿色标记部分为修改部分. 配置编译环境的基本步骤: 安装 Visual Studio 2012 安装 Java SDK 安装 ...

  5. android 编译突然出错,错误原因 Could not resolve com.tencent.mm.opensdk:wechat-sdk-android-without-mta:+.

    错误追根是因为微信支付依赖的错误 解决办法: 微信支付依赖版本+号改为微信支付依赖最新版本 在这里https://bintray.com/wechat-sdk-team/maven可以查看到wecha ...

  6. wwww

    public class MainActivity extends AppCompatActivity implements XListView.IXListViewListener{ ; priva ...

  7. 阿里P8架构师浅析——MySQL的高并发优化

    一.数据库结构的设计 1.数据行的长度不要超过8020字节,如果超过这个长度的话在物理页中这条数据会占用两行从而造成存储碎片,降低查询效率. 2.能够用数字类型的字段尽量选择数字类型而不用字符串类型的 ...

  8. Java 调用http接口(基于OkHttp的Http工具类方法示例)

    目录 Java 调用http接口(基于OkHttp的Http工具类方法示例) OkHttp3 MAVEN依赖 Http get操作示例 Http Post操作示例 Http 超时控制 工具类示例 Ja ...

  9. vue 异步加载远程组件(支持编译less语法)

    本代码已组件化,可以直接使用. 说明:本组件可以直接解析.vue文件,为了支持less语法解析,在组件中引入less.js,可在less官网下载. 组件代码 <template> < ...

  10. C语言编程入门之--第五章C语言基本运算和表达式-part3

    5.3  挑几个运算符来讲 常用的运算符除了加减乘除(+-*/)外,还有如下: 注意:以下运算符之间用逗号隔开,C语言中也有逗号运算符,这里不讲逗号运算符. 1. 赋值运算符 =,+=,*= 2. 一 ...