Java | 記錄基于CAS登錄模塊的幾個(gè)安全問題的解決

手頭的項(xiàng)目的登錄模塊,基本都是集成了部門內(nèi)封裝出的基于CAS的中心鑒權(quán)組件,在安全掃描中暴露了一些問題嗡载,有些是因?yàn)闆]有合理的使用這一開源框架導(dǎo)致的匾二,有的是通用的問題哮独,在此記錄問題和解決方案。
1察藐、密碼明文傳輸問題
2皮璧、頁面無驗(yàn)證碼、無登錄防抖分飞,易被暴力破解問題
3悴务、開放重定向問題

密碼明文傳輸

問題描述

用戶輸入的密碼,雖然在頁面的輸入框中顯示為“*****”浸须,卻在接口層面通過明文傳輸惨寿,易被抓包工具捕獲。

解決思路

使用RSA非對稱加密删窒,前端對密碼進(jìn)行加密裂垦,后端解密后,再與數(shù)據(jù)庫存儲的憑證進(jìn)行比對肌索。

代碼實(shí)現(xiàn)

前端

前端是在CAS項(xiàng)目中的casLoginView中進(jìn)行改造蕉拢,使用JavaScript (JQuery) + HTML + CSS;
1诚亚、 改造登錄結(jié)構(gòu)代碼 - 將原有的登錄表單中的按鈕進(jìn)行隱藏晕换,增加一個(gè)用于點(diǎn)擊的登錄按鈕;

  <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屬性置為空字符串闸准,或刪去該屬性,否則提交時(shí)會(huì)提交一個(gè)密文和一個(gè)明文梢灭。
2夷家、引入用于加密的JS
下載JS蒸其,放在common/js目錄下,并在頁面引入库快。

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

3摸袁、登錄邏輯改造
原先登錄是觸發(fā)了表單提交后,瀏覽器自帶的post事件义屏,將原有按鈕進(jìn)行隱藏靠汁,監(jiān)聽顯示出來的登錄按鈕的點(diǎn)擊事件。
可以使用回車監(jiān)聽方法闽铐,禁用原有回車登錄方法蝶怔,或也調(diào)用加密密碼后提交的邏輯。

<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
            }
            // 登陸驗(yàn)證之前阳啥,對密碼進(jìn)行加密處理
            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>

后端

后端僅需要在驗(yàn)證密碼之前斩狱,對加密后的密碼進(jìn)行解密即可。
下面給出解密方法示例:

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);
}

添加驗(yàn)證碼

后端改造

集成驗(yàn)證碼扎瓶,對于后端來說沒什么難度所踊。引入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("驗(yàn)證碼驗(yàn)證結(jié)果 = "  + successStr);
  return ResponseEntity.ok(successStr);
}


前端改造

增加了驗(yàn)證碼的登陸頁面

1概荷、對前端登錄頁面稍加改造秕岛;可以進(jìn)行樣式的自定義適配。

 <div class="input-p captcha">
   <div class="input__prepend captcha"></div>
    <input id="captcha" name=""
           class="captcha" tabindex="3"
           placeholder="請輸入驗(yàn)證碼" accesskey="p"
           maxlength="4" autocomplete="off">
    <img id="cimg"
         src=""
         title="看不清误证?點(diǎn)擊更換另一個(gè)继薛。" />
</div>

2、增加進(jìn)入頁面后愈捅,請求驗(yàn)證碼遏考、校驗(yàn)驗(yàn)證碼、點(diǎn)擊更換驗(yàn)證碼等交互邏輯

<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
            }
            // 驗(yàn)證碼驗(yàn)證失敗
            if(!validateCaptcha()){
                return;
            }
            // 登陸驗(yàn)證之前蓝谨,對密碼進(jìn)行加密處理
            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);
    }
    // 對驗(yàn)證碼進(jìn)行驗(yàn)證
    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('驗(yàn)證碼輸入錯(cuò)誤灌具,請重新輸入!');
                        //密碼驗(yàn)證失敗后譬巫,重新請求驗(yàn)證碼
                        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('請輸入驗(yàn)證碼');
            }

            return false;
        }
    }
</script>

可以看到咖楣,在用戶觸發(fā)登錄動(dòng)作時(shí),先校驗(yàn)了驗(yàn)證碼是否合法芦昔,再去調(diào)用后臺登錄接口诱贿,這樣可以一定程度上避免被暴力破解。

