cas client与shiro集成框架pac4j源码分析

cas client与shiro集成框架pac4j源码分析

一、前言

pac4j-cas关键filter有3个,分别为io.buji.pac4j.filter.SecurityFilterio.buji.pac4j.filter.CallbackFilterio.buji.pac4j.filter.LogoutFilter
SecurityFilter负责对登录认证判断,重定向到cas服务的login页面等;
CallbackFilter负责在cas登录完成后,cas回调客户端,校验cas颁发的ST等;
LogoutFilter负责单点登出。

二、SecurityFilter

        // cas 资源认证拦截器
        SecurityFilter securityFilter = new SecurityFilter();
        securityFilter.setConfig(config);
        securityFilter.setClients(casProperties.getCasClientName());

三、CallbackFilter

流程:
io.buji.pac4j.filter.CallbackFilter#doFilter
->org.pac4j.core.engine.DefaultCallbackLogic#perform 逻辑类
->org.pac4j.core.client.IndirectClient#getCredentials
->org.pac4j.core.client.BaseClient#retrieveCredentials
->org.pac4j.cas.credentials.authenticator.CasAuthenticator#validate 校验类
->org.jasig.cas.client.validation.AbstractUrlBasedTicketValidator#validate
->org.jasig.cas.client.validation.AbstractUrlBasedTicketValidator#constructValidationUrl

CAS Protocol
java-cas-client
gateway & renew

CAS is an HTTP2,3-based protocol that requires each of its components to be accessible through specific URIs. This section will discuss each of the URIs:

URIDescription
/logincredential requestor / acceptor
/logoutdestroy CAS session (logout)
/validateservice ticket validation
/serviceValidateservice ticket validation [CAS 2.0]
/proxyValidateservice/proxy ticket validation [CAS 2.0]
/proxyproxy ticket service [CAS 2.0]
/p3/serviceValidateservice ticket validation [CAS 3.0]
/p3/proxyValidateservice/proxy ticket validation [CAS 3.0]

说明:建议根据CAS服务端版本设置使用相应的CAS Protocol ,CAS 5.x建议使用CasProtocol.CAS30,低版本建议使用CasProtocol.CAS20,以防获取额外属性信息org.pac4j.core.profile.UserProfile#getAttribute(java.lang.String)失败。
validate结果会以xml方式返回给cas客户端。

原因CasProtocol.CAS30在构造url时org.jasig.cas.client.validation.AbstractUrlBasedTicketValidator#constructValidationUrl时会拼接/p3/serviceValidate

    @Bean
    public CasConfiguration casConfig(){
        final CasConfiguration configuration = new CasConfiguration();
        //CAS server登录地址
        configuration.setLoginUrl(casProperties.getCasServerUrl() + "/login");
        //CAS 版本,默认为 CAS30,CAS服务端版本较低时使用CasProtocol.CAS20
        configuration.setProtocol(CasProtocol.CAS30);
        configuration.setAcceptAnyProxy(true);
        configuration.setPrefixUrl(casProperties.getCasServerUrl() + "/");
        return configuration;
    }
        final ShiroCallbackLogic<Object, J2EContext> shiroCallbackLogic = new ShiroCallbackLogic<>();
        shiroCallbackLogic.setErrorUrl(casProperties.getCasClientUrl() + "/unauth");
        // cas 认证后回调拦截器
        CallbackFilter callbackFilter = new CallbackFilter();
        callbackFilter.setCallbackLogic(shiroCallbackLogic);
        callbackFilter.setDefaultUrl(casProperties.getCasClientUrl());
        callbackFilter.setConfig(config);

