cas6.6.x~7.0.RC版本自定义多种登录方式手机验证码登录稍加修改即可实现

CAS7.0.0RC版本扩展默认登录以及增加多种登录方式

除了扩展默认登录还增加了第三方的钉钉登录



前言

CAS在新版本中使用了SpringBoot3以及gradle 所以需要在熟悉SpringBoot3和gradle的前提下进行学习

工作需要需要对cas5.3.x版本进行升级到7.0RC版本
cas5.3升级7.0版本改动较大故升级过程在这里记录一下


注意事项

Jdk版本使用openJDK17 不要使用Oracle版本的JDK 会启动报错的

一、获取cas7.0覆盖war工程

cas 官方给我们提供了基础的工程只需要我们在此基础上进行修改就能实现我们想要的功能

git clone https://github.com/apereo/cas-overlay-template.git

二、扩展步骤

1.在build.gradle文件中引入库

引入位置看下图
引包示例

代码如下(示例):

implementation "org.apereo.cas:cas-server-core-api-configuration-model"
implementation "org.apereo.cas:cas-server-webapp-init"
implementation "org.apereo.cas:cas-server-support-jdbc-drivers"
implementation "org.apereo.cas:cas-server-support-jdbc"
implementation "org.apereo.cas:cas-server-core-web-api"
implementation "org.apereo.cas:cas-server-core-authentication-mfa-api"
implementation "org.apereo.cas:cas-server-core-webflow-mfa-api"
implementation "org.apereo.cas:cas-server-core-webflow-api"
implementation "org.apereo.cas:cas-server-support-jpa-util"
implementation "org.apereo.cas:cas-server-support-json-service-registry"
implementation "org.apereo.cas:cas-server-core-webflow"
implementation "org.apereo.cas:cas-server-core-authentication"
implementation "org.apereo.cas:cas-server-core-authentication-api"

2.修改cas默认登录自定义数据源以及返回自定义返回属性

创建UsernamePasswordAuthentication并继承AbstractUsernamePasswordAuthenticationHandler

代码如下(示例):

package com.wangda.cas.auth;


import com.google.common.collect.Lists;
import com.wangda.cas.domain.User;
import org.apereo.cas.authentication.AuthenticationHandlerExecutionResult;
import org.apereo.cas.authentication.PreventedException;
import org.apereo.cas.authentication.credential.UsernamePasswordCredential;
import org.apereo.cas.authentication.exceptions.AccountPasswordMustChangeException;
import org.apereo.cas.authentication.handler.support.AbstractUsernamePasswordAuthenticationHandler;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.services.ServicesManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.security.auth.login.FailedLoginException;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.List;

public class UsernamePasswordAuthentication extends AbstractUsernamePasswordAuthenticationHandler {
    private final Logger logger = LoggerFactory.getLogger(UsernamePasswordAuthentication.class);
    private final JdbcTemplate jdbcTemplate;

    private PasswordEncoder passwordEncoder;

    public UsernamePasswordAuthentication(String name, ServicesManager servicesManager, PrincipalFactory principalFactory, Integer order, JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) {
        super(name, servicesManager, principalFactory, order);

        this.jdbcTemplate = jdbcTemplate;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    protected AuthenticationHandlerExecutionResult authenticateUsernamePasswordInternal(UsernamePasswordCredential credential, String originalPassword) throws GeneralSecurityException, PreventedException {
        String username = credential.getUsername();
        String password = new String(credential.getPassword());
        // 这里修改成你自己的数据源
        User user = jdbcTemplate.queryForObject("select * from xxx where mobile=?", new BeanPropertyRowMapper<>(User.class), username);
        boolean matches = passwordEncoder.matches(password, user.getPassword());
        if (!matches) {
            logger.warn(credential.getUsername() + ":用户密码认证失败");
            throw new FailedLoginException("账号密码错误");
        } else {
            if (!validateStrength(password)) {
            // 抛出这个异常后会让cas进入casMustChangePassView.html
                throw new AccountPasswordMustChangeException();
            }
        }
        // 增加自定义属性
        HashMap<String, List<Object>> payload = new HashMap<>(3);
        payload.put("name", Lists.newArrayList(user.getDisplayName()));
        payload.put("mobile", Lists.newArrayList(user.getMobile()));
        payload.put("sub", Lists.newArrayListWithCapacity(0));
        payload.put("externalId", Lists.newArrayList(user.getExternalId()));
        logger.info(credential.getUsername() + ":用户密码认证登录成功");
        return createHandlerResult(credential, this.principalFactory.createPrincipal(user.getId().toString(), payload));
    }

    private boolean validateStrength(String pwd) {
        int flag = 0;
        if (pwd.matches(".*[0-9].*")) {
            flag++;
        }
        if (pwd.matches(".*[a-z].*")) {
            flag++;
        }
        if (pwd.matches(".*[A-Z].*")) {
            flag++;
        }
        if (pwd.matches(".*[!@*#$\\-_()+=&¥].*")) {
            flag++;
        }
        return flag >= 2;
    }
}

请注意在代码中更换你自己的数据源


接着找到 cas提供给我们的配置类CasOverlayOverrideConfiguration进行bean注册

