整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

结合cookie和session,把登陆流程梳理的清清楚楚

文为小编原创文章,首发于Java识堂,一个高原创,高收藏,有干货的微信公众号,一起成长,一起进步,欢迎关注

前言

原来分享过一篇文章,Java自定义注解及应用,当时为了能突出重点,直接在url中传了用户的所属角色,并写了一般的做法。加上最近看了一些人的简历,发现神奇的相似,都有类似商城的项目,为了不至于问些特别Low的问题,便总结了一下登录这个模块所涉及的东西

单机Session

Http协议使用的是无状态连接,这样会造成什么问题呢?看如下Demo

测试



HttpServletRequest对象代表客户端的请求,当客户端通过HTTP协议访问服务器时,HTTP请求头中的所有信息都封装在这个对象中,当在一个请求中时HttpServletRequest中的信息可以共享,而在不同的请求中HttpServletRequest并不能共享,这样就会造成用户确实进行过登录操作,但是跳到购物车页面时发现并没有东西,因为应用并不知道访问这个页面的用户是谁

我们可以用一个HttpSession对象保存跨多个请求的会话状态,上面的例子就是保存用户名,看下图理解为什么HttpSession可以跨请求保存状态




对客户的第一个请求,容器会生成一个唯一的会话ID,并通过响应把它返回给客户。客户再在以后的每一个请求中发回这个会话ID。容器看到ID后,就会找到匹配的会话,并把这个会话与请求关联

将上面代码改成如下,再测试



果然能保存会话状态了,客户和容器如何交换会话ID信息呢?其实是通过cookie实现的


看上面能保存会话的代码,我们并没有对cookie进行操作啊,其实是容器几乎会做cookie的所有工作,从最开始的Servlet开始讲这些操作是如何实现的,先看一下Servlet执行过程

  1. 用户点击页面发送请求->Web服务器应用(如Apache)->Web容器应用(如tomcat)
  2. 容器创建两个对象HttpServletRequest和HttpServletResponse
  3. 根据URL找到servlet,并为请求创建或分配一个线程,将请求和响应对象传递给这个servlet线程
  4. 容器调用Servlet的service()方法,根据请求的不同类型,service()方法会调用doGet()和doPost()方法,假如请求是HTTP GET请求
  5. doGet()方法生成动态页面,并把这个对象塞到响应对象里。容器有响应对象的一个引用
  6. 线程结束,容器把响应对象装换为一个HTTP请求,把它发回给客户,然后删除请求和响应对象

容器使用部署描述文件把URL映射到Servlet ,一个Servlet可以有3个名字,(1)用户知道的URL名,(2)部署人员知道的内部名,(3)实际的文件名

加入使用Spring MVC时要在web.xml中配置如下内容

根据url-pattern->servlet-name->servlet-class的三级映射关系,容器即可根据用户输入的URL找到对应的Servlet

从这个就可以看出其实Spring MVC框架其实在Servlet上面封装了一层,当我们自己用Servlet编写程序时,可以从HttpServletRequest中获取HttpSession,如下

public class LoginServlet extends HttpServlet {

 @Override
 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

 HttpSession session = req.getSession();
 }

 @Override
 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

 super.doPost(req, resp);
 }
}

在响应中发送一个会话cookie

HttpSession session = req.getSession();


我们只需要写上述一行代码即可,来看看容器帮我们做了哪些事情

  1. 建立一个新的HttpSession对象
  2. 生成唯一的会话ID
  3. 建立新的Cookie对象
  4. 把会话Id放到cookie中
  5. 在响应中设置cookie

从请求得到会话ID

HttpSession session = req.getSession();


与响应生成会话ID和cookie时用的方法一样

if (请求包含一个会话ID cookie) {
 找到与该ID匹配的会话
} else if (没有会话Id cookie OR 没有与此会话ID匹配的当前会话) {
 创建一个新会话
}


如上面用的方法,我们并没有直接从HttpServletRequest 中获取HttpSession

public String login(HttpSession session, @RequestParam("username") String username)


能直接获取到HttpSession,其实是框架帮我们执行了HttpSession session = req.getSession(),然后设置进来的。我们可以设置session的过期时间,以保证用户登录后长期不操作需要重新登录

分布式Session

当整个服务是分布式的该怎么处理呢?用户在服务器A上登录,结果在服务器B上查看购物车信息,因为在A上登录,HttpSession存在A服务器上,当访问B服务器上的购物车信息因为获取不到用户登录的HttpSession,就会认为用户没有登录,这种情况该怎么处理呢?

实现分布式Session有多种方式,这里就介绍一下用Redis实现分布式Session,其实Spring Session项目就使用Redis实现Session共享的

理解了单机Session,分布式Session也不难理解,主要步骤如下

  1. 用户登录以后,先生成类似于sessionId的唯一标识,我们把它叫token
  2. new一个cookie,将token写到cookie当中传递到客户端,并将以key=token,value=用户信息的hash放到redis中,当然cookie和这个hash都可以设置过期时间
  3. 客户端在随后的访问中服务器从cookie中拿到这个token,根据这个token去Redis中取到用户信息