開放重定向問題

開放重定向問題的定義:https://www.wangan.com/articles/1132

簡而言之咕缎,就是在我們服務(wù)的登錄瘪松、登出地址中咸作,將原本的服務(wù)地址${MY_SERVICE}替換成其他锨阿,也可以被CAS后端轉(zhuǎn)發(fā)跳轉(zhuǎn)宵睦。

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

而經(jīng)過排除和閱讀CAS文檔,發(fā)現(xiàn)是在我們配置認(rèn)證客戶端定義JSON時(shí)墅诡,將所有的serviceId都配成可以通配所有網(wǎng)址導(dǎo)致的壳嚎!

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

容易得出,serviceId的值是一個(gè)正則表達(dá)式末早,僅當(dāng)能匹配到正則時(shí)烟馅,才會(huì)進(jìn)行跳轉(zhuǎn),不然會(huì)顯示出:

錯(cuò)誤提示然磷,無法進(jìn)行重定向跳轉(zhuǎn)

根據(jù)官網(wǎng)的建議郑趁,應(yīng)該將serviceId配置得越精確越好,配置成具體的網(wǎng)址姿搜,就能避免重定向到其他網(wǎng)站的問題了寡润。
那么問題又來了,在進(jìn)行部署之前舅柜,我們可能并不知道這個(gè)網(wǎng)址梭纹。如果已經(jīng)進(jìn)行了代碼打包,就改不了這個(gè)配好的網(wǎng)址了致份,有什么辦法從外部數(shù)據(jù)源或配置文件中讀取呢变抽?這樣更改了其他服務(wù)的部署地址,CAS不需要重新打包氮块,如果可以讀取到動(dòng)態(tài)的數(shù)據(jù)源绍载,CAS組件甚至不用重啟。
查閱官網(wǎng):https://apereo.github.io/cas/5.3.x/planning/Getting-Started.html
關(guān)于Service的管理中滔蝉,我們可以看到多種存儲方案:

存儲方案

借助配置 + 內(nèi)存管理方案击儡,可以實(shí)現(xiàn)服務(wù)的動(dòng)態(tài)配置。
給出我的實(shí)現(xiàn)代碼:

    @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語義化系統(tǒng)");
        service.setEvaluationOrder(1);
        services.add(service);
        return services;
    }

這樣就可以從CAS的服務(wù)配置中讀取锰提,當(dāng)然也可以配置一個(gè)服務(wù)列表曙痘。需要將原有的JSON配置刪去。

小結(jié)

分享了幾個(gè)改造的方法立肘,需要在現(xiàn)有的框架下進(jìn)行盡量小的改動(dòng)边坤,后續(xù)可以考慮提取成通用的JS代碼,降低其他服務(wù)的改造成本谅年。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末茧痒,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子融蹂,更是在濱河造成了極大的恐慌旺订,老刑警劉巖弄企,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異区拳,居然都是意外死亡拘领,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門樱调,熙熙樓的掌柜王于貴愁眉苦臉地迎上來约素,“玉大人,你說我怎么就攤上這事笆凌∈チ裕” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵乞而,是天一觀的道長送悔。 經(jīng)常有香客問我,道長爪模,這世上最難降的妖魔是什么欠啤? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮呻右,結(jié)果婚禮上跪妥,老公的妹妹穿的比我還像新娘。我一直安慰自己声滥,他們只是感情好眉撵,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著落塑,像睡著了一般纽疟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上憾赁,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天污朽,我揣著相機(jī)與錄音,去河邊找鬼龙考。 笑死蟆肆,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的晦款。 我是一名探鬼主播炎功,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼缓溅!你這毒婦竟也來了蛇损?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎淤齐,沒想到半個(gè)月后股囊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡更啄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年稚疹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锈死。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡贫堰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出待牵,到底是詐尸還是另有隱情,我是刑警寧澤喇勋,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布缨该,位于F島的核電站,受9級特大地震影響川背,放射性物質(zhì)發(fā)生泄漏贰拿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一熄云、第九天 我趴在偏房一處隱蔽的房頂上張望膨更。 院中可真熱鬧,春花似錦缴允、人聲如沸荚守。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽矗漾。三九已至,卻和暖如春薄料,著一層夾襖步出監(jiān)牢的瞬間敞贡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工摄职, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留誊役,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓谷市,卻偏偏與公主長得像蛔垢,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子歌懒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容