CAS在线用户统计实现

简介: 随着应用平台的不断发展,对统一认证的能力需求越来越广泛。CAS框架在交付项目中承载了所有应用的登录能力,对平台用户登录状态统一管理。为了满足平台监控、日志等方面对用户实时在线情况展示的需求,需要在CAS server端开放rest api,实现用户在线数据统计。

介绍

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”模块提供了一些自身能力端点。如图:
image.png
SingleSignOnSessionsReportController 提供了以下方法给管理页面,供CAS服务端监控页面实现活跃会话查询、销毁能力。
image.png
基于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 容器:
image.png
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
}

相关文章
|
存储 网络协议 安全
DNS科普系列: DNS防火墙(DNS Firewall)
DNS防火墙是一种网络安全解决方案,可以防止网络用户和系统链接到已知的恶意网络位置,可以有效的预防威胁发生, 阻止访问受感染的站点,进而防止进一步的威胁。能够主动检测到已被感染的系统,并通过安全报告的形式将此类威胁通知用户。还能够保护用户的网络环境,提高网络安全性。
DNS科普系列: DNS防火墙(DNS Firewall)
|
Web App开发 iOS开发
无法安装此app,因为无法验证其完整性 ,解决方案
无法安装此app,因为无法验证其完整性 ,解决方案
|
安全 测试技术
网站CSRF跨站漏洞修复方案
CSRF通俗来讲就是跨站伪造请求攻击,英文Cross-Site Request Forgery,在近几年的网站安全威胁排列中排前三,跨站攻击利用的是网站的用户在登陆的状态下,在用户不知不觉的情况下执行恶意代码以及执行网站的权限操作,CSRF窃取不了用户的数据,只能执行用户能操作的一些数据。比如在用户不知道的情况下, 把账户里的金额,以及银行卡号,体现功能,都转移到其他人账户里去。如果被攻击者是一个管理员的权限,那么就会对网站安全构成严重的危害。
1631 0
网站CSRF跨站漏洞修复方案
|
11月前
|
存储 安全 Go
Map的遍历与判断键是否存在-《Go语言实战指南》
本文介绍了 Go 语言中对 `map` 的常见操作,包括遍历所有项和判断键是否存在。通过 `for range` 可以遍历 `map` 的键值对、仅键或仅值(需忽略键)。注意,`map` 遍历顺序是随机的。判断键是否存在时,使用双赋值语法 `value, ok := map[key]`,其中 `ok` 表示键是否存在。直接访问不存在的键会返回类型的零值,可能导致逻辑错误。掌握这些机制可更安全高效地处理键值对数据。
|
人工智能 数据可视化 TensorFlow
《解锁DevEco Studio:开启鸿蒙AI模型可视化开发新征程》
在人工智能与鸿蒙系统深度融合的趋势下,DevEco Studio作为华为打造的一站式开发平台,为人工智能模型的可视化开发提供了强大支持。通过搭建基础环境、引入AI框架(如HiAI或TensorFlow Lite)、运用智能代码编辑和低代码开发工具,以及借助DeepSeek等AI辅助编程功能,开发者可高效构建多端一致的AI应用。从环境配置到模型训练与界面优化,DevEco Studio助力探索创新应用场景,推动鸿蒙生态蓬勃发展,为用户带来智能化新体验。
422 0
|
存储 监控 负载均衡
搭建用户量比较大的合约交易所一般使用什么样的服务器配置
搭建用户量比较大的合约交易所需要使用高性能的服务器配置,以满足系统的稳定性、可靠性和性能要求。
|
存储 人工智能 自然语言处理
Github上的十大RAG(信息检索增强生成)框架
信息检索增强生成(RAG)是一种结合了检索系统和生成模型优势的技术,能够显著提升大型语言模型的性能。RAG通过从外部知识库中检索相关信息,增强模型的输入,从而生成更加准确、符合上下文、实时更新的响应。GitHub上涌现出多个开源RAG框架,如Haystack、RAGFlow、txtai等,每个框架都有独特的功能和特性,适用于不同的应用场景。这些框架不仅提高了模型的准确性和可靠性,还增强了过程的透明度和可解释性。
2185 2
|
JavaScript 前端开发 安全
【JavaScript 与 TypeScript 技术专栏】JavaScript 与 TypeScript 的对比与选择
【4月更文挑战第30天】本文对比了JavaScript和TypeScript在前端开发中的特点与差异。JavaScript以其灵活性和广泛支持成为Web开发基石,而TypeScript作为其超集,引入静态类型检查和面向对象概念,提升代码质量和开发效率。在项目选择中,考虑规模、团队协作和类型安全需求。两者可结合使用,逐步迁移或互操作,以适应不同场景。开发者应根据实际需求权衡利弊,发挥语言优势。
621 0
|
Web App开发 数据格式 索引
|
缓存 Java 大数据
CDH大数据环境参数优化指南
CDH大数据环境参数优化指南