cas深度解析:cas 客户端是如何验证serviceTicket(简称st)的?

今天有同事问子涵先生,cas获取用户数据的时候,是请求了什么接口?额,,,一时语顿……似乎问题没提在点子上。
请求什么接口……明明是客户端远程请求cas Server验证了ticket之后,由cas Server重定向送过来的嘛!当然你也可以理解为请求了什么接口,只是这个接口的参数是ticket和客户端的serviceUrl组成的,对,就是你登录的那个url。想了解更多,请阅读下文吧。

一、Cas ticket两种验证方式

本以为cas源码中提供的仅一种方式,于是乎想一探究竟,居然发现了多种。
cas ticke验证方式大体上讲有2个体系:

  • AbstractTicketValidationFilter,基于Filter的ticket的验证;
  • AbstractUrlBasedTicketValidator,基于TicketValidator的验证;

1-1 基于TicketValidator

有几个实现类

  • Cas20ServiceTicketValidator
  • Cas20ProxyTicketValidator
  • 等等

接下来看一下Cas20ServiceTicketValidator类图:

在这里插入图片描述

AbstractUrlBasedTicketValidator中有个validate方法:

    public Assertion validate(final String ticket, final String service) throws TicketValidationException {


        final String validationUrl = constructValidationUrl(ticket, service);
        if (log.isDebugEnabled()) {
            log.debug("Constructing validation url: " + validationUrl);
        }

        try {
        	log.debug("Retrieving response from server.");
            //从后端访问cas Server验证st的有效性
            final String serverResponse = retrieveResponseFromServer(new URL(validationUrl), ticket);

            if (serverResponse == null) {
                throw new TicketValidationException("The CAS server returned no response.");
            }
            
            if (log.isDebugEnabled()) {
            	log.debug("Server response: " + serverResponse);
            }
			//根据服务端的返回信息解析用户数据
            return parseResponseFromServer(serverResponse);
        } catch (final MalformedURLException e) {
            throw new TicketValidationException(e);
        }
    }

org.jasig.cas.client.validation.Cas20ServiceTicketValidator#parseResponseFromServer

    protected final Assertion parseResponseFromServer(final String response) throws TicketValidationException {
        final String error = XmlUtils.getTextForElement(response,
                "authenticationFailure");

        if (CommonUtils.isNotBlank(error)) {
            throw new TicketValidationException(error);
        }

        final String principal = XmlUtils.getTextForElement(response, "user");
        final String proxyGrantingTicketIou = XmlUtils.getTextForElement(response, "proxyGrantingTicket");
        final String proxyGrantingTicket = this.proxyGrantingTicketStorage != null ? this.proxyGrantingTicketStorage.retrieve(proxyGrantingTicketIou) : null;

        if (CommonUtils.isEmpty(principal)) {
            throw new TicketValidationException("No principal was found in the response from the CAS server.");
        }

        final Assertion assertion;
        final Map<String,Object> attributes = extractCustomAttributes(response);
        if (CommonUtils.isNotBlank(proxyGrantingTicket)) {
            final AttributePrincipal attributePrincipal = new AttributePrincipalImpl(principal, attributes, proxyGrantingTicket, this.proxyRetriever);
            assertion = new AssertionImpl(attributePrincipal);
        } else {
            assertion = new AssertionImpl(new AttributePrincipalImpl(principal, attributes));
        }

        customParseResponse(response, assertion);

        return assertion;
    }

1-2 Filter

当时这个项目是基于Filter集成的,在web.xml中配置了ticket验证的Filter。

<filter>
    <filter-name>CAS Validation Filter</filter-name>
    <filter-class>com.xxxx.fuie.filter.Cas20ProxyReceivingTicketValidationFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CAS Validation Filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

我们参考源码自定义了com.xxxx.fuie.filter.Cas20ProxyReceivingTicketValidationFilter并重新替换了CAS Validation Filter中的class。该class类图如下:

在这里插入图片描述

AbstractTicketValidationFilter负责解析用户数据,并放到session中,此处对于cas登录流程不展开描述了,请自行搜索,或结合server端的源码中的login-webflow进行查看。
com.xxxx.fuie.filter.Cas20ProxyReceivingTicketValidationFilter源码片段如下:

public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {

        if (!preFilter(servletRequest, servletResponse, filterChain)) {
            return;
        }

        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        final HttpServletResponse response = (HttpServletResponse) servletResponse;
        final String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName());

        if (CommonUtils.isNotBlank(ticket)) {
            if (log.isDebugEnabled()) {
                log.debug("Attempting to validate ticket: " + ticket);
            }

            try {
                final Assertion assertion = this.ticketValidator.validate(ticket, constructServiceUrl(request, response));

                if (log.isDebugEnabled()) {
                    log.debug("Successfully authenticated user: " + assertion.getPrincipal().getName());
                }
				//用户信息放入request
                request.setAttribute(CONST_CAS_ASSERTION, assertion);
				// 用户信息放入session
                if (this.useSession) {
                    request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion);
                }
                onSuccessfulValidation(request, response, assertion);

                if (this.redirectAfterValidation) {
                    log. debug("Redirecting after successful ticket validation.");
                    response.sendRedirect(constructServiceUrl(request, response));
                    return;
                }
            } catch (final TicketValidationException e) {
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                log.warn(e, e);

                onFailedValidation(request, response);

                if (this.exceptionOnValidationFailure) {
                    throw new ServletException(e);
                }

                return;
            }
        }

        filterChain.doFilter(request, response);

    }

用户认证完成后,就可以从此次request中获取用户信息:

