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

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

密码明文传输

问题描述

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

解决思路

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

代码实现

前端

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

  <form id = "fm1">
    <input id="username" name="username" class="input_user_name"
                           tabindex="1" placeholder="请输入用户名称" accesskey="n" type="text" value=""
                           maxlength="30" autocomplete="false">
    <input id="password" name="" class="input_password" tabindex="2"
                           placeholder="请输入登录密码" accesskey="p" type="password" value=""
                           maxlength="28" autocomplete="off">
     <input id="login_normal1" class="login-button" name="submit"
               accesskey="l"
               value="登 录" tabindex="3" type="button">
     <input id="login_normal" style="display: none"
               name="submit" accesskey="l"
               value="登 录" tabindex="3" type="submit">
</form>

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

<script src="common/js/jsencrypt.min.js" type="text/javascript"></script>

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

<script type="text/javascript">
    $(document).ready(function(){
        if (window.top.location !== self.location) {
            top.location.replace(self.location);
        }
        $("#login_normal1").click( function() {
            if(!checkSubmit()){
                return
            }
            // 登陆验证之前,对密码进行加密处理
            const password = encrypt($('#password').val())
            $('#login_normal')
                .attr('name', "password")
                .attr('value', password)
            $('#login_normal').click()

        });
    });
    function encrypt(password) {
        var encrypt = new JSEncrypt()
          // 此处需要填入自己生成的密钥。
        encrypt.setPublicKey(``);
        return encrypt.encrypt(password);
    }
    function checkSubmit() {
        var username = $("#username").val().trim();
        var password = $("#password").val().trim();

        if (username == ''||username==null) {
            $('#username').focus();
            $('#msg1').html('请输入用户名!');
            return false;
        }
        if (password == ''||password==null) {
            $('#password').focus();
            $('#msg1').html('请输入密码!');
            return false;
        }
        return true;

    }
}
</script>

后端

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

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或其他依赖;

<dependency>
   <groupId>com.github.whvcse</groupId>
    <artifactId>easy-captcha</artifactId>
    <version>1.6.2</version>
</dependency>

接口暴露:

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<String> 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、对前端登录页面稍加改造;可以进行样式的自定义适配。

 <div class="input-p captcha">
   <div class="input__prepend captcha"></div>
    <input id="captcha" name=""
           class="captcha" tabindex="3"
           placeholder="请输入验证码" accesskey="p"
           maxlength="4" autocomplete="off">
    <img id="cimg"
         src=""
         title="看不清?点击更换另一个。" />
</div>

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

<script type="text/javascript">
    $(document).ready(function(){
        if (window.top.location !== self.location) {
            top.location.replace(self.location);
        }
        $("#login_normal1").click( function() {
            if(!checkSubmit()){
                return
            }
            // 验证码验证失败
            if(!validateCaptcha()){
                return;
            }
            // 登陆验证之前,对密码进行加密处理
            const password = encrypt($('#password').val())
            $('#login_normal')
                .attr('name', "password")
                .attr('value', password)
            $('#login_normal').click()

        });

        $("#cimg").click(function(){
            initCaptcha()
        })
        initCaptcha();
    });

    //
    function initCaptcha(){
        var _codeImage = $('#cimg');
        var rand = Math.random();
        var url = '/captcha/code?rand=' + rand;
        _codeImage.attr("src", url);
    }
    // 对验证码进行验证
    function validateCaptcha(){
        var isValid = false
        $.ajax({
            url: '/captcha/check?code=' + $('#captcha').val(),
            type: 'GET',
            async:false,
            success: function(data) {
                if (data) {
                    if(data === 'ok'){
                        isValid =  true
                    }else {
                        $('#msg1').html('验证码输入错误,请重新输入!');
                        //密码验证失败后,重新请求验证码
                        initCaptcha()
                        isValid =  false
                    }
                }
            }
        })
        return isValid
    }
    function checkSubmit() {
        var username = $("#username").val().trim();
        var password = $("#password").val().trim();
        var captcha = $("#captcha").val().trim();
        if (username > '' && password > '' && captcha > '') {
            $('#msg1').html("");
            return true;
        }
        else {
            if(!username || !password){
                $('#msg1').html('请输入您的用户名和密码');
            }else {
                $('#msg1').html('请输入验证码');
            }

            return false;
        }
    }
</script>

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

开放重定向问题

开放重定向问题的定义: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代码,降低其他服务的改造成本。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容