在上一節中我們使用了CAS的多種認證方式完成了多種方式的登錄認證,也就是我們主要是使用了CAS爲我們封裝的多種不同的認證方案,基本上能夠滿足我們多種需求的認證,如果還對不多的CAS認證方式不是很瞭解,可以先去複習一下原文——CAS單點登錄(三)——多種認證方式。
但是如果CAS框架提供的方案還是不能滿足我們的需要,比如我們不僅需要用戶名和密碼,還要驗證其他信息,比如郵箱,手機號,但是郵箱,手機信息在另一個數據庫,還有在一段時間內同一IP輸入錯誤次數限制等。這裏就需要我們自定義認證策略,自定義CAS的web認證流程。
自定義認證校驗策略
我們知道CAS爲我們提供了多種認證數據源,我們可以選擇JDBC、File、JSON等多種方式,但是如果我想在自己的認證方式中可以根據提交的信息實現不同數據源選擇,這種方式就需要我們去實現自定義認證。
自定義策略主要通過現實更改CAS配置,通過AuthenticationHandler在CAS中設計和註冊自定義身份驗證策略,攔截數據源達到目的。
主要分爲下面三個步驟:
- 設計自己的認證處理數據的程序
- 註冊認證攔截器到CAS的認證引擎中
- 更改認證配置到CAS中
首先我們還是添加需要的依賴庫:
<!-- Custom Authentication -->
<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>
如果我們認證的方式僅僅是傳統的用戶名和密碼,實現AbstractUsernamePasswordAuthenticationHandler這個抽象類就可以了,官方給的實例也是這個。
可以查看官方參考文檔:Configuring-Custom-Authentication。官方的實例有一個坑,給出的是5.2.x版本以前的例子,5.3.x版本後的jar包更改了,而且有個地方有坑,在5.2.x版本前的可以,新的5.3.x是不行的。
接着我們自定義我們自己的實現類CustomUsernamePasswordAuthentication,如下:
/**
* @author anumbrella
*/
public class CustomUsernamePasswordAuthentication extends AbstractUsernamePasswordAuthenticationHandler {
public CustomUsernamePasswordAuthentication(String name, ServicesManager servicesManager, PrincipalFactory principalFactory, Integer order) {
super(name, servicesManager, principalFactory, order);
}
@Override
protected AuthenticationHandlerExecutionResult authenticateUsernamePasswordInternal(UsernamePasswordCredential usernamePasswordCredential, String s) throws GeneralSecurityException, PreventedException {
String username = usernamePasswordCredential.getUsername();
String password = usernamePasswordCredential.getPassword();
System.out.println("username : " + username);
System.out.println("password : " + password);
// JDBC模板依賴於連接池來獲得數據的連接,所以必須先要構造連接池
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/cas");
dataSource.setUsername("root");
dataSource.setPassword("123");
// 創建JDBC模板
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
String sql = "SELECT * FROM user WHERE username = ?";
User info = (User) jdbcTemplate.queryForObject(sql, new Object[]{username}, new BeanPropertyRowMapper(User.class));
System.out.println("database username : "+ info.getUsername());
System.out.println("database password : "+ info.getPassword());
if (info == null) {
throw new AccountException("Sorry, username not found!");
}
if (!info.getPassword().equals(password)) {
throw new FailedLoginException("Sorry, password not correct!");
} else {
//可自定義返回給客戶端的多個屬性信息
HashMap<String, Object> returnInfo = new HashMap<>();
returnInfo.put("expired", info.getDisabled());
final List<MessageDescriptor> list = new ArrayList<>();
return createHandlerResult(usernamePasswordCredential,
this.principalFactory.createPrincipal(username, returnInfo), list);
}
}
}
這裏給出的與官方實例不同在兩個地方, 其一,返回的爲AuthenticationHandlerExecutionResult而不是HandlerResult,其實源碼是一樣的,在新版本重新命名了而已。第二點,createHandlerResult傳入的warings不能爲null,不然程序運行後提交信息始終無法認證成功!!!
代碼主要通過攔截傳入的Credential,獲取用戶名和密碼,然後再自定義返回給客戶端的用戶信息。這裏便可以通過代碼方式自定義返回給客戶端多個不同屬性信息。
接着我們注入配置信息,繼承AuthenticationEventExecutionPlanConfigurer。
/**
* @author anumbrella
*/
@Configuration("CustomAuthenticationConfiguration")
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class CustomAuthenticationConfiguration implements AuthenticationEventExecutionPlanConfigurer {
@Autowired
private CasConfigurationProperties casProperties;
@Autowired
@Qualifier("servicesManager")
private ServicesManager servicesManager;
@Bean
public AuthenticationHandler myAuthenticationHandler() {
// 參數: name, servicesManager, principalFactory, order
// 定義爲優先使用它進行認證
return new CustomUsernamePasswordAuthentication(CustomUsernamePasswordAuthentication.class.getName(),
servicesManager, new DefaultPrincipalFactory(), 1);
}
@Override
public void configureAuthenticationExecutionPlan(final AuthenticationEventExecutionPlan plan) {
plan.registerAuthenticationHandler(myAuthenticationHandler());
}
}
最後我們我們在src/main/resources目錄下新建META-INF目錄,同時在下面新建spring.factories文件,將配置指定爲我們自己新建的信息。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=net.anumbrella.sso.config.CustomAuthenticationConfiguration
數據庫還是原來的設計,如下:
啓動應用,輸入用戶名和密碼,查看控制檯我們打印的信息,可以發現我們從登陸頁面提交的數據以及從數據庫中查詢到的數據,匹配信息,登錄認證成功!!
從而現實了我們自定義用戶名和密碼的校驗,同時我們還可以選擇不同的數據源方式。
可能還有讀者提出疑問,我提交的信息不止用戶名和密碼,那該如何自定義認證?
這裏就要我們繼承AbstractPreAndPostProcessingAuthenticationHandler這個藉口,其實上面的AbstractUsernamePasswordAuthenticationHandler就是繼承實現的這個類,它只是用於簡單的用戶名和密碼的校驗。我們可以查看源碼,如下:
所以我們要自定義實現AbstractPreAndPostProcessingAuthenticationHandler接可以了。
比如這裏我新建CustomerHandlerAuthentication類,如下:
/**
* @author anumbrella
*/
public class CustomerHandlerAuthentication extends AbstractPreAndPostProcessingAuthenticationHandler {
public CustomerHandlerAuthentication(String name, ServicesManager servicesManager, PrincipalFactory principalFactory, Integer order) {
super(name, servicesManager, principalFactory, order);
}
@Override
public boolean supports(Credential credential) {
//判斷傳遞過來的Credential 是否是自己能處理的類型
return credential instanceof UsernamePasswordCredential;
}
@Override
protected AuthenticationHandlerExecutionResult doAuthentication(Credential credential) throws GeneralSecurityException, PreventedException {
UsernamePasswordCredential usernamePasswordCredentia = (UsernamePasswordCredential) credential;
String username = usernamePasswordCredentia.getUsername();
String password = usernamePasswordCredentia.getPassword();
System.out.println("username : " + username);
System.out.println("password : " + password);
// JDBC模板依賴於連接池來獲得數據的連接,所以必須先要構造連接池
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/cas");
dataSource.setUsername("root");
dataSource.setPassword("123");
// 創建JDBC模板
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
String sql = "SELECT * FROM user WHERE username = ?";
User info = (User) jdbcTemplate.queryForObject(sql, new Object[]{username}, new BeanPropertyRowMapper(User.class));
System.out.println("database username : "+ info.getUsername());
System.out.println("database password : "+ info.getPassword());
if (info == null) {
throw new AccountException("Sorry, username not found!");
}
if (!info.getPassword().equals(password)) {
throw new FailedLoginException("Sorry, password not correct!");
} else {
final List<MessageDescriptor> list = new ArrayList<>();
return createHandlerResult(usernamePasswordCredentia,
this.principalFactory.createPrincipal(username, Collections.emptyMap()), list);
}
}
}
這裏我只是簡單實現了用戶名和密碼的信息獲取,當有更多信息提交時,在轉換Credential時便可以拿到提交的信息。後面我會講解,這裏不明白沒關係。
接着我們在CustomAuthenticationConfiguration中將AuthenticationHandler更改爲CustomerHandlerAuthentication。
@Bean
public AuthenticationHandler myAuthenticationHandler() {
// 參數: name, servicesManager, principalFactory, order
// 定義爲優先使用它進行認證
return new CustomerHandlerAuthentication(CustomerHandlerAuthentication.class.getName(),
servicesManager, new DefaultPrincipalFactory(), 1);
}
啓動應用,可以發現跟先前能達到相同效果。
關於擴展用戶提交的自定義表單信息的知識將在下一節進行講解。
代碼實例:Chapter3