知識點:
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();
}
}