 @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }

        @Bean
        public AuthenticationHandler passwordAuthenticationHandler(JdbcTemplate jdbcTemplate,
                                                                   PasswordEncoder passwordEncoder,
                                                                   @Qualifier("servicesManager") 
                                                                   ServicesManager servicesManager) {
            return new UsernamePasswordAuthentication(PasswordAuthentication.class.getName(), servicesManager, new DefaultPrincipalFactory(), 1, jdbcTemplate, passwordEncoder);
        }

        /**
         *
         * @param passwdlessAuthenticationHandler 增加第二种登录方式
         * @param passwordAuthenticationHandler 修改cas默认登录认证方式
         * @return
         */
        @Bean
        public AuthenticationEventExecutionPlanConfigurer myPlan(
                @Qualifier("passwdlessAuthenticationHandler") final AuthenticationHandler passwdlessAuthenticationHandler,
                @Qualifier("passwordAuthenticationHandler") final AuthenticationHandler passwordAuthenticationHandler) {
            return plan -> {
                plan.registerAuthenticationHandler(passwdlessAuthenticationHandler);
                plan.registerAuthenticationHandler(passwordAuthenticationHandler);
            };
        }

三、增加第二种自定义登录方式

  1. 增加自定义认证Credential

import org.apereo.cas.authentication.credential.OneTimeTokenCredential;

;
public class PasswdlessCredential extends OneTimeTokenCredential {
    private String token;
    private String id;

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    @Override
    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
    
}
  1. 增加webflow配置
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.web.flow.CasWebflowConstants;
import org.apereo.cas.web.flow.configurer.AbstractCasMultifactorWebflowConfigurer;
import org.apereo.cas.web.flow.configurer.CasMultifactorWebflowCustomizer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.webflow.definition.registry.FlowDefinitionRegistry;
import org.springframework.webflow.engine.ActionState;
import org.springframework.webflow.engine.Flow;
import org.springframework.webflow.engine.TransitionSet;
import org.springframework.webflow.engine.builder.support.FlowBuilderServices;

import java.util.List;
import java.util.Optional;

public class PassdlessWebflowConfigurer extends AbstractCasMultifactorWebflowConfigurer {

    public static final String LESSACTION = "passdlessWebflowAction";
    public static final String MFA_PASSDLESS_EVENT_ID="passdlessWebflowAction";

    public PassdlessWebflowConfigurer(final FlowBuilderServices flowBuilderServices,
                                      final FlowDefinitionRegistry loginFlowDefinitionRegistry,
                                      final FlowDefinitionRegistry flowDefinitionRegistry,
                                      final ConfigurableApplicationContext applicationContext,
                                      final CasConfigurationProperties casProperties,
                                      final List<CasMultifactorWebflowCustomizer> mfaFlowCustomizers) {
        super(flowBuilderServices, loginFlowDefinitionRegistry, applicationContext,
                casProperties, Optional.of(flowDefinitionRegistry), mfaFlowCustomizers);
    }

    @Override
    protected void doInitialize() {
        Flow loginFlow = getLoginFlow();
        createClientActionActionState(loginFlow);

    }

    private void createClientActionActionState(final Flow flow) {


        final ActionState actionState = createActionState(flow, LESSACTION, createEvaluateAction(LESSACTION));
        final TransitionSet transitionSet = actionState.getTransitionSet();
        transitionSet.add(createTransition(CasWebflowConstants.TRANSITION_ID_SUCCESS, CasWebflowConstants.STATE_ID_CREATE_TICKET_GRANTING_TICKET));
        transitionSet.add(createTransition(CasWebflowConstants.TRANSITION_ID_ERROR, getStartState(flow).getId()));
        transitionSet.add(createTransition(CasWebflowConstants.TRANSITION_ID_RESUME, getStartState(flow).getId()));
        transitionSet.add(createTransition(CasWebflowConstants.TRANSITION_ID_AUTHENTICATION_FAILURE, CasWebflowConstants.STATE_ID_HANDLE_AUTHN_FAILURE));
        transitionSet.add(createTransition(CasWebflowConstants.TRANSITION_ID_WARN, CasWebflowConstants.STATE_ID_WARN));
        setStartState(flow, actionState);

    }


}
  1. 增加处理类

