该文档为转载内容:javascript
加密解密
1 前端js加密概述
2 先后端加密解密
21 引用的js加密库
22 js加密解密
23 Java端加密解密PKCS5Padding与js的Pkcs7一致
验证码
1 概述
2 验证码生成器
3 控制器使用验证码 如 CodeController
应用
1 loginhtml
2 Controller
4 实现思路html
对系统安全性要求比较高,那么须要选择https协议来传输数据。固然不少状况下通常的web网站,若是安全要求不是很高的话,用http协议就能够了。在这种状况下,密码的明文传输显然是不合适的,由于若是请求在传输过程当中被截了,就能够直接拿明文密码登陆网站了。
HTTPS(443)在HTTP(80)的基础上加入了SSL(Secure Sockets Layer 安全套接层)协议,SSL依靠证书来验证服务器的身份,并为浏览器和服务器之间的通讯加密。传输前用公钥加密,服务器端用私钥解密。前端
对于使用http协议的web前端的加密,只能防君子不能防小人。前端是彻底暴露的,包括你的加密算法。
知道了加密算法,密码都是能够破解的,只是时间问题。请看知乎上的一篇文章:对抗拖库java
因此加密是为了增长破解的时间成本,若是破解须要花费的时间让人难以接受,这也就达到了目的。web
而为了保证数据库中存储的密码更安全,则须要在后端用多种单向(非对称)加密手段混合进行加密存储。算法
前端加密后端又须要解密,因此须要对称加密算法,即前端使用 encrypted = encrypt(password+key),后端使用 password = decrypt(encrypted +key) ,前端只传输密码与key加密后的字符串encrypted ,这样即便请求被拦截了,也知道了加密算法,可是因为缺乏key因此很难破解出明文密码。因此这个key很关键。而这个key是由后端控制生成与销毁的,用完即失效,因此即便能够模拟用加密后的密码来发请求模拟登陆,可是key已经失效了,后端仍是验证不过的。数据库
注意,若是本地环境本就是不安全的,key被知道了,那就瞬间就能够用解密算法破解出密码了。这里只是假设传输的过程当中被截获的情形。因此前端加密是防不了小人的。若是真要防,能够将加密算法的js文件进行压缩加密,不断更新的手段来使js文件难以获取,让黑客难以获取加密算法。变态的google就是这么干的,本身实现一个js虚拟机,经过不断更新加密混淆js文件让加密算法难以获取。这样黑客不知道加密算法就没法破解了。apache
经常使用的对称加密算法有DES、3DES(TripleDES)、AES、RC二、RC四、RC5和Blowfis。能够参考:经常使用加密算法的Java实现总结后端
这里采用js端与java端互通的AES加密算法。数组
1.2 先后端加密解密
1.2.1 引用的js加密库
Cryptojs下载
1
2
3
1.2.2 js加密解密
var data = "888888";
var srcs = CryptoJS.enc.Utf8.parse(data);
var key = CryptoJS.enc.Utf8.parse('o7H8uIM2O5qv65l2');//Latin1 w8m31+Yy/Nw6thPsMpO5fg==
function Encrypt(word){
var srcs = CryptoJS.enc.Utf8.parse(word); var encrypted = CryptoJS.AES.encrypt(srcs, key, {mode:CryptoJS.mode.ECB,padding: CryptoJS.pad.Pkcs7}); return encrypted.toString();
}
function Decrypt(word){
var decrypt = CryptoJS.AES.decrypt(word, key, {mode:CryptoJS.mode.ECB,padding: CryptoJS.pad.Pkcs7}); return CryptoJS.enc.Utf8.stringify(decrypt).toString();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
这里key是页面加载的时候由服务器端生成的,用隐藏域保存。
1.2.3 Java端加密解密(PKCS5Padding与js的Pkcs7一致)
package com.jykj.demo.util;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import sun.misc.BASE64Decoder;
public class EncryptUtil {
private static final String KEY = "abcdefgabcdefg12";
private static final String ALGORITHMSTR = "AES/ECB/PKCS5Padding";
public static String base64Encode(byte[] bytes){
return Base64.encodeBase64String(bytes);
}
public static byte[] base64Decode(String base64Code) throws Exception{
return new BASE64Decoder().decodeBuffer(base64Code);
}
public static byte[] aesEncryptToBytes(String content, String encryptKey) throws Exception {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128);
Cipher cipher = Cipher.getInstance(ALGORITHMSTR);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), "AES"));
return cipher.doFinal(content.getBytes("utf-8")); } public static String aesEncrypt(String content, String encryptKey) throws Exception { return base64Encode(aesEncryptToBytes(content, encryptKey)); } public static String aesDecryptByBytes(byte[] encryptBytes, String decryptKey) throws Exception { KeyGenerator kgen = KeyGenerator.getInstance("AES"); kgen.init(128); Cipher cipher = Cipher.getInstance(ALGORITHMSTR); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), "AES")); byte[] decryptBytes = cipher.doFinal(encryptBytes); return new String(decryptBytes); } public static String aesDecrypt(String encryptStr, String decryptKey) throws Exception { return aesDecryptByBytes(base64Decode(encryptStr), decryptKey); } /** * 测试 * */ public static void main(String[] args) throws Exception { String content = "Test String么么哒"; //0gqIDaFNAAmwvv3tKsFOFf9P9m/6MWlmtB8SspgxqpWKYnELb/lXkyXm7P4sMf3e System.out.println("加密前:" + content); System.out.println("加密密钥和解密密钥:" + KEY); String encrypt = aesEncrypt(content, KEY); System.out.println(encrypt.length()+":加密后:" + encrypt); String decrypt = aesDecrypt(encrypt, KEY); System.out.println("解密后:" + decrypt); }
}
验证码是用来区分人机的操做。
验证码划代的标准是人机识别过程当中基于对人类知识的应用。
第一代:标准验证码
这一代验证码是便是咱们常见的图形验证码、语音验证码,基于机器难以处理复杂的计算机视觉及语音识别问题,而人类却能够轻松的识别来区分人类及机器。这一代验证码初步利用了人类知识容易解答,而计算机难以解答的机制进行人机判断。
第二代:创新验证码
第二代验证码是基于第一代验证码的核心思想(经过人类知识能够解答,而计算机难以解答的问题进行人机判断)而产生的创新的交互优化型验证码。第二代验证码基于第一代验证码的核心原理--“人机之间知识的差别”,拓展出大量创新型验证码。
第三代:无知识型验证码
第三代验证码最大的特色是再也不基于知识进行人机判断,而是基于人类固有的生物特征以及操做的环境信息综合决策,来判断是人类仍是机器。无知识型验证码最大特色即无需人类思考,从而不会打断用户操做,进而提供更好的用户体验。
如Google的新版ReCaptcha、阿里巴巴的滑动验证。参考知乎 关于验证码
2.2 验证码生成器
package com.jykj.demo.util;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;
import javax.imageio.ImageIO;
/**
*/
public class ValidateCode {
// 图片的宽度。
private int width = 160;
// 图片的高度。
private int height = 28;
// 验证码字符个数
private int codeCount = 4;
// 验证码干扰线数
private int lineCount = 150;
// 验证码
private String code = null;
// 验证码图片Buffer
private BufferedImage buffImg = null;
private char[] codeSequence = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R',
'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
public ValidateCode() {
this.createCode();
}
图片宽
图片高*/
图片宽
图片高
字符个数
干扰线条数*/
public void createCode() {
int x = 0, fontHeight = 0, codeY = 0;
int red = 0, green = 0, blue = 0;
x = width / (codeCount + 2);// 每一个字符的宽度 fontHeight = height - 2;// 字体的高度 codeY = height - 4; // 图像buffer buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g = buffImg.createGraphics(); // 生成随机数 Random random = new Random(); // 将图像填充为白色 g.setColor(Color.WHITE); g.fillRect(0, 0, width, height); // 建立字体 Font font = new Font("Fixedsys", Font.BOLD, fontHeight); g.setFont(font); //干扰线 for (int i = 0; i < lineCount; i++) { int xs = random.nextInt(width); int ys = random.nextInt(height); int xe = xs + random.nextInt(width / 8); int ye = ys + random.nextInt(height / 8); red = random.nextInt(255); green = random.nextInt(255); blue = random.nextInt(255); g.setColor(new Color(red, green, blue)); g.drawLine(xs, ys, xe, ye); } // randomCode记录随机产生的验证码 StringBuffer randomCode = new StringBuffer(); // 随机产生codeCount个字符的验证码。 for (int i = 0; i < codeCount; i++) { String strRand = String.valueOf(codeSequence[random.nextInt(codeSequence.length)]); // 产生随机的颜色值,让输出的每一个字符的颜色值都将不一样。 red = random.nextInt(255); green = random.nextInt(255); blue = random.nextInt(255); g.setColor(new Color(red, green, blue)); g.drawString(strRand, (i + 1) * x, codeY); // 将产生的四个随机数组合在一块儿。 randomCode.append(strRand); } // 将四位数字的验证码保存到Session中。 code = randomCode.toString();
}
public void write(String path) throws IOException {
OutputStream sos = new FileOutputStream(path);
this.write(sos);
}
public void write(OutputStream sos) throws IOException {
ImageIO.write(buffImg, "png", sos);
sos.close();
}
public BufferedImage getBuffImg() {
return buffImg;
}
public String getCode() {
return code;
}
}
2.3 控制器使用验证码 如 CodeController
@RequestMapping("/getCode.do")
public void getCode(HttpServletRequest reqeust, HttpServletResponse response) throws IOException {
response.setContentType("image/jpeg"); // 禁止图像缓存。 response.setHeader("Pragma", "no-cache"); response.setHeader("Cache-Control", "no-cache"); response.setDateHeader("Expires", 0); HttpSession session = reqeust.getSession(); ValidateCode vCode = new ValidateCode(100, 28, 4, 100); session.setAttribute(Helper.SESSION_CHECKCODE, vCode.getCode()); vCode.write(response.getOutputStream()); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
3.1 login.html
3.2 Controller
在请求登陆页面时须要后端生成一个随机的16位字符串的key,用于先后端加密解密用,该key在登陆成功后销毁,存储在session中。
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String login(){
//生成login_token
session.setAttribute(Helper.SESSION_LOGIN_TOKEN,RandomUtil.generateString(16));//登陆令牌,用于密码加密的key,16位长度
if(session.getAttribute(Helper.SESSION_USER) == null){
return "login";
}
else
return "redirect:/";
}
1
2
3
4
5
6
7
8
9
10
接下来是提交form表单的请求
@RequestMapping(value = "/signIn", method = RequestMethod.POST,produces = "text/html;charset=UTF-8")
@ResponseBody
public String signIn(String username,String password,boolean remember,String checkCode) throws AuthorizationException{
System.out.println(username+","+password+","+remember+","+checkCode);
Object token = session.getAttribute(Helper.SESSION_LOGIN_TOKEN);//原始令牌
if(token==null) return JSON.toJSONString(new Result(false,"timeout"));//登陆成功后token失效,则页面失效,客户端须要重定向到主界面 Object countObj = session.getAttribute(Helper.SESSION_LOGIN_FAILURE_COUNT); int count = countObj==null?ConfigInfo.login_failure_count:Integer.parseInt(countObj.toString()); System.out.println("剩余次数:"+count); //验证码逻辑 if(count<=0){//须要验证码 Object oldCode = session.getAttribute(Helper.SESSION_CHECKCODE); if(checkCode==null||oldCode==null){//该登陆界面没有验证码字段,可是已经消耗掉了剩余次数,说明该页面是过时页面,须要从新登陆 return JSON.toJSONString(new Result(false,"timeout"));//客户端须要重定向到主界面 } if(checkCode.trim().isEmpty()) return JSON.toJSONString(new Result(false,"请输入验证码")); if(oldCode.toString().equalsIgnoreCase(checkCode)){ //验证经过,可信客户端,给两次剩余次数 count=2; session.setAttribute(Helper.SESSION_LOGIN_FAILURE_COUNT,2); }else{ return JSON.toJSONString(new Result(false,"codeError"));//验证码不正确,客户端须要刷新验证码 } } //解密 try { password = EncryptUtil.aesDecrypt(password,token.toString());//解密后 System.out.println("Decrypt:"+password); } catch (Exception e) { e.printStackTrace(); return JSON.toJSONString(new Result(false,"timeout"));//客户端须要重定向到主界面 } //登陆校验 String key = RandomUtil.generateString(16);//从新生成登陆令牌,任何登陆失败的操做都须要更新登陆令牌 ViewSysUser user = sysUserService.selectUserPwd(username,password); if(user == null){ session.setAttribute(Helper.SESSION_LOGIN_TOKEN,key); session.setAttribute(Helper.SESSION_LOGIN_FAILURE_COUNT,--count);//剩余次数-1 if(count<=0) return JSON.toJSONString(new Result(false,"checkCode"));//客户端须要重定向到登陆界面将验证码显示出来 return JSON.toJSONString(new Result(false,"用户名或密码错误!",key)); }else{ if(user.getUserid()!=ConfigInfo.admin_id && !user.getuStatus().equals(ConfigInfo.user_status_normal)) { session.setAttribute(Helper.SESSION_LOGIN_TOKEN,key); return JSON.toJSONString(new Result(false,"登陆失败,该帐号已被禁止使用!",key)); } //登陆成功 session.removeAttribute(Helper.SESSION_LOGIN_TOKEN); loginUser = user; session.setAttribute(Helper.SESSION_USER,loginUser); sysEventService.insertEventLog(Helper.logTypeSecurity,username+" 登陆系统"); return JSON.toJSONString(new Result(true,"登陆成功!")); } }
下面是生成随机数的工具类,很简单
package com.jykj.demo.util;
import java.util.Random;
public class RandomUtil {
public static final String ALLCHAR = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static final String LETTERCHAR = "abcdefghijkllmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static final String NUMBERCHAR = "0123456789";
/** * 返回一个定长的随机字符串(只包含大小写字母、数字) * * @param length * 随机字符串长度 * @return 随机字符串 */ public static String generateString(int length) { StringBuffer sb = new StringBuffer(); Random random = new Random(); for (int i = 0; i < length; i++) { sb.append(ALLCHAR.charAt(random.nextInt(ALLCHAR.length()))); } return sb.toString(); } /** * 返回一个定长的随机纯字母字符串(只包含大小写字母) * * @param length * 随机字符串长度 * @return 随机字符串 */ public static String generateMixString(int length) { StringBuffer sb = new StringBuffer(); Random random = new Random(); for (int i = 0; i < length; i++) { sb.append(LETTERCHAR.charAt(random.nextInt(LETTERCHAR.length()))); } return sb.toString(); } /** * 返回一个定长的随机纯大写字母字符串(只包含大小写字母) * * @param length * 随机字符串长度 * @return 随机字符串 */ public static String generateLowerString(int length) { return generateMixString(length).toLowerCase(); } /** * 返回一个定长的随机纯小写字母字符串(只包含大小写字母) * * @param length * 随机字符串长度 * @return 随机字符串 */ public static String generateUpperString(int length) { return generateMixString(length).toUpperCase(); } /** * 生成一个定长的纯0字符串 * * @param length * 字符串长度 * @return 纯0字符串 */ public static String generateZeroString(int length) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < length; i++) { sb.append('0'); } return sb.toString(); } /** * 根据数字生成一个定长的字符串,长度不够前面补0 * * @param num * 数字 * @param fixdlenth * 字符串长度 * @return 定长的字符串 */ public static String toFixdLengthString(long num, int fixdlenth) { StringBuffer sb = new StringBuffer(); String strNum = String.valueOf(num); if (fixdlenth - strNum.length() >= 0) { sb.append(generateZeroString(fixdlenth - strNum.length())); } else { throw new RuntimeException("将数字" + num + "转化为长度为" + fixdlenth + "的字符串发生异常!"); } sb.append(strNum); return sb.toString(); } /** * 每次生成的len位数都不相同 * * @param param * @return 定长的数字 */ public static int getNotSimple(int[] param, int len) { Random rand = new Random(); for (int i = param.length; i > 1; i--) { int index = rand.nextInt(i); int tmp = param[index]; param[index] = param[i - 1]; param[i - 1] = tmp; } int result = 0; for (int i = 0; i < len; i++) { result = result * 10 + param[i]; } return result; }
}
3.4 实现思路
如今淘宝登陆界面采用的是 无知识型验证码,只须要拖动滑块来判断是不是机器仍是人,若是拖滑块验证失败,会弹出验证码输入或点击选择的框,来进行二次验证。若验证成功后,连续5次输入错误的用户名或密码,则又会弹出验证码来须要继续验证。也就是说有一个风险分析系统,若是知足必定的条件(如连续屡次输入错误等)则须要增强验证。风险分析系统要综合多种因素如ip,用户信息等等。Google更简单,经过点击复选框(I’m not a robot)就经过验证。
刚开始时是不须要验证码的,用session来存储剩余次数,当连续5次验证都失败后,该计数递减为0,则后台判断该客户端不是很可信,须要验证码来增强验证,从新刷新登陆界面(能够重定向实现)把验证码输入框加载出来。客户端须要同时提交帐号密码以及验证码到后台验证,若验证码经过验证,从新将次数复位(或自定义设置),表示该客户端暂时可信下次提交登陆时能够不须要验证码。
对于密码的加密传输与后端解密,key的生成与销毁的控制很关键。当加载登陆页面时,由后台生成一个key给该页面,并保存到隐藏域中,同时该key也是存在session中。前端js用AES加密算法将密码和key混合加密,提交给后台,后台用相应的解密算法还原出原始密码,而后该原始密码用存储时使用的多重混合加密算法进行加密与数据库中的密码匹配验证。当验证成功后,移除session中的key。而验证失败后的逻辑很关键,验证失败后,须要从新生成一个key给客户端,因此客户端经过返回的信息将key赋值到那个隐藏域字段中,这样达到刷新key的目的。
整体来讲,验证码的逻辑会有点复杂。验证码的验证最好放到后台来验证,若是放到前台就须要用一个隐藏域字段来存这个验证码,这样的话机器也能够获取到,那机器就不用识别图片就能够验证了,这样验证码就失去了做用。