当用户登出时只要删除key为token的hash,并且将cookie的最长时间设置为0,重新放回HttpServletResponse即可,鉴于篇幅限制,就不写具体代码了

为什么要在密码中加盐

直接存储

以前系统存储密码时都是类似如下形式

假如用户信息泄露,用户的账号安全将受到威胁,参考CSDN密码泄露事件

加密存储

既然明文存储会有安全问题,那就加密存储,一般常用的加密算法是MD5和SHA,当用户注册时,数据库中保存的密码是加密后的密码,当用户登录时先对登录的密码进行MD5,然后和数据库中的密码比对,正确则登录成功,失败则登录失败

以为这样就足够安全了?其实远远不够,有的人将各种密码的MD5值都算出来,做成一个字典,前面说的泄露的CSDN的密码就是一个很好的素材,这样就可以通过

泄露密码的MD5值->MD5字典->原始的字符串的映射关系,得到泄露的密码,针对这种情况,有2种做法,一种是将密码多次进行MD5,即对加密后的MD5值再次进行MD5,另一种就是加盐

加盐存储

由于盐值时随机生成的,我们算一下破解一个用户的密码需要多长时间,假如数据库中密码是如此生成的MD5(明文密码+Salt),MD5的方式也被坏人知道了,假如坏人有600w个字典,得先对这些字典加Salt做一次MD5再匹配,而且还有可能匹配不出来,破解一个账号的成本就这么高,而且盐值和密码的方式进行MD5的方式也多种多样啊,Salt可以插中间,Salt倒序再进行MD5。当然还可以这样啊MD5(Salt[0] + 明文密码 + Salt[5])。如果还觉得不够安全,还可以对加盐生成的MD5值再次MD5啊,次数由你定,这样几乎是破解不了

用session做一个验证码登录的案例

转发:实现服务器的跳转

重定向是浏览器的跳转

1. 问:什么时候使用转发,什么时候使用重定向?

如果要保留请求域中的数据,使用转发,否则使用重定向。

以后访问数据库,增删改使用重定向,查询使用转发。

2. 问:转发或重定向后续的代码是否还会运行?

无论转发或重定向后续的代码都会执行

https://ww.lanzous.com/b015g184b 密码:b53v

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>login</title>


    <script>
        window.onload = function(){
            document.getElementById("img").onclick = function(){
                this.src="/Project1/checkCodeServlet?time="+new Date().getTime();
            }
        }


    </script>
    <style>
        div{
            color: red;
        }

    </style>
</head>
<body>

    <form action="/Project1/loginServlet" method="post">
        <table>
            <tr>
                <td>用户名</td>
                <td><input type="text" name="username"></td>
            </tr>
            <tr>
                <td>密码</td>
                <td><input type="password" name="password"></td>
            </tr>
            <tr>
                <td>验证码</td>
                <td><input type="text" name="checkCode"></td>
            </tr>
            <tr>
                <td colspan="2"><img id="img" src="/Project1/checkCodeServlet"></td>
            </tr>
            <tr>
                <td colspan="2"><input type="submit" value="登录"></td>
            </tr>
        </table>


    </form>


    <div><%=request.getAttribute("cc_error") == null ? "" : request.getAttribute("cc_error")%></div>
    <div><%=request.getAttribute("login_error") == null ? "" : request.getAttribute("login_error") %></div>

</body>
</html>
package cn.itcast.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@WebServlet("/loginServlet")
public class LoginServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //1.设置request编码
        request.setCharacterEncoding("utf-8");
        //2.获取参数
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        String checkCode = request.getParameter("checkCode");
        //3.先获取生成的验证码
        HttpSession session = request.getSession();
        String checkCode_session = (String) session.getAttribute("checkCode_session");
        //删除session中存储的验证码
        session.removeAttribute("checkCode_session");
        //3.先判断验证码是否正确
        if(checkCode_session!= null && checkCode_session.equalsIgnoreCase(checkCode)){
            //忽略大小写比较
            //验证码正确
            //判断用户名和密码是否一致
            if("zhangsan".equals(username) && "123".equals(password)){//需要调用UserDao查询数据库
                //登录成功
                //存储信息,用户信息
                session.setAttribute("user",username);
                //重定向到success.jsp
                response.sendRedirect(request.getContextPath()+"/success.jsp");
            }else{
                //登录失败
                //存储提示信息到request
                request.setAttribute("login_error","用户名或密码错误");
                //转发到登录页面
                request.getRequestDispatcher("/login.jsp").forward(request,response);
            }


        }else{
            //验证码不一致
            //存储提示信息到request
            request.setAttribute("cc_error","验证码错误");
            //转发到登录页面
            request.getRequestDispatcher("/login.jsp").forward(request,response);

        }

    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doPost(request, response);
    }
}
package cn.itcast.servlet;

import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;

@WebServlet("/checkCodeServlet")
public class CheckCodeServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {


        int width = 100;
        int height = 50;