源码分析:
io.buji.pac4j.filter.CallbackFilter#doFilter

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

        assertNotNull("callbackLogic", callbackLogic);
        assertNotNull("config", config);

        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        final HttpServletResponse response = (HttpServletResponse) servletResponse;
        final SessionStore<J2EContext> sessionStore = config.getSessionStore();
        final J2EContext context = new J2EContext(request, response, sessionStore != null ? sessionStore : ShiroSessionStore.INSTANCE);
        final HttpActionAdapter<Object, J2EContext> adapter = httpActionAdapter != null ? httpActionAdapter : J2ENopHttpActionAdapter.INSTANCE;

        callbackLogic.perform(context, config, adapter, this.defaultUrl, this.saveInSession, this.multiProfile, false, this.defaultClient);
    }

org.pac4j.core.engine.DefaultCallbackLogic#perform

 public R perform(C context, Config config, HttpActionAdapter<R, C> httpActionAdapter, String inputDefaultUrl, Boolean inputSaveInSession, Boolean inputMultiProfile, Boolean inputRenewSession, String client) {
        this.logger.debug("=== CALLBACK ===");

        HttpAction action;
        try {
            String defaultUrl;
            if (inputDefaultUrl == null) {
                defaultUrl = "/";
            } else {
                defaultUrl = inputDefaultUrl;
            }

            boolean saveInSession;
            if (inputSaveInSession == null) {
                saveInSession = true;
            } else {
                saveInSession = inputSaveInSession;
            }

            boolean multiProfile;
            if (inputMultiProfile == null) {
                multiProfile = false;
            } else {
                multiProfile = inputMultiProfile;
            }

            boolean renewSession;
            if (inputRenewSession == null) {
                renewSession = true;
            } else {
                renewSession = inputRenewSession;
            }

            CommonHelper.assertNotNull("clientFinder", this.clientFinder);
            CommonHelper.assertNotNull("context", context);
            CommonHelper.assertNotNull("config", config);
            CommonHelper.assertNotNull("httpActionAdapter", httpActionAdapter);
            CommonHelper.assertNotBlank("defaultUrl", defaultUrl);
            Clients clients = config.getClients();
            CommonHelper.assertNotNull("clients", clients);
            List<Client> foundClients = this.clientFinder.find(clients, context, client);
            CommonHelper.assertTrue(foundClients != null && foundClients.size() == 1, "unable to find one indirect client for the callback: check the callback URL for a client name parameter or suffix path or ensure that your configuration defaults to one indirect client");
            Client foundClient = (Client)foundClients.get(0);
            this.logger.debug("foundClient: {}", foundClient);
            CommonHelper.assertNotNull("foundClient", foundClient);
            Credentials credentials = foundClient.getCredentials(context);
            this.logger.debug("credentials: {}", credentials);
            CommonProfile profile = foundClient.getUserProfile(credentials, context);
            this.logger.debug("profile: {}", profile);
            this.saveUserProfile(context, config, profile, saveInSession, multiProfile, renewSession);
            action = this.redirectToOriginallyRequestedUrl(context, defaultUrl);
        } catch (RuntimeException var19) {
            return this.handleException(var19, httpActionAdapter, context);
        }

        return httpActionAdapter.adapt(action.getCode(), context);
    }

ShiroCallbackLogic类图
org.pac4j.core.client.IndirectClient#getCredentials

    /**
     * <p>Get the credentials from the web context. In some cases, a {@link HttpAction} may be thrown:</p>
     * <ul>
     * <li>if the <code>CasClient</code> receives a logout request, it returns a 200 HTTP status code</li>
     * <li>for the <code>IndirectBasicAuthClient</code>, if no credentials are sent to the callback url, an unauthorized response
     * (401 HTTP status code) is returned to request credentials through a popup.</li>
     * </ul>
     *
     * @param context the current web context
     * @return the credentials
     */
    @Override
    public final C getCredentials(final WebContext context) {
        init();
        final C credentials = retrieveCredentials(context);
        // no credentials -> save this authentication has already been tried and failed
        if (credentials == null) {
            context.getSessionStore().set(context, getName() + ATTEMPTED_AUTHENTICATION_SUFFIX, "true");
        } else {
            cleanAttemptedAuthentication(context);
        }
        return credentials;
    }

org.pac4j.core.client.BaseClient#retrieveCredentials

    /**
     * Retrieve the credentials.
     *
     * @param context the web context
     * @return the credentials
     */
    protected C retrieveCredentials(final WebContext context) {
        try {
            final C credentials = this.credentialsExtractor.extract(context);
            if (credentials == null) {
                return null;
            }
            final long t0 = System.currentTimeMillis();
            try {
                this.authenticator.validate(credentials, context);
            } finally {
                final long t1 = System.currentTimeMillis();
                logger.debug("Credentials validation took: {} ms", t1 - t0);
            }
            return credentials;
        } catch (CredentialsException e) {
            logger.info("Failed to retrieve or validate credentials: {}", e.getMessage());
            logger.debug("Failed to retrieve or validate credentials", e);

            return null;
        }
    }

org.pac4j.cas.credentials.authenticator.CasAuthenticator#validate

    @Override
    public void validate(final TokenCredentials credentials, final WebContext context) {
        init();

        final String ticket = credentials.getToken();
        try {
            final String finalCallbackUrl = callbackUrlResolver.compute(urlResolver, callbackUrl, clientName, context);
            final Assertion assertion = configuration.retrieveTicketValidator(context).validate(ticket, finalCallbackUrl);
            final AttributePrincipal principal = assertion.getPrincipal();
            logger.debug("principal: {}", principal);

            final String id = principal.getName();
            final Map<String, Object> newPrincipalAttributes = new HashMap<>();
            final Map<String, Object> newAuthenticationAttributes = new HashMap<>();
            // restore both sets of attributes
            final Map<String, Object> oldPrincipalAttributes = principal.getAttributes();
            final Map<String, Object> oldAuthenticationAttributes = assertion.getAttributes();
            final InternalAttributeHandler attrHandler = ProfileHelper.getInternalAttributeHandler();
            if (oldPrincipalAttributes != null) {
                oldPrincipalAttributes.entrySet().stream()
                    .forEach(e -> newPrincipalAttributes.put(e.getKey(), attrHandler.restore(e.getValue())));
            }
            if (oldAuthenticationAttributes != null) {
                oldAuthenticationAttributes.entrySet().stream()
                    .forEach(e -> newAuthenticationAttributes.put(e.getKey(), attrHandler.restore(e.getValue())));
            }

            final CommonProfile profile;
            // in case of CAS proxy, don't restore the profile, just build a CAS one
            if (configuration.getProxyReceptor() != null) {
                profile = getProfileDefinition().newProfile(principal, configuration.getProxyReceptor());
                profile.setId(ProfileHelper.sanitizeIdentifier(profile, id));
                getProfileDefinition().convertAndAdd(profile, newPrincipalAttributes, newAuthenticationAttributes);
            } else {
                profile = ProfileHelper.restoreOrBuildProfile(getProfileDefinition(), id, newPrincipalAttributes,
                        newAuthenticationAttributes, principal, configuration.getProxyReceptor());
            }
            logger.debug("profile returned by CAS: {}", profile);

            credentials.setUserProfile(profile);
        } catch (final TicketValidationException e) {
            String message = "cannot validate CAS ticket: " + ticket;
            throw new TechnicalException(message, e);
        }
    }

org.pac4j.cas.config.CasConfiguration#retrieveTicketValidator

 public TicketValidator retrieveTicketValidator(final WebContext context) {
        if (this.defaultTicketValidator != null) {
            return this.defaultTicketValidator;
        } else {
            if (this.protocol == CasProtocol.CAS10) {
                return buildCas10TicketValidator(context);
            } else if (this.protocol == CasProtocol.CAS20) {
                return buildCas20TicketValidator(context);
            } else if (this.protocol == CasProtocol.CAS20_PROXY) {
                return buildCas20ProxyTicketValidator(context);
            } else if (this.protocol == CasProtocol.CAS30) {
                return buildCas30TicketValidator(context);
            } else if (this.protocol == CasProtocol.CAS30_PROXY) {
                return buildCas30ProxyTicketValidator(context);
            } else if (this.protocol == CasProtocol.SAML) {
                return buildSAMLTicketValidator(context);
            } else {
                throw new TechnicalException("Unable to initialize the TicketValidator for protocol: " + this.protocol);
            }
        }
    }