import com.google.common.collect.Lists;
import com.idsmanager.dingdang.jwt.DingdangUserRetriever;
import com.idsmanager.dingdang.jwt.DingdangUserRetriever.User;
import org.apereo.cas.authentication.*;
import org.apereo.cas.authentication.credential.UsernamePasswordCredential;
import org.apereo.cas.authentication.handler.support.AbstractPreAndPostProcessingAuthenticationHandler;
import org.apereo.cas.authentication.handler.support.AbstractUsernamePasswordAuthenticationHandler;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.authentication.principal.Service;
import org.apereo.cas.services.ServicesManager;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;

import javax.security.auth.login.FailedLoginException;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.List;

public class PasswdlessHandler extends AbstractPreAndPostProcessingAuthenticationHandler {

    @Value("${jwt.publicKey}")
    private String publicKey;


    public PasswdlessHandler(String name, ServicesManager servicesManager, PrincipalFactory principalFactory, Integer order, LoginHandle handle) {
        super(name, servicesManager, principalFactory, order);
        this.handle = handle;
    }

    @Override
    protected AuthenticationHandlerExecutionResult doAuthentication(Credential credential, Service service) throws Exception {


        PasswdlessCredential token = PasswdlessCredential.class.cast(credential);
        DingdangUserRetriever retrieve = new DingdangUserRetriever(token.getToken(), publicKey);
        try {
            User user = retrieve.retrieve();
            if (user != null) {
                com.wangda.cas.domain.User sysUser = //获取用户
                HashMap<String, List<Object>> payload = new HashMap<>(4);
                token.setId(user.getExternalId());
                payload.put("name", Lists.newArrayList(sysUser.getDisplayName()));
                payload.put("mobile", Lists.newArrayList(sysUser.getMobile()));
                payload.put("externalId", Lists.newArrayList(sysUser.getExternalId()));
                return createHandlerResult(credential, this.principalFactory.
                        createPrincipal(sysUser.getId().toString(), payload));
            }
        } catch (Exception e) {
            throw new FailedLoginException(e.getMessage());
        }
        throw new FailedLoginException();
    }


    public boolean supports(final Credential credential) {
        return credential != null && PasswdlessCredential.class.isAssignableFrom(credential.getClass());
    }
    
}

  1. 增加 Action类
import com.wangda.cas.auth.PasswdlessCredential;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.apereo.cas.authentication.adaptive.AdaptiveAuthenticationPolicy;
import org.apereo.cas.web.flow.actions.AbstractAuthenticationAction;
import org.apereo.cas.web.flow.resolver.CasDelegatingWebflowEventResolver;
import org.apereo.cas.web.flow.resolver.CasWebflowEventResolver;
import org.apereo.cas.web.support.WebUtils;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;

public class PasswdlessAction extends AbstractAuthenticationAction {

    public PasswdlessAction(CasDelegatingWebflowEventResolver initialAuthenticationAttemptWebflowEventResolver, CasWebflowEventResolver serviceTicketRequestWebflowEventResolver,
            AdaptiveAuthenticationPolicy adaptiveAuthenticationPolicy) {
        super(initialAuthenticationAttemptWebflowEventResolver, serviceTicketRequestWebflowEventResolver, adaptiveAuthenticationPolicy);
    }

    @Override
    public Event doExecute(final RequestContext context) {
		// 这里可以改成任何你想获取的属性方式
        final HttpServletRequest request = WebUtils.getHttpServletRequestFromExternalWebflowContext(context);
        String token = request.getParameter("id_token");
        if (StringUtils.isEmpty(token)) {
            return error();
        }
        String client_name = request.getParameter("client_name");
        // 这里是关键这个credential会触发我们自己定义的认证处理类
        PasswdlessCredential clientCredential = new PasswdlessCredential();
        clientCredential.setId(client_name);
        clientCredential.setToken(token);
        WebUtils.putCredential(context, clientCredential);
        return super.doExecute(context);
    }

}
  1. 向spring中注册我们上面的bean为了职责分明我们这里创建一个新的配置类
    提示 在工程resource/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中增加我们下面的配置类要不然spring扫描不到这个是SpringBoot3中的新特性
