cas多方式登录相关知识点的总结
知识点:
cas多表单登录(在用户名,密码的基础上,增加短信验证码登录)
自定义认证策略
自定义字段添加为空校验的错误信息
Controller层接口的调用
一:场景
项目涉及到的业务是,在原cas用户名,密码登录的基础上,增加短信验证码登录和ca登录,在同事代码的基础上,加以修改添加功能,在这个过程中遇到了一些问题,下面针对验证码登录实现的思路和知识点加以总结
二:cas短信验证码实现涉及到的知识点
(1)实现思路
关于cas的概念,和搭建的一系列知识点的介绍,可参考博客 https://blog.csdn.net/Anumbrella (里面有一系列的教程)
由于在登录界面,提交的form表单对象,在源码里定义了为credential对象,自己打算再添加一个类似credential对象作为短信验证码登录的对象,发现代码不太好改,于是在credential实体类中加了一个 loginType (登录类型字段)作为区分不同表单登录的标识,然后结合自定义认证策略,给对象再加上phone(电话号码)和noteCode(短信验证码)两个字段,做为第二个form的属性,最后在自定义策略继承AbstractPreAndPostProcessingAuthenticationHandler抽象类的CustomerHandlerAuthentication对象的doAuthentication方法中,根据loginType判断登录方式,去不同字段,做逻辑处理。
(2)自定义认证策略(提交的信息不只用户名密码时)
当我们登录时,不仅需要验证cas框架自带的用户名密码验证,还需要验证其他邮箱,图片验证码(同一表单)、手机号,短信验证码(第二表单)时,需要我们自定义认证策略,自定义Cas的web认证流程。
a.引入依赖
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-core-authentication-api</artifactId>
<version>${cas.version}</version>
</dependency>
<!-- Custom Configuration -->
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-core-configuration-api</artifactId>
<version>${cas.version}</version>
</dependency>
b.新建CustomerHandlerAuthentication继承AbstractPreAndPostProcessingAuthenticationHandler
//自定义策略
public class CustomerHandlerAuthentication extends AbstractPreAndPostProcessingAuthenticationHandler {
private SnowFlakeWorker worker = new SnowFlakeWorker(0);
@Resource(name = "casDataSource")
private DataSource dataSource;
public CustomerHandlerAuthentication(String name, ServicesManager servicesManager, PrincipalFactory principalFactory, Integer order) {
super(name, servicesManager, principalFactory, order);
}
@Override
protected AuthenticationHandlerExecutionResult doAuthentication(Credential credential) throws GeneralSecurityException, PreventedException {
try {
CustomCredential logincredential = (CustomCredential) credential;
//获取登录类型标识,选择登录验证方式
String logintype= logincredential.getLogintype();
String username = logincredential.getUsername();
String password = logincredential.getPassword();
String phone = logincredential.getPhone();
String nodeCode = logincredential.getNoteCode();
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
//1.用户密码登录
if(logintype.equals("1")){
String sql = "SELECT * FROM base_user WHERE login_code = ? and logic_delete = 0";
BaseUser user = (BaseUser) jdbcTemplate.queryForObject(sql, new Object[]{username}, new BeanPropertyRowMapper(BaseUser.class));
if (user == null) {
throw new AccountException("用户不存在!");
}
if (!user.getLoginPassword().equals(SecureUtil.md5(String.valueOf(password)))) {
throw new FailedLoginException("用户名密码不正确!");
} else {
//将登录类型一个全局变量里
CasVariate.loginType=logintype;
//可自定义返回给客户端的多个属性信息
saveLoginLog(user.getUserId(), jdbcTemplate);
final List<MessageDescriptor> list = new ArrayList<>();
return createHandlerResult(logincredential,
this.principalFactory.createPrincipal(username), list);
}
//2.短信验证码登录
}else if(logintype.equals("2")){
String sql = "SELECT * FROM base_user WHERE mobliephone = ? and logic_delete = 0";
BaseUser user = (BaseUser) jdbcTemplate.queryForObject(sql, new Object[]{phone}, new BeanPropertyRowMapper(BaseUser.class));
if (user == null) {
throw new AccountException("改手机号尚未注册!");
}
//if (!user.getNoteCode().equals(nodeCode)) {
if (!CasVariate.nodeCodeMap.get(phone).equals(nodeCode)) {
throw new FailedLoginException("验证码填写错误!");
} else {
//将登录类型一个全局变量里
CasVariate.loginType=logintype;
//可自定义返回给客户端的多个属性信息
saveLoginLog(user.getUserId(), jdbcTemplate);
final List<MessageDescriptor> list = new ArrayList<>();
return createHandlerResult(logincredential,
this.principalFactory.createPrincipal(user.getLoginCode()), list);
}
}
//3.ca登录
} catch (Exception e) {
System.out.println(e.getMessage());
throw new AccountException("登录失败!");
}
return null;
}
@Override
public boolean supports(Credential credential) {
//判断传递过来的Credential 是否是自己能处理的类型
//return credential instanceof UsernamePasswordCredential;
return credential instanceof CustomCredential;//短信验证实体类
}
private void saveLoginLog(Long userId, JdbcTemplate jdbcTemplate) {
jdbcTemplate.update("INSERT INTO dc_j_login_log (log_id,user_id,login_time) VALUES(?, ?, ?)",
ps -> {
ps.setLong(1, worker.nextId());
ps.setLong(2, userId);
ps.setString(3, DateUtil.formatDateTime(new Date()));
});
}
}
c.把CustomAuthenticationConfiguration中myAuthenticationHandle中改为
@Configuration("CustomAuthenticationConfiguration")
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class CustomAuthenticationConfiguration implements AuthenticationEventExecutionPlanConfigurer, CasWebflowExecutionPlanConfigurer {
@Bean(name = "casDataSource")
@Qualifier("casDataSource")
@ConfigurationProperties(prefix="spring.datasource")
public DataSource getCasDataSource(){
return DataSourceBuilder.create().build();
}
@Autowired
private CasConfigurationProperties casProperties;
@Autowired
@Qualifier("servicesManager")
private ServicesManager servicesManager;
@Bean
public AuthenticationHandler myAuthenticationHandler() {
// 参数: name, servicesManager, principalFactory, order
// 定义为优先使用它进行认证
return new CustomerHandlerAuthentication(CustomerHandlerAuthentication.class.getName(),
servicesManager, new DefaultPrincipalFactory(), 1);
}
@Override
public void configureAuthenticationExecutionPlan(final AuthenticationEventExecutionPlan plan) {
plan.registerAuthenticationHandler(myAuthenticationHandler());
}
}
(3)自定义字段添加为空校验的错误信息(phone,noteCode为空校验错误信息)
在表单提交前在submit中的return 后面的方法中添加校验是可以的(saveLoginType())
以一种方式是更改webflow流程,先进行验证后,再经过我们自定义校验
a.在CustomWebflowConfigurer中更改bindCredential方法
public class CustomWebflowConfigurer extends AbstractCasWebflowConfigurer {
public CustomWebflowConfigurer(FlowBuilderServices flowBuilderServices, FlowDefinitionRegistry loginFlowDefinitionRegistry, ApplicationContext applicationContext, CasConfigurationProperties casProperties) {
super(flowBuilderServices, loginFlowDefinitionRegistry, applicationContext, casProperties);
}
@Override
protected void doInitialize() {
final Flow flow = super.getLoginFlow();
bindCredential(flow);
}
/**
* 绑定自定义的Credential信息
* @param flow
*/
protected void bindCredential(Flow flow) {
// 重写绑定自定义credential
createFlowVariable(flow, CasWebflowConstants.VAR_ID_CREDENTIAL, CustomCredential.class);
// 登录页绑定新参数
final ViewState state = (ViewState) flow.getState(CasWebflowConstants.STATE_ID_VIEW_LOGIN_FORM);
final BinderConfiguration cfg = getViewStateBinderConfiguration(state);
// 由于用户名以及密码已经绑定,所以只需对新加系统参数绑定即可
// 字段名,转换器,是否必须字段
cfg.addBinding(new BinderConfiguration.Binding("logintype", null, true));
cfg.addBinding(new BinderConfiguration.Binding("phone", null, false));
cfg.addBinding(new BinderConfiguration.Binding("noteCode", null, false));
final ActionState actionState = (ActionState) flow.getState(CasWebflowConstants.STATE_ID_REAL_SUBMIT);
final List<Action> currentActions = new ArrayList<>();
actionState.getActionList().forEach(currentActions::add);
currentActions.forEach(a -> actionState.getActionList().remove(a));
actionState.getActionList().add(createEvaluateAction("validateLoginAction"));
currentActions.forEach(a -> actionState.getActionList().add(a));
actionState.getTransitionSet().add(createTransition("phoneError", CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM));
actionState.getTransitionSet().add(createTransition("noteCodeError", CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM));
}
}
将phone和noteCode字段必填改为false,添加Webflow的流程,将原来的action备份一下,删掉,再将需要的action添加进去,同时将备份的action还原
b.新建ValidateLoginAction类,添加表单信息的验证
public class ValidateLoginAction extends AbstractAction {
private static final String PHONE_CODE = "phoneError";
private static final String NOTE__CODE = "noteCodeError";
@Override
protected Event doExecute(RequestContext requestContext) throws Exception {
CustomCredential credential = (CustomCredential) WebUtils.getCredential(requestContext);
if (credential instanceof CustomCredential) {
String logintype=credential.getLogintype();
String phone = credential.getPhone();
String noteCode = credential.getNoteCode();
//字段为空提交时
//将登录类型一个全局变量里
CasVariate.loginType=logintype;
//短信登录
if(logintype.equals("2")) {
//电话号码为空校验
if (phone.equals("") || phone == null) {
return getError(requestContext, PHONE_CODE);
}
//电话号码为空校验
if (noteCode.equals("") || noteCode == null) {
return getError(requestContext, NOTE__CODE);
}
}
}
return null;
}
/**
* 跳转到错误页
* @param requestContext
* @return
*/
private Event getError(final RequestContext requestContext, String CODE) {
final MessageContext messageContext = requestContext.getMessageContext();
messageContext.addMessage(new MessageBuilder().error().code(CODE).build());
return getEventFactorySupport().event(this, CODE);
}
}
c.将ValidateLoginAction类注入到CustomerAuthWebflowConfiguration中
//注入ValidateLoginAction
@Bean
@RefreshScope
@ConditionalOnMissingBean(name = "validateLoginAction")
public Action validateLoginAction() {
ValidateLoginAction validateCaptchaAction = new ValidateLoginAction();
return validateCaptchaAction;
}
d.将messages_zh_CN.properties拷贝一份,到resource下添加,(乱码时,可以在idea中设置一下)
#自定义为空异常
phoneError=手机号不能为空
noteCodeError=短信验证码不能为空
e.效果
(4)Controller层接口的调用(自定义访问控制器)
controller层
@Controller
@RequestMapping(value = "/note")
public class CustomController {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomController.class);
@Resource(name = "casDataSource")
private DataSource dataSource;
/**
* 保存手机验证码
* @return
*/
@RequestMapping(value = "/saveCode", method = { RequestMethod.GET, RequestMethod.POST })
@ResponseBody
public void saveNodeCode(String phone,String noteCode) {
//保存手机验证码到map中
CasVariate.nodeCodeMap.put(phone,noteCode);
}
}
新建CustomControllerConfiguration类,将CustomController注入bean到spring容器
//自定义控制器
@Configuration("CustomControllerConfiguration")
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class CustomControllerConfiguration {
// 注册bean到spring容器
@Bean
@ConditionalOnMissingBean(name="CustomController")
public CustomController customController(){
return new CustomController();
}
}