发表于: 2020-06-29 23:14:30

1 1736


今天完成的事情:

学习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加盐加密。


明天计划完成的事情:看一下深度思考,开始任务六。


返回列表 返回列表
评论

    分享到