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
}

相关文章
|
存储 NoSQL 算法
Redis-用户关注、附近商户、用户签到、UV统计
好友关注 关注和取消关注 针对用户的操作:可以对用户进行关注和取消关注功能。 实现思路: 需求:基于该表数据结构,实现两个接口: 关注和取关接口 判断是否关注的接口 关注是User之间的关系,是博主与粉丝的关系,数据库中有一张tb_follow表来标示: 注意: 这里需要把主键修改为自增长,简化开发。 FollowController //关注 @PutMapping("/{id}/{isFollow}") public Result follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow"
97 1
|
6月前
|
SQL 分布式计算 Spark
【指标计算】Spark 计算指定用户与其他用户购买的相同商品
该代码示例使用Spark SQL解决查找指定用户(user01)与其他用户共同购买商品的问题。首先,创建SparkSession和模拟购买数据,然后通过SQL查询获取user01购买的商品集合。接着,对比所有用户购买记录,筛选出购买过相同商品且非user01的用户。输出显示了这些匹配用户的商品ID。关键在于使用`array_contains`函数检查商品是否在指定用户的购买列表中。遇到类似需求时,可参考Spark SQL官方函数文档。欢迎讨论复杂指标计算问题。
71 4
|
5月前
索葛售票系统使用步骤--交易查询
索葛售票系统使用步骤--交易查询
|
6月前
|
存储 监控 NoSQL
|
NoSQL Redis
Redis学习7:按次结算的服务控制、微信会话顺序管理(应用场景总结)
现在数据类型五种基本的已经学完了,现在开始应用一个简单的业务场景。
Redis学习7:按次结算的服务控制、微信会话顺序管理(应用场景总结)
|
缓存 前端开发 JavaScript
四个简单例子教你通过用户行为记录提高用户体验
四个简单例子教你通过用户行为记录提高用户体验
192 0
四个简单例子教你通过用户行为记录提高用户体验
|
存储 SQL 搜索推荐
高并发帐户查询怎么做?
高并发帐户查询怎么做?
148 0
|
存储 NoSQL 安全
【Redis】位图以及位图的使用场景(统计在线人数和用户在线状态)
【Redis】位图以及位图的使用场景(统计在线人数和用户在线状态)
【Redis】位图以及位图的使用场景(统计在线人数和用户在线状态)
|
监控 应用服务中间件 nginx
日志服务之分析用户访问行为-5
日志服务之分析用户访问行为-5
189 0
|
Web App开发 弹性计算 监控
日志服务之分析用户访问行为-2
日志服务之分析用户访问行为-2
107 0