分两篇:
- 第一篇:了解加密加密的基本概念
- 第二篇:实践前后台的加密解密流程
目标
分几种场景来介绍前后端加密解密的方式
前端加密
从新浪微博登陆-看前端加密方式
新浪微博的登陆,分析登陆接口,会发现你输入的账号和密码参数是找不到的,取而代之的是su和sp参数,su是加密后的账号,sp参数是加密后的密码。su参数是通过Base64处理的,sp是通过servicetime(时间戳),nonce(一次性参数),rsaPubkey(RSA公钥) 处理加密而成。分析的具体流程看上面参考链接即可。
从上面可以得知,密码的加密是通过RSA非对称加密,RSA公钥加密得到。提取RSA加密的js:
RSA公钥加密,只能保证数据被其他人获取,但是不能保证篡改。在第一篇提到,前端加密的意义,一部分是保证用户输入的真实数据,不会被中间人获取。中间人即使获得sp,依然无法得知真正的密码是什么,防止信息泄露,防止撞库。
加密流程js:
这里会用到 crypto-js 来进行base64 的转换,当然可以使用其他js代替。
参考:https://github.com/gengzi/codecopy/tree/master/src/main/resources/js/securityInterface
encrypt.js
用户名:
function getusername(username) {
var str = CryptoJS.enc.Utf8.parse(username);
return CryptoJS.enc.Base64.stringify(str);
}
密码:
function getpassword(pwd, servicetime, nonce, rsaPubkey) {
var RSAKey = new sinaSSOEncoder.RSAKey();
RSAKey.setPublic(rsaPubkey, '10001');
var password = RSAKey.encrypt([servicetime, nonce].join('\t') + '\n' + pwd);
return password;
}
代码实践
思路:通过java调用js的方式来实践,对于前端参数的加密,后端解密数据
(1)编写加密js,使用java调用js加密处理,调用后端接口。
(2)后端接口接收参数,解密处理,拿出原始数据。
环境:java8
代码参考:https://github.com/gengzi/codecopy/tree/master/src/main/resources/js/
crypto-js.js 、base.js 、encrypt.js 、paramCrypot.js
java代码:fun.gengzi.codecopy.business.authentication 下的代码
第一步:
/**
* java 调用js 进行加密
*/
@Override
public void paramEncryptionToJs() {
ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
ScriptEngine nashorn = scriptEngineManager.getEngineByName("nashorn");
try {
String basePath = ClassLoader.getSystemClassLoader().getResource("js/base.js").getPath();
String jsencryptPath = ClassLoader.getSystemClassLoader().getResource("js/jsencrypt/jsencrypt.js").getPath();
String cryptoPath = ClassLoader.getSystemClassLoader().getResource("js/crypto-js-4.0.0/crypto-js.js").getPath();
String encryptPath = ClassLoader.getSystemClassLoader().getResource("js/securityInterface/encrypt.js").getPath();
String paramCrypotPath = ClassLoader.getSystemClassLoader().getResource("js/securityInterface/paramCrypot.js").getPath();
nashorn.eval(Files.newBufferedReader(Paths.get(basePath.substring(1))));
nashorn.eval(Files.newBufferedReader(Paths.get(cryptoPath.substring(1))));
nashorn.eval(Files.newBufferedReader(Paths.get(jsencryptPath.substring(1))));
nashorn.eval(Files.newBufferedReader(Paths.get(encryptPath.substring(1))));
nashorn.eval(Files.newBufferedReader(Paths.get(paramCrypotPath.substring(1))));
Invocable in = (Invocable) nashorn;
String username = "16636663456";
String password = "gengzi666";
Object o = in.invokeFunction("requestParamCrypotByJsencrypt", username, password);
logger.info("加密后的参数数据 : {} ", o);
// 发送请求
if (StringUtils.isNoneBlank(o.toString())) {
// 发送请求
String body = HttpRequest.post(SecurityInterfaceConstans.PARAMENCRYPTIONURL)
.body(o.toString()).execute().body();
}
} catch (ScriptException | IOException | NoSuchMethodException e) {
e.printStackTrace();
}
}
第二步:
/**
* 其实前端对参数的加密,来保证安全,就好像是一个纸老虎。但是依然能提高接口的安全程度。
* <p>
* 敏感数据加密后,攻击者无法仅通过网络抓包来详细了解敏感数据的内容。
* 为了让这个纸老虎更加逼真,对前端js 混淆,添加一些无效参数,记录用户行为,将这些组合在一起,提升攻击者的难度。
* 可能攻击者分析分析着,就放弃了。
* <p>
* 该接口演示,使用微博登陆的加密js,对用户名和密码进行加密处理,并提交至后台。
*
* @return
*/
@ApiOperation(value = "前端-后端:提交参数的加密与解密", notes = "前端-后端:提交参数的加密与解密" +
"演示后台解密")
@ApiImplicitParams({
@ApiImplicitParam(name = "ParamEncryptionEntity", value = "ParamEncryptionEntity", required = true)})
@ApiResponses({@ApiResponse(code = 200, message = "\t{\n" +
"\t \"status\": 200,\n" +
"\t \"info\": {\n" +
"\t }\n" +
"\t \"message\": \"success\",\n" +
"\t}\n")})
@PostMapping("/paramEncryption")
@ResponseBody
public ReturnData paramEncryption(@RequestBody ParamEncryptionEntity paramEncryptionEntity) {
// 编写加密js 的function 方法
// java 调用这些js 方法,执行加密操作,发送ajax 请求
// 前端js混淆,引入额外参数,替换页面的原属性值
// 后台接受 ajax 请求,获取参数,将参数解密处理,执行具体业务
logger.info("paramEncryptionEntity : {}", paramEncryptionEntity.toString());
String nonce = paramEncryptionEntity.getNonce();
String su = Base64.decodeStr(paramEncryptionEntity.getSu());
logger.info("su : {}", su);
// 跟 nonce 获取 publickey 省略
Optional<String> sp = RSAUtils.decryptByPublic(paramEncryptionEntity.getSp(), secretkey);
logger.info("sp : {}", sp.orElse(""));
// 比对参数
String signStr = sp.orElse("");
String password = null;
if (signStr.contains(paramEncryptionEntity.getServiceTime() + "\t" + paramEncryptionEntity.getNonce() + "\n")) {
String[] split = signStr.split("\n");
password = split[split.length - 1];
}
//TODO 与数据库比对账号密码,一致登陆成功
logger.info("username :{} and password : {}", su, password);
ReturnData ret = ReturnData.newInstance();
ret.setSuccess();
return ret;
}
一些问题和解决方法
java调用js可能会提示:navigator is not defined 或者 window is not defined
参考:如果运行的环境不支持Windows和导航器,则不能使用这两个对象。
原因:java调用js,有些参数是浏览器支持的,所以会出现有些参数没有被定义
解决方法:在调用js前,先定义这些参数,并在加载在最前面。
var navigator = { appName: 'Netscape', userAgent: '', };
var window = {};
调用:
window.JSEncrypt = JSEncrypt;
window. // 需要这样调用
/**
* RSA 解密,使用密钥解密
* @param str 需要解密的内容
* @param key 密钥
* @returns {WordArray|PromiseLike<ArrayBuffer>|null|*|undefined}
*/
function decryptRSAByPrivateKey(str, key) {
Encrypt = new window.JSEncrypt();
Encrypt.setPrivateKey(key);
return Encrypt.decrypt(str);
}
问题2: 使用java调用js 应该不能发送http请求(ajax)
因为ajax 使用的是浏览器的一些对象元素