@Override
    public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) {
        this.outTarget = target;
        this.request = request;

        int index = target.lastIndexOf(";jsessionid");

        if (index > -1) {
            outTarget = target.substring(0, index);
        }

        AttributePrincipal principal1 = (AttributePrincipal) request.getUserPrincipal();
        //=======忽略不相干的代码===================
    }

在这里插入图片描述

感谢您的赏读。客官,点赞、留言再走呗~或者留下您的问题我们一起探讨

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论
可以使用Spring Security CAS扩展来实现。在pom.xml文件中添加以下依赖项: ``` <dependency> <groupId>org.springframework.security.extensions</groupId> <artifactId>spring-security-cas</artifactId> <version>1.0.7.RELEASE</version> </dependency> ``` 然后在application.properties文件中添加以下配置: ``` # CAS server URL cas.server.url=https://cas.example.com/cas # CAS server login URL cas.server.login.url=https://cas.example.com/cas/login # CAS server logout URL cas.server.logout.url=https://cas.example.com/cas/logout # CAS service URL cas.service.url=http://localhost:8080/login/cas # CAS service name cas.service.name=MyApp # CAS service login URL cas.service.login.url=http://localhost:8080/login # CAS service logout URL cas.service.logout.url=http://localhost:8080/logout # CAS service validate URL cas.service.validate.url=https://cas.example.com/cas/serviceValidate # CAS service ticket parameter name cas.service.ticket.parameterName=ticket # CAS service renew parameter name cas.service.renew.parameterName=renew # CAS service gateway parameter name cas.service.gateway.parameterName=gateway # CAS service artifact parameter name cas.service.artifact.parameterName=artifact # CAS service proxy callback URL cas.service.proxy.callbackUrl=http://localhost:8080/proxyCallback # CAS service proxy callback parameter name cas.service.proxy.callbackParameterName=pgtIou # CAS service proxy granting ticket parameter name cas.service.proxy.grantingTicket.parameterName=pgtIou # CAS service proxy granting ticket storage class cas.service.proxy.grantingTicket.storageClass=org.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl # CAS service proxy granting ticket storage file cas.service.proxy.grantingTicket.storageFile=/tmp/cas-proxy-granting-tickets # CAS service proxy granting ticket storage clean interval cas.service.proxy.grantingTicket.storageCleanInterval=3600000 # CAS service proxy granting ticket storage clean up cas.service.proxy.grantingTicket.storageCleanUp=true # CAS service proxy granting ticket storage clean up interval cas.service.proxy.grantingTicket.storageCleanUpInterval=3600000 # CAS service proxy granting ticket storage clean up max age cas.service.proxy.grantingTicket.storageCleanUpMaxAge=7200000 ``` 然后在Spring Boot应用程序中添加以下配置类: ``` @Configuration @EnableWebSecurity @EnableCasSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CasAuthenticationEntryPoint casAuthenticationEntryPoint; @Autowired private CasAuthenticationProvider casAuthenticationProvider; @Autowired private SingleSignOutFilter singleSignOutFilter; @Autowired private CasAuthenticationFilter casAuthenticationFilter; @Autowired private CasProperties casProperties; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/login").permitAll() .anyRequest().authenticated() .and() .exceptionHandling() .authenticationEntryPoint(casAuthenticationEntryPoint) .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/") .addLogoutHandler(new SingleSignOutHandler(casProperties.getServer().getLogoutUrl())) .and() .addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class) .addFilter(casAuthenticationFilter); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(casAuthenticationProvider); } @Bean public ServiceProperties serviceProperties() { ServiceProperties serviceProperties = new ServiceProperties(); serviceProperties.setService(casProperties.getService().getUrl()); serviceProperties.setSendRenew(false); return serviceProperties; } @Bean public CasAuthenticationEntryPoint casAuthenticationEntryPoint() { CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint(); casAuthenticationEntryPoint.setLoginUrl(casProperties.getServer().getLoginUrl()); casAuthenticationEntryPoint.setServiceProperties(serviceProperties()); return casAuthenticationEntryPoint; } @Bean public CasAuthenticationProvider casAuthenticationProvider() { CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider(); casAuthenticationProvider.setAuthenticationUserDetailsService(new UserDetailsServiceImpl()); casAuthenticationProvider.setServiceProperties(serviceProperties()); casAuthenticationProvider.setTicketValidator(new Cas30ServiceTicketValidator(casProperties.getServer().getUrl())); casAuthenticationProvider.setKey("casAuthenticationProviderKey"); return casAuthenticationProvider; } @Bean public SingleSignOutFilter singleSignOutFilter() { SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter(); singleSignOutFilter.setCasServerUrlPrefix(casProperties.getServer().getUrl()); singleSignOutFilter.setIgnoreInitConfiguration(true); return singleSignOutFilter; } @Bean public CasAuthenticationFilter casAuthenticationFilter() { CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter(); casAuthenticationFilter.setAuthenticationManager(authenticationManager()); casAuthenticationFilter.setFilterProcessesUrl("/login/cas"); return casAuthenticationFilter; } } ``` 最后,在Spring Boot应用程序中添加以下服务类: ``` @Service public class UserDetailsServiceImpl implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken> { @Override public UserDetails loadUserDetails(CasAssertionAuthenticationToken token) throws UsernameNotFoundException { String username = token.getName(); List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER"); return new User(username, "", authorities); } } ``` 现在,您可以使用Spring Boot应用程序调用CAS客户端自动配置支持来解析票据。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

子涵先生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值