        //1.创建一对象,在内存中图片(验证码图片对象)
        BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);


        //2.美化图片
        //2.1 填充背景色
        Graphics g = image.getGraphics();//画笔对象
        g.setColor(Color.PINK);//设置画笔颜色
        g.fillRect(0,0,width,height);

        //2.2画边框
        g.setColor(Color.BLUE);
        g.drawRect(0,0,width - 1,height - 1);

        String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789";
        //生成随机角标
        Random ran = new Random();
        StringBuilder sb = new StringBuilder();
        for (int i = 1; i <= 4; i++) {
            int index = ran.nextInt(str.length());
            //获取字符
            char ch = str.charAt(index);//随机字符
            sb.append(ch);

            //2.3写验证码
            g.drawString(ch+"",width/5*i,height/2);
        }
        String checkCode_session = sb.toString();
        //将验证码存入session
        request.getSession().setAttribute("checkCode_session",checkCode_session);

        //2.4画干扰线
        g.setColor(Color.GREEN);

        //随机生成坐标点

        for (int i = 0; i < 10; i++) {
            int x1 = ran.nextInt(width);
            int x2 = ran.nextInt(width);

            int y1 = ran.nextInt(height);
            int y2 = ran.nextInt(height);
            g.drawLine(x1,y1,x2,y2);
        }


        //3.将图片输出到页面展示
        ImageIO.write(image,"jpg",response.getOutputStream());


    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doPost(request,response);
    }
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>

    <h1><%=request.getSession().getAttribute("user")%>,欢迎您</h1>

</body>
</html>


系列文章旨在记录和总结自己在Java Web开发之路上的知识点、经验、问题和思考,希望能帮助更多(Java)码农和想成为(Java)码农的人。

提示:尽量使用头条APP阅读,头条网页展示代码会有问题。

目录

  1. 介绍
  2. 原有的用户退出功能
  3. 思路
  4. 修改include.jsp
  5. 添加处理用户退出请求的动作
  6. 总结

介绍

前面的文章我们实现了租房网平台的用户注册、用户登录、会话跟踪等功能,本篇文章继续实现用户的登出/退出功能。

原有的用户退出功能

实际上,我们之前的版本中,只要用户登录之后,在每个页面当中已经有退出按钮,如下图:


其对应的代码在我们的 include.jsp 中:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="java.util.List" %>
<%@ page import="houserenter.entity.House" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>租房网</title>
</head>
<body>
<h1>你好,${sessionScope.userName}!欢迎来到租房网! <a href="login.html">退出</a></h1>
<br><br>

可以看到,这个退出按钮就是一个普通的链接,直接返回到登录页面而已。那这样有什么不好的地方呢?我们做这样一个实验:

  1. 先登录到租房网平台;
  2. 登录后可以打开其他页面,我这里假设打开某个房源详情页面,其URL是:http://localhost:8080/house-renter/house-details.action?houseId=1
  3. 然后点击退出,会跳转到登录页面;
  4. 此时我直接在浏览器的地址栏中重新输入上述URL,可以新开一个浏览器标签页,甚至另外打开一个浏览器进程(不过必须是同一款浏览器,我这里是谷歌浏览器),敲回车;
  5. 结果是我跳过了登录步骤,直接打开了该房源详情页面。

结论就是这样简单的登录功能很不安全。

究其原因,其实是我们采用了session进行会话跟踪,只要session不过期,Servlet容器(我们这里是Tomcat)中的该session对象就还有效,绑定到该session对象中的数据也就还有效。

不过,因为HTTP是基于TCP的,所以不同的TCP连接肯定会产生不同的session,大家有兴趣的话可以自行测试一下TCP连接和session之间的关系。

思路

所以,我们的用户退出功能必须是这样的:

  1. 用户点击退出按钮;
  2. Servlet容器必须感知到用户的退出;
  3. Servlet容器销毁该用户的session。

修改include.jsp

Servlet容器感知用户的退出很简单,只要发送一个请求给Servlet容器即可。

所以我们设计一个用户退出的动作,让上面的退出按钮指向该动作:

<h1>你好,${sessionScope.userName}!欢迎来到租房网! <a href="logout.action">退出</a></h1>

添加处理用户退出请求的动作

我们可以在HouseRenterController中添加处理用户退出请求的动作:

	@GetMapping("/logout.action")
	public ModelAndView getLogout(HttpSession session) {
		System.out.println("session: " + session);
		System.out.println("session id: " + session.getId());
    
		session.invalidate();
    
		ModelAndView mv = new ModelAndView();
		mv.setViewName("redirect:login.html");
		return mv;
	}

重点关注的是,我们调用了HttpSession的invalidate()方法,这样我们就销毁了该session。

我们是如何得知该方法的呢?我们可以充分利用IDE的自动补足功能,然后查看每一个方法的javadoc:


可以发现invalidate()方法就是用来使此session无效,并解除绑定到它的任何对象。

总结

大家可以自行验证一下,经过这样改造后,上述问题得到解决。

  • 调用HttpSession的invalidate()方法可以使会话无效。