@AutoConfiguration
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class CasZzdConfiguration {
 @ConditionalOnMissingBean(name = "passdlessWebflowConfigurer")
    @Bean
    public CasWebflowConfigurer passdlessWebflowConfigurer(
            final CasConfigurationProperties casProperties,
            final ConfigurableApplicationContext applicationContext,
            @Qualifier("passdlessFlowRegistry") final FlowDefinitionRegistry passdlessFlowRegistry,
            @Qualifier(CasWebflowConstants.BEAN_NAME_LOGIN_FLOW_DEFINITION_REGISTRY) final FlowDefinitionRegistry loginFlowDefinitionRegistry,
            @Qualifier(CasWebflowConstants.BEAN_NAME_FLOW_BUILDER_SERVICES) final FlowBuilderServices flowBuilderServices) {
        val cfg = new PassdlessWebflowConfigurer(flowBuilderServices,
                loginFlowDefinitionRegistry, passdlessFlowRegistry, applicationContext, casProperties,
                MultifactorAuthenticationWebflowUtils.getMultifactorAuthenticationWebflowCustomizers(applicationContext));
        cfg.setOrder(100);
        return cfg;
    }
     @Bean
    @ConditionalOnMissingBean(name = "passdlessFlowRegistry")
    public FlowDefinitionRegistry passdlessFlowRegistry(
            final ConfigurableApplicationContext applicationContext,
            @Qualifier(CasWebflowConstants.BEAN_NAME_FLOW_BUILDER_SERVICES) final FlowBuilderServices flowBuilderServices,
            @Qualifier(CasWebflowConstants.BEAN_NAME_FLOW_BUILDER) final FlowBuilder flowBuilder) {

        val builder = new FlowDefinitionRegistryBuilder(applicationContext, flowBuilderServices);
        builder.addFlowBuilder(flowBuilder, PassdlessWebflowConfigurer.MFA_PASSDLESS_EVENT_ID);
        return builder.build();
    }

    @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
    @Bean
    public Action passdlessWebflowAction(
            final CasConfigurationProperties casProperties,
            final ConfigurableApplicationContext applicationContext,
            @Qualifier("adaptiveAuthenticationPolicy") final AdaptiveAuthenticationPolicy adaptiveAuthenticationPolicy,
            @Qualifier("serviceTicketRequestWebflowEventResolver") final CasWebflowEventResolver serviceTicketRequestWebflowEventResolver,
            @Qualifier("initialAuthenticationAttemptWebflowEventResolver") final CasDelegatingWebflowEventResolver initialAuthenticationAttemptWebflowEventResolver) {

        return WebflowActionBeanSupplier.builder()
                .withApplicationContext(applicationContext)
                .withProperties(casProperties)
                .withAction(() -> new PasswdlessAction(initialAuthenticationAttemptWebflowEventResolver,
                        serviceTicketRequestWebflowEventResolver,
                        adaptiveAuthenticationPolicy)).build().get();

    }
    @Bean
    @ConditionalOnMissingBean(name = "passdlessCasWebflowExecutionPlanConfigurer")
    public CasWebflowExecutionPlanConfigurer passdlessCasWebflowExecutionPlanConfigurer(
            @Qualifier("passdlessWebflowConfigurer") final CasWebflowConfigurer passdlessWebflowConfigurer) {
        return plan -> plan.registerWebflowConfigurer(passdlessWebflowConfigurer);
    }
}

接着启动服务如何启动如何Debug请查阅读工程中README.md
6. 测试我们刚自定义的登录方式

在浏览器中访问 https://localhost:8443/cas/login?id_token=42343243&client_name=rerew
cas就会进入我们自定义PasswdlessHandler类中的doAuthentication认证方法中
基于这个方法很容易实现微信扫码 钉钉扫码登录

  1. 测试cas默认登录

在浏览器中访问 https://localhost:8443/cas/login
输入账号密码后点击登录后cas会进入 UsernamePasswordAuthentication类中的 authenticateUsernamePasswordInternal方法

修改cas默认登录页面

./gradlew[.bat] listTemplateViews

查看所有可以覆盖的模版
在resource/templates文件夹中创建上面列出来的文件可以实现覆盖例如修改login页面
在templates下创建login/casLoginView.html
在里面添加内容就可以了或者参考看我之前的文章

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值