如果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 tongyao */ 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 tongyao */ @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 tongyao */ 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); }
启动应用,可以发现跟先前能达到相同效果。
关于扩展用户提交的自定义表单信息的可以查看我更多的文章。