背景:
…
CAS介绍:
CAS ( Central Authentication Service ),最初由耶鲁大学的Shawn Bayern
开发,后由Jasig社区维护,经过十多年发展,目前已成为影响最大、广泛使用的、基于Java实现的、开源SSO解决方案。cas旨在为 Web应用系统提供一种可靠的单点登录解决方法(属于 Web SSO )。 CAS 开始于 2001 年, 并在 2004 年 12 月正式成为JA-SIG 的一个项目。
CAS原理:
cas单点登录流程理解:
- 浏览器请求cas客户端(接入cas的系统)时,cas客户端对当前请求浏览器校验是否存在session(odoo系统中则校验当前cookie是否存在session_id值,根据session_id值判断/data/sessions文件夹内是否存在对应session。如启用redis管理session,则在redis中查询是否存在对应session_id的数据判断是否登录)。
- 如校验无登录cas客户端,则返回重定向到cas服务器。链接形如:https://localcas:8443/cas/login?service=http%3A%2F%2F192.168.99.102%3A8069%2Fweb%2Flogin。
其中https://localcas:8443/cas/login为cas服务器登录地址,service参数值为cas客户端登录地址 - 通过用户名密码登录cas认证系统,认证成功后携带ticket参数重定向到cas客户端登录 地址,形如:
http://192.168.99.102:8069/web/login?ticket=ST-17-3hA0-atfanDFWw4beSnHodOdMCY-DESKTOP-RAY - 浏览器请求重定向地址到cas客户端。
- cas客户端获取请求参数ticket,并携带ticket参数请求cas服务端换取用户。
- 成功获取用户名则创建session,设置cookie值。让浏览器重定向到cas客户端系统首页。
- 没有成功获取用户名则返回cas服务端登录地址。
PS:CAS认证系统通过cookie值中Jsession_id值判断是否通过CAS认证。
核心代码:
- 安装python-cas库
pip install python-cas
- 引入cas库 修改controllers里登录的方法
# -*- coding:utf-8 -*-
import cas
import odoo
import random
import werkzeug
import xmltodict
import configparser
from odoo import _
from odoo import http
from odoo.service import security
from odoo.addons.web.controllers import main
from odoo.tools import ustr, consteq, frozendict, pycompat
from odoo.http import request, Response, Root, OpenERPSession
class HomeFLih(main.Home):
# OA单点登录
@http.route('/web/login', type='http', auth="none",csrf=False, sitemap=False)
def web_login(self, redirect=None, **kw):
if 'logoutRequest' in kw:
return self.web_logout(redirect=redirect,**kw)
# 生成CAS客户端类实例
client = cas.CASClient(
# CAS 版本 ,V1,V2,V3 版本
version='3',
# CAS 服务端登录地址,有get_login_url方法拼接/login 生成
server_url='https://localcas:8443/cas/',
# CAS 客户端登录地址
service_url='http://192.168.99.102:8069/web/login',
# 是否校验ssl证书
verify_ssl_certificate=False
)
# 判断是否有ticket参数
if 'ticket' in request.params:
ticket = request.params.get('ticket','')
# 请求CAS服务端,使用ticket换取用户名
res = client.verify_ticket(ticket)
user_name = res[0]
if not user_name:
cas_url = client.get_login_url()
return werkzeug.utils.redirect(cas_url,303)
# 写死数据库,仅为demo需要,生产环境可读取配置文件赋值
request.session.db = 'test'
users = request.env['res.users'].search([('login','=ilike',user_name)],limit=1)
request.session['uid'] = users.id if users else False
request.session['ticket'] = ticket
request.params['login'] = users.login if users else False
# 随便赋值,用于cas用户校验逻辑
request.params['password'] = users.login if users else False
# 用于cas用户校验逻辑
request.session.context['CAS_SIGNAL_LOGIN'] = True
# 未找到用户,报错
if not users:
return """<script language="javascript" type="text/javascript">
alert('""" + _('The username %s is not found,Plz contact the admin!')%(user_name) + """');
window.opener=null;
window.open("", "_self");
window.close();
</script>"""
uid = request.session.authenticate(request.session.db, request.params['login'],
request.params['password'])
if uid is not False:
request.uid = uid
return http.redirect_with_hash('/web')
else:
cas_url = client.get_login_url()
return werkzeug.utils.redirect(cas_url,303)
# -*- coding:utf-8 -*-
import logging
from odoo.http import request
from odoo.exceptions import AccessDenied
from odoo import api, fields, models, tools, SUPERUSER_ID, _
_logger = logging.getLogger(__name__)
# 修改源码密码校验
class ResUsers(models.Model):
_inherit = 'res.users'
@classmethod
def _login(cls, db, login, password, context={}):
if not password:
raise AccessDenied()
ip = request.httprequest.environ['REMOTE_ADDR'] if request else 'n/a'
try:
with cls.pool.cursor() as cr:
self = api.Environment(cr, SUPERUSER_ID, context)[cls._name]
with self._assert_can_auth():
user = self.search(self._get_login_domain(login))
if not user:
raise AccessDenied()
user = user.sudo(user.id)
user._check_credentials(password)
user._update_last_login()
except AccessDenied:
_logger.info("Login failed for db:%s login:%s from %s", db, login, ip)
raise
_logger.info("Login successful for db:%s login:%s from %s", db, login, ip)
return user.id
@classmethod
def authenticate(cls, db, login, password, user_agent_env):
"""Verifies and returns the user ID corresponding to the given
``login`` and ``password`` combination, or False if there was
no matching user.
:param str db: the database on which user is trying to authenticate
:param str login: username
:param str password: user password
:param dict user_agent_env: environment dictionary describing any
relevant environment attributes
"""
if user_agent_env and user_agent_env.get('context'):
uid = cls._login(db, login, password, context=user_agent_env['context'])
else:
uid = cls._login(db, login, password)
if user_agent_env and user_agent_env.get('base_location'):
with cls.pool.cursor() as cr:
env = api.Environment(cr, uid, {})
if env.user.has_group('base.group_system'):
# Successfully logged in as system user!
# Attempt to guess the web base url...
try:
base = user_agent_env['base_location']
ICP = env['ir.config_parameter']
if not ICP.get_param('web.base.url.freeze'):
ICP.set_param('web.base.url', base)
except Exception:
_logger.exception("Failed to update web.base.url configuration parameter")
return uid
def _check_credentials(self, password):
# convert to base_crypt if needed
if self.env.context.get('CAS_SIGNAL_LOGIN', False):
return
else:
return super(ResUsers, self)._check_credentials(password)
实现效果:
cas单点登出流程理解:
- 客户端主动登出:失效客户端系统session,重定向到CAS服务端登出页面,执行CAS验证系统登出操作。
- CAS服务端或其他第三方客户端执行登出:当
CAS服务端或其他第三方客户端执行登出时,CAS验证系统也执行登出操作。CAS验证系统登出时,将请求第三方系统,将登出用户的ticket传到第三方系统服务端,让第三方执行登出操作。
核心代码:
- 修改controllers里登录的方法
# -*- coding:utf-8 -*-
import cas
import odoo
import random
import werkzeug
import xmltodict
import configparser
from odoo import _
from odoo import http
from odoo.service import security
from odoo.addons.web.controllers import main
from odoo.tools import ustr, consteq, frozendict, pycompat
from odoo.http import request, Response, Root, OpenERPSession
class Home(main.Home):
# OA单点登出
@http.route('/web/logout', type='http', auth="none",csrf=False, sitemap=False)
def web_logout(self, redirect=None, **kw):
logoutRequest = kw.get('logoutRequest','')
root = xmltodict.parse(logoutRequest)
ticket = root.get('samlp:LogoutRequest', {}).get('samlp:SessionIndex', '')
# 搜索odoo session文件,执行登出操作。
# 循环访问本地session文件效率很低,可改用数据库存储ticket与session_uid关系,或使用radis代替本地文件存储session信息
for rec in http.root.session_store.list():
logout_session = http.root.session_store.get(rec)
if logout_session.get('ticket') == ticket:
logout_session.logout(keep_db=True)
http.root.session_store.save(logout_session)
# #http.root.session_store.delete(logout_session)
response = request.make_response('')
return response
class Session(main.Session):
@http.route('/web/session/logout', type='http', auth="none")
def logout(self, redirect='/web'):
client = cas.CASClient(
version='3',
server_url='https://localcas:8443/cas/',
service_url='http://192.168.99.102:8069/web/login',
verify_ssl_certificate=False
)
request.session.logout(keep_db=True)
return werkzeug.utils.redirect(client.get_logout_url(client.service_url), 303)
实现效果: