Java | 记录基于CAS登录模块的几个安全问题的解决

手头的项目的登录模块,基本都是集成了部门内封装出的基于CAS的中心鉴权组件,在安全扫描中暴露了一些问题,有些是因为没有合理的使用这一开源框架导致的,有的是通用的问题,在此记录问题和解决方案。
1、密码明文传输问题
2、页面无验证码、无登录防抖,易被暴力破解问题
3、开放重定向问题

密码明文传输

问题描述

用户输入的密码,虽然在页面的输入框中显示为“*****”,却在接口层面通过明文传输,易被抓包工具捕获。

解决思路

使用RSA非对称加密,前端对密码进行加密,后端解密后,再与数据库存储的凭证进行比对。

代码实现

前端

前端是在CAS项目中的casLoginView中进行改造,使用JavaScript (JQuery) + HTML + CSS;
1、 改造登录结构代码 - 将原有的登录表单中的按钮进行隐藏,增加一个用于点击的登录按钮;

  

注意,需要将原有的密码输入框input的name属性置为空字符串,或删去该属性,否则提交时会提交一个密文和一个明文。
2、引入用于加密的JS
下载JS,放在common/js目录下,并在页面引入。


3、登录逻辑改造
原先登录是触发了表单提交后,浏览器自带的post事件,将原有按钮进行隐藏,监听显示出来的登录按钮的点击事件。
可以使用回车监听方法,禁用原有回车登录方法,或也调用加密密码后提交的逻辑。


后端

后端仅需要在验证密码之前,对加密后的密码进行解密即可。
下面给出解密方法示例:

private String decrypt(String password) throws Exception {
 BASE64Decoder base64Decoder = new BASE64Decoder();
    byte[] keyByte = base64Decoder.decodeBuffer(");
    PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyByte);
    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    RSAPrivateKey privateKey = (RSAPrivateKey)keyFactory.generatePrivate(keySpec);
    byte[] dataByte = base64Decoder.decodeBuffer(password);
    Cipher cipher = Cipher.getInstance("RSA");
    cipher.init(Cipher.DECRYPT_MODE, privateKey);
    byte[] result = cipher.doFinal(dataByte);
    return new String(result);
}

添加验证码

后端改造

集成验证码,对于后端来说没什么难度。引入easy-captcha或其他依赖;


   com.github.whvcse
    easy-captcha
    1.6.2

接口暴露:

import com.wf.captcha.utils.CaptchaUtil;

@GetMapping("/capcha/code")
public void captchaCode(HttpServletRequest request,HttpServletResponse response) throws Exception {
    CaptchaUtil.out(request, response);
}

@GetMapping("/captcha/check")
public ResponseEntity captchaCode(@RequestParam String code, HttpServletRequest request) throws Exception {
  boolean success = false;
  if (CaptchaUtil.ver(code, request)) {
      success = true;
  }
  CaptchaUtil.clear(request);
  String successStr =  success ? "ok" : "error";
  System.out.println("验证码验证结果 = "  + successStr);
  return ResponseEntity.ok(successStr);
}


前端改造

增加了验证码的登陆页面

1、对前端登录页面稍加改造;可以进行样式的自定义适配。

 

2、增加进入页面后,请求验证码、校验验证码、点击更换验证码等交互逻辑


可以看到,在用户触发登录动作时,先校验了验证码是否合法,再去调用后台登录接口,这样可以一定程度上避免被暴力破解。

开放重定向问题

开放重定向问题的定义:https://www.wangan.com/articles/1132

简而言之,就是在我们服务的登录、登出地址中,将原本的服务地址${MY_SERVICE}替换成其他,也可以被CAS后端转发跳转。

http://${CAS}/cas/login?service=http://${MY_SERVICE}
http://${CAS}/cas/logout?service=http://${MY_SERVICE}

而经过排除和阅读CAS文档,发现是在我们配置认证客户端定义JSON时,将所有的serviceId都配成可以通配所有网址导致的!

{
  "@class" : "org.apereo.cas.services.RegexRegisteredService", 
  "serviceId" : "^(https|imaps|http)://.*",
  "name" : "",
  "id" : 1000,
  "description" : "",
  "evaluationOrder" : 1,
  "theme": ""
}

容易得出,serviceId的值是一个正则表达式,仅当能匹配到正则时,才会进行跳转,不然会显示出:

错误提示,无法进行重定向跳转

根据官网的建议,应该将serviceId配置得越精确越好,配置成具体的网址,就能避免重定向到其他网站的问题了。
那么问题又来了,在进行部署之前,我们可能并不知道这个网址。如果已经进行了代码打包,就改不了这个配好的网址了,有什么办法从外部数据源或配置文件中读取呢?这样更改了其他服务的部署地址,CAS不需要重新打包,如果可以读取到动态的数据源,CAS组件甚至不用重启。
查阅官网:https://apereo.github.io/cas/5.3.x/planning/Getting-Started.html
关于Service的管理中,我们可以看到多种存储方案:

存储方案

借助配置 + 内存管理方案,可以实现服务的动态配置。
给出我的实现代码:

    @Value("${supportServiceId}")
    private String supportServiceId;


    @Bean
    public List inMemoryRegisteredServices() {
        final List services = new ArrayList<>();
        final RegexRegisteredService service = new RegexRegisteredService();
        service.setServiceId(supportServiceId);
        service.setName("moss");
        service.setId(1L);
        service.setTheme("moss");
        service.setDescription("MOSS2.0语义化系统");
        service.setEvaluationOrder(1);
        services.add(service);
        return services;
    }

这样就可以从CAS的服务配置中读取,当然也可以配置一个服务列表。需要将原有的JSON配置删去。

小结

分享了几个改造的方法,需要在现有的框架下进行尽量小的改动,后续可以考虑提取成通用的JS代码,降低其他服务的改造成本。

你可能感兴趣的:(Java | 记录基于CAS登录模块的几个安全问题的解决)