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);
};
}
三、增加第二种自定义登录方式
- 增加自定义认证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;
}
}
- 增加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);
}
}
- 增加处理类
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());
}
}
- 增加 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);
}
}
- 向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认证方法中
基于这个方法很容易实现微信扫码 钉钉扫码登录
- 测试cas默认登录
在浏览器中访问 https://localhost:8443/cas/login
输入账号密码后点击登录后cas会进入 UsernamePasswordAuthentication类中的 authenticateUsernamePasswordInternal方法
修改cas默认登录页面
./gradlew[.bat] listTemplateViews
查看所有可以覆盖的模版
在resource/templates文件夹中创建上面列出来的文件可以实现覆盖例如修改login页面
在templates下创建login/casLoginView.html
在里面添加内容就可以了或者参考看我之前的文章