CAS单点登录原理及改造

      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;

    @Override
    public String getId() {
        return this.phone;
    }

    @Generated
    public PhoneCaptchaCredential() {

    }

    @Generated
    public PhoneCaptchaCredential(String phone, String captcha) {
        this.phone = phone;
        this.captcha = captcha;
    }
}

       重写DefaultLoginWebflowConfigurer.createRememberMeAuthnWebflowConfig的逻辑:

@Override
    protected 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);
    }

    @Override
    protected 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);
    }


    @Override
    public boolean supports(Credential credential) {
        return credential instanceof PhoneCaptchaCredential;
    }
}

     最后定义AuthConfig:

@Configuration("MyAuthConfig")
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class MyAuthConfig implements AuthenticationEventExecutionPlanConfigurer {

    @Autowired
    private CasConfigurationProperties casProperties;

    @Autowired
    @Qualifier("servicesManager")
    private ServicesManager servicesManager;

    @Autowired
    @Qualifier("loginFlowRegistry")
    private FlowDefinitionRegistry loginFlowRegistry;
    @Autowired
    private ApplicationContext applicationContext;
    @Autowired
    private FlowBuilderServices flowBuilderServices;

    @Bean
    public PrePostAuthenticationHandler myAuthenticationHandler() {
        return new CasAuthenticationHandler(CasAuthenticationHandler.class.getName(),
                servicesManager, new DefaultPrincipalFactory(), 1);
    }

    @Override
    public 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文件,完成页面适配修改:

  

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值