org.jasig.cas.client.validation.AbstractUrlBasedTicketValidator#validate

    public final Assertion validate(final String ticket, final String service) throws TicketValidationException {
        final String validationUrl = constructValidationUrl(ticket, service);
        logger.debug("Constructing validation url: {}", validationUrl);

        try {
            logger.debug("Retrieving response from server.");
            final String serverResponse = retrieveResponseFromServer(new URL(validationUrl), ticket);

            if (serverResponse == null) {
                throw new TicketValidationException("The CAS server returned no response.");
            }

            logger.debug("Server response: {}", serverResponse);

            return parseResponseFromServer(serverResponse);
        } catch (final MalformedURLException e) {
            throw new TicketValidationException(e);
        }
    }

org.jasig.cas.client.validation.AbstractUrlBasedTicketValidator#constructValidationUrl

protected final String constructValidationUrl(final String ticket, final String serviceUrl) {
        final Map<String, String> urlParameters = new HashMap<String, String>();

        logger.debug("Placing URL parameters in map.");
        urlParameters.put("ticket", ticket);
        urlParameters.put("service", serviceUrl);

        if (this.renew) {
            urlParameters.put("renew", "true");
        }

        logger.debug("Calling template URL attribute map.");
        populateUrlAttributeMap(urlParameters);

        logger.debug("Loading custom parameters from configuration.");
        if (this.customParameters != null) {
            urlParameters.putAll(this.customParameters);
        }

        final String suffix = getUrlSuffix();
        final StringBuilder buffer = new StringBuilder(urlParameters.size() * 10 + this.casServerUrlPrefix.length()
                + suffix.length() + 1);

        int i = 0;

        buffer.append(this.casServerUrlPrefix);
        if (!this.casServerUrlPrefix.endsWith("/")) {
            buffer.append("/");
        }
        buffer.append(suffix);

        for (Map.Entry<String, String> entry : urlParameters.entrySet()) {
            final String key = entry.getKey();
            final String value = entry.getValue();

            if (value != null) {
                buffer.append(i++ == 0 ? "?" : "&");
                buffer.append(key);
                buffer.append("=");
                final String encodedValue = encodeUrl(value);
                buffer.append(encodedValue);
            }
        }

        return buffer.toString();

    }

四、LogoutFilter

    /**
     * 自定义存储
     * 注意:不可使用{@link io.buji.pac4j.context.ShiroSessionStore},否则无法建立票据与session之间联系,导致单点退出失效!
     * @return
     */
    @Bean
    public SessionStore sessionStore(){
        return new J2ESessionStore();
    }

    @Bean
    public Config config(CasClient casClient, SessionStore sessionStore) {
        Config config = new Config(casClient);
        config.setSessionStore(sessionStore);
        return config;
    }
        // 登出拦截器
        LogoutFilter logoutFilter = new LogoutFilter();
        logoutFilter.setConfig(config);
        // 单点登出
        logoutFilter.setCentralLogout(true);
        // 本地登出
        logoutFilter.setLocalLogout(true);
        logoutFilter.setDefaultUrl(casProperties.getCasClientUrl() + "/callback?client_name=" + casProperties.getCasClientName());

参考:
利用pac4j的封装,实现自定义cas校验ST、集成jwt

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

搬山境KL攻城狮

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

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

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

打赏作者

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

抵扣说明:

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

余额充值