手頭的項(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);
}
前端改造
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ì)顯示出:
根據(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ù)的改造成本谅年。