CAS是由耶鲁大学开发的单点登录系统,其核心的知识点包括以下几个概念:
1) TGT: 票据,或称大令牌,在登录成功之后生成,其中包含了用户信息
2) TGC: TGT的key,TGT存储在session中,TGC以cookie形式保存在浏览器中,当再次访问CAS时,会根据TGC去查找对应的TGT
3) ST: 小令牌,由TGT生成,默认使用一次就失效
业务系统前后端分离情况下基于CAS完成单点登录的时序图如图所示:
假设业务系统域名为mail.test.com,cas服务的域名为cas.test.com
1) 在浏览器首先打开mail.test.com,由于cookie以及storage中都没有token信息,故跳转到CAS服务中,url为cas.test.com/login?service=mail.test.com
2) CAS服务返回登录页面,用户输入用户名密码进行登录
3) CAS校验用户名密码,校验成功,则根据用户信息生成TGT、TGC,并将TGC写入到浏览器的cookie中,同时根据TGT生成ST,再重定向到mail.test.com?ticket=ST
4) 浏览器再次接收到mail.test.com请求,由于此时携带了ticket,故调用后端服务接口
5) 后端服务收到ticket,调用cas.test.com/p3/seviceValidate校验ticket,若校验通过,则返回的校验信息中携带了用户信息;根据用户信息生成token返回给前端
6) 前端将token保存到浏览器的cookie或storage中
这里需要注意的时CAS只是进行身份认证,token的生成需要业务系统自己实现
假设此时另一个业务系统bussiness.test.com也进入了CAS,在浏览器首次打开bussiness.test.com时,由于当前域名下没有存储token,则也会跳转到CAS中,url为cas.test.com/login?service=bussiness.test.com;此时CAS域名下的cookie中存储了TGC,则CAS服务根据TGC可以查找到TGT,再根据TGT生成ST,重定向到bussiness.test.com?ticket=ST,后面的流程与前述相同。
CAS默认是通过用户名密码登录,在业务需要场景下,需要将登录方式修改为基于短信验证码登录,为此需要对CAS进行二次开发。
CAS源码中使用UsernamePasswordCredentail类来实现登录凭证,在此定义一个新的类来实现验证码登录凭证:
public class PhoneCaptchaCredential implements Credential {private static final long serialVersionUID = -1616013347177519641L;@Size(min = 1, message = "required.phone")private String phone;@Size(min = 1, message = "required.captcha")private String captcha;@Overridepublic String getId() {return this.phone;}@Generatedpublic PhoneCaptchaCredential() {}@Generatedpublic PhoneCaptchaCredential(String phone, String captcha) {this.phone = phone;this.captcha = captcha;}
}
重写DefaultLoginWebflowConfigurer.createRememberMeAuthnWebflowConfig的逻辑:
@Overrideprotected void createRememberMeAuthnWebflowConfig(Flow flow) {if (casProperties.getTicket().getTgt().getRememberMe().isEnabled()) {createFlowVariable(flow, CasWebflowConstants.VAR_ID_CREDENTIAL, PhoneCaptchaCredential.class);final ViewState state = getState(flow, CasWebflowConstants.STATE_ID_VIEW_LOGIN_FORM, ViewState.class);final BinderConfiguration cfg = getViewStateBinderConfiguration(state);cfg.addBinding(new BinderConfiguration.Binding("phone", null, false));cfg.addBinding(new BinderConfiguration.Binding("captcha", null, true));} else {createFlowVariable(flow, CasWebflowConstants.VAR_ID_CREDENTIAL, UsernamePasswordCredential.class);}}
在配置文件中需添加 cas.ticket.tgt.rememberMe.enabled=true
自定义handler完成认证流程:
public class CasAuthenticationHandler extends AbstractPreAndPostProcessingAuthenticationHandler {private static final org.slf4j.Logger logger = LoggerFactory.getLogger(CasAuthenticationHandler.class);public CasAuthenticationHandler(String name, ServicesManager servicesManager, PrincipalFactory principalFactory, Integer order) {super(name, servicesManager, principalFactory, order);}private AuthenticationHandlerExecutionResult doPhoneAuthentication(PhoneCaptchaCredential phoneCaptchaCredential, JdbcTemplate jdbcTemplate)throws GeneralSecurityException {String phone = phoneCaptchaCredential.getPhone();String captcha = phoneCaptchaCredential.getCaptcha();JedisPool pool = new JedisPool(new JedisPoolConfig(), "xx.xx.xx.xx", 6379);Jedis redis = pool.getResource();String redisCode = redis.get("CAS-" + phone);if (null == redisCode) {logger.error("验证码已过期");throw new AccountException("验证码已过期!");}if (!StringUtils.equals(redisCode, captcha)) {logger.error("验证码错误");throw new AccountException("验证码错误!");}String sql = "select * from cas_user where phone = ? and status = 0";User info = (User) jdbcTemplate.queryForObject(sql, new Object[]{phone}, new BeanPropertyRowMapper(User.class));if (info == null) {logger.error("用户不存在或账户已锁定");throw new AccountException("用户不存在或账户已锁定");}final List<MessageDescriptor> list = new ArrayList<>();/**可自定义返回给客户端的多个属性信息**/HashMap<String, Object> returnInfo = new HashMap<>();returnInfo.put("status", info.getStatus());returnInfo.put("deleted", info.getDeleted());returnInfo.put("name", info.getName());returnInfo.put("sex", info.getSex());returnInfo.put("age", info.getAge());return createHandlerResult(phoneCaptchaCredential,this.principalFactory.createPrincipal(info.getUsername(), returnInfo), list);}@Overrideprotected AuthenticationHandlerExecutionResult doAuthentication(Credential credential) throws GeneralSecurityException, PreventedException {// 先构建数据库驱动连接池DriverManagerDataSource dataSource = new DriverManagerDataSource();dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");dataSource.setUrl("jdbc:mysql://xx.xx.xx.xx:3306/test");dataSource.setUsername("xxxx");dataSource.setPassword("xxxxxx");return doPhoneAuthentication((PhoneCaptchaCredential) credential, jdbcTemplate);}@Overridepublic boolean supports(Credential credential) {return credential instanceof PhoneCaptchaCredential;}
}
最后定义AuthConfig:
@Configuration("MyAuthConfig")
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class MyAuthConfig implements AuthenticationEventExecutionPlanConfigurer {@Autowiredprivate CasConfigurationProperties casProperties;@Autowired@Qualifier("servicesManager")private ServicesManager servicesManager;@Autowired@Qualifier("loginFlowRegistry")private FlowDefinitionRegistry loginFlowRegistry;@Autowiredprivate ApplicationContext applicationContext;@Autowiredprivate FlowBuilderServices flowBuilderServices;@Beanpublic PrePostAuthenticationHandler myAuthenticationHandler() {return new CasAuthenticationHandler(CasAuthenticationHandler.class.getName(),servicesManager, new DefaultPrincipalFactory(), 1);}@Overridepublic void configureAuthenticationExecutionPlan(AuthenticationEventExecutionPlan plan) {plan.registerAuthenticationHandler(myAuthenticationHandler());}@Bean("defaultLoginWebflowConfigurer")public CasWebflowConfigurer defaultLoginWebflowConfigurer() {DefaultCaptchaWebflowConfigurer c = new DefaultCaptchaWebflowConfigurer(flowBuilderServices, loginFlowRegistry, applicationContext, casProperties);c.initialize();return c;}
}
修改templates中html文件,完成页面适配修改: