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"
111 1
|
7月前
|
SQL XML JSON
技术心得:查询重置功能的实现
技术心得:查询重置功能的实现
49 0
|
8月前
|
存储 监控 NoSQL
|
8月前
|
存储 监控 NoSQL
使用Redis的Bitmap统计一周连续登录的用户
使用Redis的Bitmap统计一周连续登录的用户
249 1
|
8月前
|
存储 移动开发 小程序
利用微搭搭建信息查询小程序
利用微搭搭建信息查询小程序
|
8月前
|
数据安全/隐私保护
用户及组管理操作
用户及组管理操作
57 0
|
存储 运维 监控
面试题分析:统计网站访问次数
平台的访问量非常高,需要实时统计网站的访问次数,请设计一个计数器解决: 初级工程师,可能回答使用synchronized锁或重入锁,进一步探讨,synchronized锁太重,有没其他方式,可能回答atomic类,进一步问,atomic类原理,什么场景下适合用,什么场景下不适合用 atomic和synchronized都是单机方案,当一个服务器不能满足性能要求时,线上使用集群,如何在集群场景下实现计数器
326 1
|
NoSQL Redis
Redis学习7:按次结算的服务控制、微信会话顺序管理(应用场景总结)
现在数据类型五种基本的已经学完了,现在开始应用一个简单的业务场景。
Redis学习7:按次结算的服务控制、微信会话顺序管理(应用场景总结)
|
Java 数据安全/隐私保护
WEB核心【记录网站登录人数,记录用户名案例】Cookie技术实现
本篇讲两个案例(记录网站登录人数,记录用户名案例)用会话技术cookie进行实现。
WEB核心【记录网站登录人数,记录用户名案例】Cookie技术实现
|
存储 NoSQL 安全
【Redis】位图以及位图的使用场景(统计在线人数和用户在线状态)
【Redis】位图以及位图的使用场景(统计在线人数和用户在线状态)
【Redis】位图以及位图的使用场景(统计在线人数和用户在线状态)