手头的项目的登录模块,基本都是集成了部门内封装出的基于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代码,降低其他服务的改造成本。