发表于: 2020-06-29 23:14:30
1 1735
今天完成的事情:
学习cookie:
Cookie是由服务器端生成,发送给User-Agent,浏览器会将Cookie的key/value保存到某个目录下的文本文件内,下次请求同一网站时就发送该Cookie给服务器。
Cookie的处理分为:
服务器像客户端发送cookie,浏览器将cookie保存,之后每次http请求浏览器都会将cookie发送给服务器端。
发送cookie:
服务器端向客户端发送Cookie是通过HTTP响应报文实现的,在Set-Cookie中设置需要像客户端发送的cookie,cookie格式如下:
Set-Cookie: "name=value;domain=.domain.com;path=/;expires=Sat, 11 Jun 2016 11:29:42 GMT;HttpOnly;secure"
其中name=value是必选项,其它都是可选项。Cookie的主要构成如下:
name:一个唯一确定的cookie名称。通常来讲cookie的名称是不区分大小写的。
value:存储在cookie中的字符串值。最好为cookie的name和value进行url编码
domain:cookie对于哪个域是有效的。所有向该域发送的请求中都会包含这个cookie信息。这个值可以包含子域(如:
yq.aliyun.com),也可以不包含它(如:.aliyun.com,则对于aliyun.com的所有子域都有效).
path: 表示这个cookie影响到的路径,浏览器跟会根据这项配置,像指定域中匹配的路径发送cookie。
expires:失效时间,表示cookie何时应该被删除的时间戳(也就是,何时应该停止向服务器发送这个cookie)。如果不设置这个时间戳,浏览器会在页面关闭时即将删除所有cookie;不过也可以自己设置删除时间。这个值是GMT时间格式,如果客户端和服务器端时间不一致,使用expires就会存在偏差。
max-age: 与expires作用相同,用来告诉浏览器此cookie多久过期(单位是秒),而不是一个固定的时间点。正常情况下,max-age的优先级高于expires。
HttpOnly: 告知浏览器不允许通过脚本document.cookie去更改这个值,同样这个值在document.cookie中也不可见。但在http请求张仍然会携带这个cookie。注意这个值虽然在脚本中不可获取,但仍然在浏览器安装目录中以文件形式存在。这项设置通常在服务器端设置。
secure: 安全标志,指定后,只有在使用SSL链接时候才能发送到服务器,如果是http链接则不会传递该信息。就算设置了secure 属性也并不代表他人不能看到你机器本地保存的 cookie 信息,所以不要把重要信息放cookie就对了服务器端设置。
服务器端解析cookie
cookie可以设置不同的域与路径,所以对于同一个name value,在不同域不同路径下是可以重复的,浏览器会按照与当前请求url或页面地址最佳匹配的顺序来排定先后顺序。所以当前端传递到服务器端的cookie有多个重复name value时,我们只需要最匹配的那个,也就是第一个。
客户端的存取
浏览器将后台传递过来的cookie进行管理,并且允许开发者在JavaScript中使用document.cookie来存取cookie。但是这个接口使用起来非常蹩脚。它会因为使用它的方式不同而表现出不同的行为。
当用来获取属性值时,document.cookie返回当前页面可用的(根据cookie的域、路径、失效时间和安全设置)所有的字符串,字符串的格式如下:
"name1=value1;name2=value2;name3=value3";
当用来设置值的时候,document.cookie属性可设置为一个新的cookie字符串。这个字符串会被解释并添加到现有的cookie集合中。
这个图就很清楚了。
按我的理解:controller接收到账号密码,对密码进行MD5加密然后与数据库中的进行验证,验证通过就将账户id+系统当前时间生成token添加到cookie中。下次登录时拦截器先检查cookie中有没有token,如果有则不用再次登录直接进入,没有就拦截请求跳转至登录页面。
修改代码:
首先对注册功能需要对密码进行加密然后存入数据库中:
@RequestMapping(value = "register", method = RequestMethod.POST)
public String register(Account account, Model model) {
logger.info(account);//输入的账号密码
//判断账号密码是否为空
if (account.getUsername().length() != 0 && account.getPassword().length() != 0) {
//判断该账号是否存在
if (accountService.selectAccount(account.getUsername()) == null) {
logger.info("加密前密码为:" + account.getPassword());
//对密码进行加密
String pswMD5 = MD5Util.stringToMD5(account.getPassword());
logger.info("加密后密码为:" + pswMD5);
account.setPassword(pswMD5);
account.setCreateat(1L);
account.setCreateby("管理员");
account.setUpdateat(1L);
account.setUpdateby("管理员");
accountService.insert(account);
model.addAttribute("msg", "注册成功");
return "login";
} else {
model.addAttribute("msg", "该账号已存在!");
return "register";
}
} else {
model.addAttribute("msg", "账号和密码不能为空!");
return "register";
}
}
登录请求
@RequestMapping(value = "login")
public String login(Account account, Model model, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
logger.info("加密前的信息:" + account);
if (account.getUsername() != null && account.getPassword() != null) {
//将密码通过MD5进行加密
String passwordMD5 = MD5Util.stringToMD5(account.getPassword());
account.setPassword(passwordMD5);
logger.info("加密后的信息:" + account);
Account account1 = accountService.select(account);
//验证账号密码是否正确
if (account1 != null) {
logger.info("登录成功");
Long id = account1.getId();//根据用户名获取id
//使用系统当前时间生成唯一token,格式为键值对
String token = id + "=" + System.currentTimeMillis();
//使用DES加密
String tokenDES = DESUtils.getEncryptString(token);
logger.info("加密后的token:" + tokenDES);
//保存到cookies中
Cookie cookie = new Cookie("token", tokenDES);
//设置cookie过期时间 单位为秒
cookie.setMaxAge(3600);
//设置cookie有效路径
cookie.setPath("/");
httpServletResponse.addCookie(cookie);
return "redirect:/u/profession";
} else {
model.addAttribute("error", "账号或密码错误");//如果账号密码错误则提示该消息
return "login";
}
} else {
model.addAttribute("error", "请先登录");//如果未登录就访问/u/profession则提示该消息
return "login";
}
}
拦截器:
public class MyInterceptor1 implements HandlerInterceptor {
@Autowired
AccountService accountService;
private static Logger logger = LogManager.getLogger(MyInterceptor1.class);
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception {
// 判断cookie中是否携带token,并进行验证
Cookie[] cookies = httpServletRequest.getCookies();
if (cookies != null) {
logger.info("Cookie长度为: " + cookies.length);
logger.info("拦截器获取到的Cookie: " + String.valueOf(cookies));
logger.info("开始遍历");
// 遍历
for (Cookie cookie : cookies) {
logger.info("当前cookie的值: " + cookie.getValue() + " 名字为:" + cookie.getName());
// 判断是否有token
if (cookie.getName().equals("token")) {
String tokenDES = cookie.getValue();
logger.info("tokenDES: " + tokenDES);
String token = DESUtils.getDecryptString(tokenDES);//解密
logger.info("token的解密value:" + token);
// 分割字符串 获取id
Long id = Long.valueOf(token.split("=")[0]);
logger.info("id为: " + id);
//验证token有效性
if (accountService.select(new Account(id)) != null) {
return true;
} else {
logger.debug("token验证失败,跳回登陆页面");
// httpServletRequest https://blog.csdn.net/gris0509/article/details/6340987
httpServletResponse.sendRedirect(httpServletRequest.getContextPath() + "/login");
return false;
}
}
}
}
logger.debug("cookies不存在");
httpServletResponse.sendRedirect(httpServletRequest.getContextPath() + "/login");
return false;
}
}
测试:
不登陆直接访问/u/profession会直接跳转到登录页面,提示需要先登录:
控制台日志输出如下:
-DEBUG-2020/06/29 19:48:40,916-com.jnshu.Interceptor.MyInterceptor1.preHandle(MyInterceptor1.java:58)-58-cookies不存在
注册功能:
如果该用户名已存在,则返回注册页面,并提示该用户名已存在:
账号密码符合要求,则注册成功:
控制台日志输出:
-INFO-2020/06/29 19:52:30,329-com.jnshu.controller.AccountController.register(AccountController.java:41)-41-Account{username='zhangsan', password='1234'}
-INFO-2020/06/29 19:52:30,330-com.jnshu.controller.AccountController.register(AccountController.java:47)-47-加密前密码为:1234
-INFO-2020/06/29 19:52:30,331-com.jnshu.controller.AccountController.register(AccountController.java:50)-50-加密后密码为:81DC9BDB52D04DC20036DBD8313ED055
在数据库可以看到该账户信息:
登录功能:
登录成功则直接跳转到u/profession页面:
控制台输出:
可以看到成功了,之后再次发送/u/profession就不用再登录了。
至此登录注册模块基本完成,验收标准里提到需要用MD5加盐。
1、盐是什么
一串随机数
2、为什么要加盐
只要明文相同,那么MD5加密后的密文就相同,于是攻击者就可以通过撞库的方式来破解出明文。加盐就是向明文中加入随机数,然后在生成MD5,这样一来即使明文相同,每次生成的MD5码也不同,如此就加大了暴力破解的难度。
加盐:加盐加密是一种对系统登录口令的加密方式,它实现的方式是将每一个口令跟一个n位随机数相关联,这个n位随机数叫做”盐“(salt)。
加盐是为了应对这么一种情况:如果两个人或多个人的密码相同,那么通过相同的加密算法得到的是相同的结果。这样会造成哪些后果呢?首先,破解一个就有可能是相当于破一片密码。而且加入小明这个用户可以查看后台数据库,那么如果他观察到小红这个用户的密码跟自己的密码是一样的(虽然都是密文),那么,也就代表他们两个人的密码是相同的。所以他就可以用小红的身份进行登录了。
其实,我们只要稍微混淆一下就能防范住了,这在加密术语中称为“加盐”。具体来说就是在原有材料(用户自定义密码)中加入其他成分(一般是用户自有且不变的因素),以此来增加系统复杂度。当这种盐和用户密码结合后,再通过摘要处理,就能得到隐蔽性更强的摘要值。
重新找了一个MD5加密工具类:
package com.jnshu.util;
import org.apache.commons.codec.binary.Hex;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
public class MD5Util {
public static String MD5(String input) {
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
return "check jdk";
} catch (Exception e) {
e.printStackTrace();
return "";
}
char[] charArray = input.toCharArray();
byte[] byteArray = new byte[charArray.length];
for (int i = 0; i < charArray.length; i++)
byteArray[i] = (byte) charArray[i];
byte[] md5Bytes = md5.digest(byteArray);
StringBuffer hexValue = new StringBuffer();
for (int i = 0; i < md5Bytes.length; i++) {
int val = ((int) md5Bytes[i]) & 0xff;
if (val < 16)
hexValue.append("0");
hexValue.append(Integer.toHexString(val));
}
return hexValue.toString();
}
public static String generate(String password) {
Random r = new Random();
StringBuilder sb = new StringBuilder(16);
sb.append(r.nextInt(99999999)).append(r.nextInt(99999999));
int len = sb.length();
if (len < 16) {
for (int i = 0; i < 16 - len; i++) {
sb.append("0");
}
}
String salt = sb.toString();
password = md5Hex(password + salt);
char[] cs = new char[48];
for (int i = 0; i < 48; i += 3) {
cs[i] = password.charAt(i / 3 * 2);
char c = salt.charAt(i / 3);
cs[i + 1] = c;
cs[i + 2] = password.charAt(i / 3 * 2 + 1);
}
return new String(cs);
}
public static boolean verify(String password, String md5) {
char[] cs1 = new char[32];
char[] cs2 = new char[16];
for (int i = 0; i < 48; i += 3) {
cs1[i / 3 * 2] = md5.charAt(i);
cs1[i / 3 * 2 + 1] = md5.charAt(i + 2);
cs2[i / 3] = md5.charAt(i + 1);
}
String salt = new String(cs2);
return md5Hex(password + salt).equals(new String(cs1));
}
private static String md5Hex(String src) {
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] bs = md5.digest(src.getBytes());
return new String(new Hex().encode(bs));
} catch (Exception e) {
return null;
}
}
}
MD5()方法是普通加密
generate()方法是加盐加密
verify()方法是判断是否为同一字符串
虽然不知道具体原理,不过现在每次使用MD5加盐加密得到的字符串都不一样了。
再次修改登录请求:
@RequestMapping(value = "login")
public String login(String username, String password, Model model, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
logger.info("加密前的信息:" + password);
//判断用户名和密码是否不为空
if (username != null && username.length() != 0 && password != null && password.length() != 0) {
Account account = accountService.selectAccount(username);
//验证账号密码是否正确
if (MD5Util.verify(password,account.getPassword())) {
logger.info("登录成功");
Long id = account.getId();//根据用户名获取id
//使用系统当前时间生成唯一token,格式为键值对
String token = id + "=" + System.currentTimeMillis();
//使用DES加密
String tokenDES = DESUtils.getEncryptString(token);
logger.info("加密后的token:" + tokenDES);
//保存到cookies中
Cookie cookie = new Cookie("token", tokenDES);
//设置cookie过期时间 单位为秒
cookie.setMaxAge(3600);
//设置cookie有效路径
cookie.setPath("/");
httpServletResponse.addCookie(cookie);
return "redirect:/u/profession";
} else {
model.addAttribute("error", "账号或密码错误");//如果账号密码错误则提示该消息
return "login";
}
} else {
model.addAttribute("error", "请先登录");//如果未登录就访问/u/profession则提示该消息
return "login";
}
}
}
注册请求:
@RequestMapping(value = "register", method = RequestMethod.POST)
public String register(Account account, Model model) {
logger.info(account);//输入的账号密码
//判断账号密码是否为空
if (account.getUsername().length() != 0 && account.getPassword().length() != 0) {
//判断该账号是否存在
if (accountService.selectAccount(account.getUsername()) == null) {
logger.info("加密前密码为:" + account.getPassword());
//对密码进行MD5加密加盐
String pswMD5 = MD5Util.generate(account.getPassword());
logger.info("加密后密码为:" + pswMD5);
account.setPassword(pswMD5);
account.setCreateat(1L);
account.setCreateby("管理员");
account.setUpdateat(1L);
account.setUpdateby("管理员");
accountService.insert(account);
model.addAttribute("msg", "注册成功");
return "login";
} else {
model.addAttribute("msg", "该账号已存在!");
return "register";
}
} else {
model.addAttribute("msg", "账号和密码不能为空!");
return "register";
}
}
想了一下似乎还要加个注销功能。实现起来还算简单,只需要点注销时清除cookie并返回首页即可:
@RequestMapping("logout")
public String logout(HttpSession session, HttpServletRequest request, HttpServletResponse response){
//session.removeAttribute("loginname"); 消耗session
Cookie[] cookies = request.getCookies();
for (Cookie cookie :cookies){//遍历所有Cookie
if(cookie.getName().equals("token")){//找到对应的cookie
cookie.setMaxAge(0);//Cookie并不能根本意义上删除,只需要这样设置为0即可
cookie.setPath("/");//很关键,设置成跟写入cookies一样的,全路径共享Cookie
response.addCookie(cookie);//重新响应
return "redirect:/home";
}
}
return "redirect:/home";
}
然后在index.jsp页面加一个注销按钮:
任务五完成。
这里我嫌登录注册页面太丑了,在网上找了个模板
现在是这种效果:
将代码部署到服务器测试一下。
各功能都正常!
提交任务五
收获:理解了cookie,session,以及MD5加盐加密。
明天计划完成的事情:看一下深度思考,开始任务六。
评论