介绍
概念
CAS(Central Authentication Service)
- 对应协议,有v1.0,v2.0,v3.0
- 对应apereo基金会的开源项目
SSO(Single-Sign-On)/ SLO(Single Log-Out)
- 单点登录
- 单点登出
CAS server
- 服务端,单独部署的cas应用
CAS client
- 需要利用统一认证的其他应用
几种协议
- CAS
- OAuth2.0
- SAML
- OpenID
原理
CAS协议
几种票据
- ticket-granting ticket (TGT)
- 俗称大令牌,或者说票根,他可以签发ST
- ticket-granting cookie (TGC)
- cas server的url下会存储的TGC cookie
- 是给cas server看的
- service ticket (ST)
- CAS server回传的,在URL上的票据
- 是给app看的
- app会去后台校验它
登录流程
- 可以通过一个通俗的例子来理解:
- 假设有一个大景区,会有不同的小景区,共用一个售票大厅(CAS系统)
- 不同的App可以理解为不同的小景区
- 用户/游客来到某一小景区的入口,准备进入的时候,会被入园管理人员告知(重定向)到售票大厅的售票窗口
- 游客在售票窗口,登记认证了自己的身份之后,拿到了一张景区门票(ST)
- 然后游客拿着门票,再来到景区入口,入园管理人员会拿着门票进行验票
- 通过验票的游客可以进入园区,且门票是限时不限次,有效期内再次入园,只用验门票
- 用户在进入其他小景区的时候,第一次依然会被告知(重定向)去售票大厅,但是之前已经登记了游客信息,可以直接快速通道拿到门票
实操
搭建一个CAS server
docker pull apereo/cas:6.6.6
docker run --name cas -p 8443:8443 -p 8442:8080 55.235.30.100:60000/library/apereo/cas:6.6.6 /bin/sh /cas-overlay/bin/run-cas.sh
# 会有certificates的问题
keytool -genkey -v -keystore debug.keystore -alias androiddebugkey -keyalg RSA -validity 10000
docker cp debug.keystore cas:/etc/cas/thekeystore
配置
/etc/cas/services/app-8900.json
- 这里可以配置支持的平台URL规则,仅支持自己的平台才能跳转CAS server。
{
"@class" : "org.apereo.cas.services.RegexRegisteredService",
"serviceId" : "^(https|http)://.*",
"name" : "app",
"id" : 8900,
"logoutType" : "BACK_CHANNEL",
"logoutUrl" : "localhost:8900/logout_callback"
}
/etc/cas/config/cas.properties
- cas.properties
cas.service-registry.core.init-from-json=true
cas.service-registry.json.location=file:/etc/cas/services
FastAPI实现案例
依赖包
- pip3 install python-cas
代码
- FastAPI的简单例子:
cas_client = CASClient(
version=3,
service_url='127.0.0.1:8002/login?next=%2Fprofile',
server_url='127.0.0.1:8000/cas/'
)
app.add_middleware(SessionMiddleware, secret_key="!secret")
@app.get('/')
async def index(request: Request):
return RedirectResponse(request.url_for('login'))
@app.get('/profile')
async def profile(request: Request):
print(request.session.get("user"))
user = request.session.get("user")
if user:
return HTMLResponse('Logged in as %s. <a href="/logout">Logout</a>' % user['user'])
return HTMLResponse('Login required. <a href="/login">Login</a>', status_code=403)
@app.get('/login')
def login(
request: Request, next: Optional[str] = None,
ticket: Optional[str] = None):
if request.session.get("user", None):
# Already logged in
return RedirectResponse(request.url_for('profile'))
# next = request.args.get('next')
# ticket = request.args.get('ticket')
if not ticket:
# No ticket, the request come from end user, send to CAS login
cas_login_url = cas_client.get_login_url()
print('CAS login URL: %s', cas_login_url)
return RedirectResponse(cas_login_url)
# There is a ticket, the request come from CAS as callback.
# need call `verify_ticket()` to validate ticket and get user profile.
print('ticket: %s', ticket)
print('next: %s', next)
user, attributes, pgtiou = cas_client.verify_ticket(ticket)
print(
'CAS verify ticket response: user: %s, attributes: %s, pgtiou: %s',
user, attributes, pgtiou)
if not user:
return HTMLResponse('Failed to verify ticket. <a href="/login">Login</a>')
else: # Login successfully, redirect according `next` query parameter.
response = RedirectResponse(next)
request.session['user'] = dict(user=user)
return response
@app.get('/logout')
def logout(request: Request):
redirect_url = request.url_for('logout_callback')
cas_logout_url = cas_client.get_logout_url(redirect_url)
print('CAS logout URL: %s', cas_logout_url)
return RedirectResponse(cas_logout_url)
@app.get('/logout_callback')
def logout_callback(request: Request):
# redirect from CAS logout request after CAS logout successfully
# response.delete_cookie('username')
request.session.pop("user", None)
return HTMLResponse('Logged out from CAS. <a href="/login">Login</a>')