介绍
CAS( Central Authentication Service ) 是 Yale 大学发起的一个企业级的、开源的项目,旨在为 Web 应用系统提供一种可靠的单点登录解决方法(属于 Web SSO )。CAS 开始于 2001 年, 并在 2004 年 12 月正式成为 JA-SIG 的一个项目。
CAS是一种单点登录开源框架,遵循apache2.0协议,代码托管在github.com/apereo/cas上。
CAS作为一种单点登录框架,后端可配置不同的用户数据库,支持自定义验证或加密逻辑,并提供不同的协议用于与业务server(cas-client)间的通信。
CAS的源码是由java写的,因此对于java的web项目天生友好。当然只要实现CAS相关协议的client,无论是哪种语言实现的,都能集成到CAS认证框架中。
架构
CAS架构图:
CAS架构包括两部分:CAS Server和CAS Client。
- CAS Server 需要独立部署,主要负责对用户的认证工作;
- CAS Client 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server。
专业术语
SSO
单点登录(SSO,Single Sign-on)是一种方便用户访问多个系统的技术,用户只需在登录时进行一次注册,就可以在多个系统间自由穿梭,不必重复输入用户名和密码来确定身份。单点登录的实质就是安全上下文(Security Context)或凭证(Credential)在多个应用系统之间的传递或共享。
前提条件
1.有一个可正常运行的 CAS Server;
2.CAS服务端版本5.x,其他版本实现源码可能有差异,参考具体实现。
实现原理
CAS Server端负责对用户登录状态统一管理,源码中在“cas/support/cas-server-support-reports-core”模块提供了一些自身能力端点。如图:
SingleSignOnSessionsReportController 提供了以下方法给管理页面,供CAS服务端监控页面实现活跃会话查询、销毁能力。
基于CAS自身实现的会话管理能力,参考getSsoSessions实现方式,在服务端自行暴露一个加密的rest api ,即可实现在线用户数据统计。
实现代码
1.在CAS Server端创建一个Controller,参考getSsoSessions方法实现活跃session的查询,代码如下:
@RestController
@RequestMapping("/openApi/status")
public class SsoSessionsController {
#开发者自行设置ACCESS_TOKEN
@Value("${api.access_token:xxx}")
private String API_ACCESS_TOKEN;
@Autowired
private CentralAuthenticationService centralAuthenticationService;
private enum SsoSessionReportOptions {
ALL("all"),
PROXIED("proxied"),
DIRECT("direct");
private final String type;
/**
* Instantiates a new Sso session report options.
*
* @param type the type
*/
SsoSessionReportOptions(final String type) {
this.type = type;
}
public String getType() {
return this.type;
}
@Override
public String toString() {
return this.type;
}
}
private enum SsoSessionAttributeKeys {
AUTHENTICATED_PRINCIPAL("authenticated_principal"),
PRINCIPAL_ATTRIBUTES("principal_attributes"),
AUTHENTICATION_DATE("authentication_date"),
AUTHENTICATION_DATE_FORMATTED("authentication_date_formatted"),
TICKET_GRANTING_TICKET("ticket_granting_ticket"),
AUTHENTICATION_ATTRIBUTES("authentication_attributes"),
PROXIED_BY("proxied_by"),
AUTHENTICATED_SERVICES("authenticated_services"),
IS_PROXIED("is_proxied"),
NUMBER_OF_USES("number_of_uses");
private final String attributeKey;
/**
* Instantiates a new Sso session attribute keys.
*
* @param attributeKey the attribute key
*/
SsoSessionAttributeKeys(final String attributeKey) {
this.attributeKey = attributeKey;
}
@Override
public String toString() {
return this.attributeKey;
}
}
/**
* Endpoint for getting SSO Sessions in JSON format.
*
* @param type the type
* @return the sso sessions
*/
@GetMapping(value = "/getSsoSessions")
public Map<String, Object> getSsoSessions(@RequestParam(defaultValue = "ALL") final String type,
HttpServletRequest request,
HttpServletResponse response) {
boolean pass = this.API_ACCESS_TOKEN.equals(request.getHeader("API_ACCESS_TOKEN"));
if (!pass){
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return null;
}
final Map<String, Object> sessionsMap = new HashMap<>(1);
final SsoSessionReportOptions option = SsoSessionReportOptions.valueOf(type);
final Collection<Map<String, Object>> activeSsoSessions = getActiveSsoSessions(option);
sessionsMap.put("activeSsoSessions", activeSsoSessions);
long totalTicketGrantingTickets = 0;
long totalProxyGrantingTickets = 0;
long totalUsageCount = 0;
final Set<String> uniquePrincipals = new HashSet<>();
for (final Map<String, Object> activeSsoSession : activeSsoSessions) {
if (activeSsoSession.containsKey(SsoSessionsController.SsoSessionAttributeKeys.IS_PROXIED.toString())) {
final Boolean isProxied = Boolean.valueOf(activeSsoSession.get(SsoSessionAttributeKeys.IS_PROXIED.toString()).toString());
if (isProxied) {
totalProxyGrantingTickets++;
} else {
totalTicketGrantingTickets++;
final String principal = activeSsoSession.get(SsoSessionsController.SsoSessionAttributeKeys.AUTHENTICATED_PRINCIPAL.toString()).toString();
uniquePrincipals.add(principal);
}
} else {
totalTicketGrantingTickets++;
final String principal = activeSsoSession.get(SsoSessionsController.SsoSessionAttributeKeys.AUTHENTICATED_PRINCIPAL.toString()).toString();
uniquePrincipals.add(principal);
}
totalUsageCount += Long.parseLong(activeSsoSession.get(SsoSessionsController.SsoSessionAttributeKeys.NUMBER_OF_USES.toString()).toString());
}
sessionsMap.put("totalProxyGrantingTickets", totalProxyGrantingTickets);
sessionsMap.put("totalTicketGrantingTickets", totalTicketGrantingTickets);
sessionsMap.put("totalTickets", totalTicketGrantingTickets + totalProxyGrantingTickets);
sessionsMap.put("totalPrincipals", uniquePrincipals.size());
sessionsMap.put("totalUsageCount", totalUsageCount);
return sessionsMap;
}
/**
* Gets sso sessions.
*
* @param option the option
* @return the sso sessions
*/
private Collection<Map<String, Object>> getActiveSsoSessions(final SsoSessionReportOptions option) {
final Collection<Map<String, Object>> activeSessions = new ArrayList<>();
final ISOStandardDateFormat dateFormat = new ISOStandardDateFormat();
getNonExpiredTicketGrantingTickets().stream().map(TicketGrantingTicket.class::cast)
.filter(tgt -> !(option == SsoSessionReportOptions.DIRECT && tgt.getProxiedBy() != null))
.forEach(tgt -> {
final Authentication authentication = tgt.getAuthentication();
final Principal principal = authentication.getPrincipal();
final Map<String, Object> sso = new HashMap<>(SsoSessionAttributeKeys.values().length);
sso.put(SsoSessionAttributeKeys.AUTHENTICATED_PRINCIPAL.toString(), principal.getId());
sso.put(SsoSessionAttributeKeys.AUTHENTICATION_DATE.toString(), authentication.getAuthenticationDate());
sso.put(SsoSessionAttributeKeys.AUTHENTICATION_DATE_FORMATTED.toString(),
dateFormat.format(DateTimeUtils.dateOf(authentication.getAuthenticationDate())));
sso.put(SsoSessionAttributeKeys.NUMBER_OF_USES.toString(), tgt.getCountOfUses());
sso.put(SsoSessionAttributeKeys.TICKET_GRANTING_TICKET.toString(), tgt.getId());
sso.put(SsoSessionAttributeKeys.PRINCIPAL_ATTRIBUTES.toString(), principal.getAttributes());
sso.put(SsoSessionAttributeKeys.AUTHENTICATION_ATTRIBUTES.toString(), authentication.getAttributes());
if (option != SsoSessionReportOptions.DIRECT) {
if (tgt.getProxiedBy() != null) {
sso.put(SsoSessionAttributeKeys.IS_PROXIED.toString(), Boolean.TRUE);
sso.put(SsoSessionAttributeKeys.PROXIED_BY.toString(), tgt.getProxiedBy().getId());
} else {
sso.put(SsoSessionAttributeKeys.IS_PROXIED.toString(), Boolean.FALSE);
}
}
sso.put(SsoSessionAttributeKeys.AUTHENTICATED_SERVICES.toString(), tgt.getServices());
activeSessions.add(sso);
});
return activeSessions;
}
/**
* Gets non expired ticket granting tickets.
*
* @return the non expired ticket granting tickets
*/
private Collection<Ticket> getNonExpiredTicketGrantingTickets() {
return this.centralAuthenticationService.getTickets(ticket -> ticket instanceof TicketGrantingTicket && !ticket.isExpired());
}
}
2.修改CAS Server项目“resource/META_INF”下 spring.factories的方式, 将controller注入spring 容器:
3.在本地8080端口启动 CAS server ,有用户通过CAS服务登陆后,调用接口
请求PATH: /openApi/status/getSsoSessions
请求Header:
请求头 | 示例值 | 说明 |
---|---|---|
API_ACCESS_TOKEN | asaddavvs223121csa21 | 配置文件中api.access_token参数 |
请求参数:
参数名 | 示例值 | 说明 |
---|---|---|
type | all | session类型:all,proxied,direct |
返回结果:
{
"totalUsageCount": 2,
"activeSsoSessions": [
{
"authentication_date": 1639725511.953000000,
"authentication_date_formatted": "2021-12-17T15:18:31Z",
"authentication_attributes": {
"credentialType": "UsernamePasswordCaptchaCredential",
"authenticationMethod": "customerhandler",
"successfulAuthenticationHandlers": [
"customerhandler"
]
},
"authenticated_principal": "{\"firstLanding\":1,\"guid\":\"5C19B80D41B143B69C7EE59B45E92836\",\"id\":1,\"userCode\":\"zkl12\",\"userName\":\"公用测试1\"}",
"number_of_uses": 2,
"ticket_granting_ticket": "TGT-42-w0G43SpVN7Px-fmiMsV5-I8iwAnv1qhiXBbT1BcRcWWyWlb9HkeDCZdoo46XjNced2c-basic-utc-sso-pre-http-6f75d76cbb-7kbqt",
"principal_attributes": {
"login_name": "{\"firstLanding\":1,\"guid\":\"5C19B80D41B143B69C7EE59B45E92836\",\"id\":1,\"userCode\":\"zkl12\",\"userName\":\"公用测试1\"}"
},
"is_proxied": false,
"authenticated_services": {
"ST-59-oQcyQyMaXsQqyvEN0giI-KL8V9g-basic-utc-sso-pre-http-6f75d76cbb-7kbqt": {
"id": "http://basic-center-web-pre.ingress.dayu.work/sso/grant",
"originalUrl": "http://basic-center-web-pre.ingress.dayu.work/sso/grant",
"artifactId": null,
"principal": "{\"firstLanding\":1,\"guid\":\"5C19B80D41B143B69C7EE59B45E92836\",\"id\":1,\"userCode\":\"zkl12\",\"userName\":\"公用测试1\"}",
"loggedOutAlready": false
}
}
},
{
"authentication_date": 1639729085.718000000,
"authentication_date_formatted": "2021-12-17T16:18:05Z",
"authentication_attributes": {
"credentialType": "UsernamePasswordCaptchaCredential",
"authenticationMethod": "customerhandler",
"successfulAuthenticationHandlers": [
"customerhandler"
]
},
"authenticated_principal": "{\"firstLanding\":1,\"guid\":\"5C19B80D41B143B69C7EE59B45E92836\",\"id\":1,\"userCode\":\"zkl12\",\"userName\":\"公用测试1\"}",
"number_of_uses": 0,
"ticket_granting_ticket": "TGT-43-ezj7Vb580xCV77qGH26dtPfps4Ri5qGWJr9IqU-yUzovPrVK-TOR1I0H6xnd5QAiwyE-basic-utc-sso-pre-http-6f75d76cbb-7kbqt",
"principal_attributes": {
"login_name": "{\"firstLanding\":1,\"guid\":\"5C19B80D41B143B69C7EE59B45E92836\",\"id\":1,\"userCode\":\"zkl12\",\"userName\":\"公用测试1\"}"
},
"is_proxied": false,
"authenticated_services": {}
}
],
"totalTicketGrantingTickets": 2,
"totalTickets": 2,
"totalPrincipals": 1,
"totalProxyGrantingTickets": 0
}