Http Server 版本3 - 实现完整的登录过程(Cookie & Session)

x33g5p2x  于2022-01-11 转载在 其他  
字(19.3k)|赞(0)|评价(0)|浏览(151)

前言:
之前,我们写了两个版本的 Http 服务器,本篇,再继续实现第三个版本
V3 将满足:
1.支持返回一个静态的 html 文件
2.解析处理 cookie (把 cookie 处理成键值对结构)
3.解析处理 body (把 body 中的数据处理成键值对结构)
4.实现一个完整的登录功能 (session 的简单实现)

request 类:

public class Request {
    private String method;
    private String url;
    private String version;
    private Map<String,String> headers = new HashMap<>();
    // url 中的参数和 body中的参数都放在 parameters 哈希表里
    private Map<String,String> parameters = new HashMap<>();
    private Map<String,String> cookies = new HashMap<>();
    private String body;

    public static Request build(InputStream inputStream) throws IOException {
        Request request = new Request();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        //1.处理首行
        String fistLine = bufferedReader.readLine();
        String[] firstLineTokens = fistLine.split(" ");
        request.method = firstLineTokens[0];
        request.url = firstLineTokens[1];
        request.version = firstLineTokens[2];
        //2.解析 url
        int pos = request.url.indexOf("?");
        if(pos != -1){
            String queryString = request.url.substring(pos + 1);
            parseKV(queryString,request.parameters);
        }
        //3.循环处理 header 部分
        String line = "";
        while ((line = bufferedReader.readLine()) != null && line.length() != 0){
            String[] headerTokens = line.split(": ");
            request.headers.put(headerTokens[0],headerTokens[1]);
        }
        //4.解析 cookie
        String cookie = request.headers.get("Cookie");
        if(cookie != null){
            // 把 cookie 进行解析
            parseCookie(cookie,request.cookies);
        }
        //5.解析 body
        if("POST".equalsIgnoreCase(request.method)
            || "PUT".equalsIgnoreCase(request.method)) {
            // 其他方法暂时不考虑
            // 需要把 body 读取出来
            // 此处的长度单位是 "字节"
            int contentLength = Integer.parseInt(request.headers.get("Content-Length"));
            // contentLength 长度单位是字节
            // contentLength 为100, body中有100个字节
            // 创建的缓冲区长度是 100个char (相当于200个字符)
            char[] buffer = new char[contentLength];
            int len = bufferedReader.read(buffer);
            request.body = new String(buffer,0,len);
            // body 中的格式形如: username=huahua&password=666
            parseKV(request.body, request.parameters);
        }
        return request;
    }

    private static void parseCookie(String cookie, Map<String, String> cookies) {
        //1.按照 "; " 拆分成多个键值对
        String[] KVTokens = cookie.split(": ");
        //2.按照 = 拆分每个键和值
        for(String kv : KVTokens){
            String[] result = kv.split("=");
            cookies.put(result[0],result[1]);
        }
    }

    private static void parseKV(String queryString, Map<String, String> parameters) {
        //1.按照 & 拆分成多个键值对
        String[] KVTokens = queryString.split("&");
        //2.按照 = 拆分每个键和值
        for(String kv : KVTokens){
            String[] result = kv.split("=");
            parameters.put(result[0],result[1]);
        }
    }

    public String getMethod() {
        return method;
    }

    public String getUrl() {
        return url;
    }

    public String getVersion() {
        return version;
    }

    public String getBody() {
        return body;
    }

    public String getParameter(String key) {
        return parameters.get(key);
    }

    public String getHeader(String key) {
        return headers.get(key);
    }

    public String getCookie(String key) {
        return cookies.get(key);
    }
}

response 类:

public class Response {
    private String version = "HTTP//1.1";
    private int status;
    private String message;
    private Map<String,String> headers = new HashMap<>();
    private StringBuilder body = new StringBuilder(); //方便拼接
    private OutputStream outputStream = null;

    //工厂方法
    public static Response build(OutputStream outputStream){
        Response response = new Response();
        response.outputStream = outputStream;
        return response;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public void setHeader(String key,String value) {
        headers.put(key,value);
    }

    public void writeBody(String content) {
        body.append(content);
    }

    public void flush() throws IOException {
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
        bufferedWriter.write(version + " " + status + " " + message + "\n");
        headers.put("Content-Length", body.toString().getBytes().length + " ");
        for (Map.Entry<String,String> entry : headers.entrySet()) {
            bufferedWriter.write(entry.getKey() + ": " + entry.getValue() + "\n");
        }
        bufferedWriter.write("\n");
        bufferedWriter.write(body.toString());
        bufferedWriter.flush();
    }
}

主类:

public class HttpServerV3 {
    private ServerSocket serverSocket = null;

    public HttpServerV3(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动...");
        ExecutorService executorService = Executors.newCachedThreadPool();
        while (true) {
            Socket clientSocket = serverSocket.accept();
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    process(clientSocket);
                }
            });
        }
    }
    public void process(Socket clientSocket){
        try {
            //1.读取请求并解析
            Request request = Request.build(clientSocket.getInputStream());
            Response response = Response.build(clientSocket.getOutputStream());
            //2.根据请求计算响应 按照不同的 Http 方法,拆分成不同的逻辑
            if("GET".equalsIgnoreCase(request.getMethod())){
                doGet(request,response);
            }
            else if("POST".equalsIgnoreCase(request.getMethod())){
                doPost(request,response);
            }
            else{
                // 其他方法,返回一个405
                response.setStatus(405);
                response.setMessage("Method Not Allowed");
            }
            //3.把响应写回客户端
            response.flush();
        } catch (IOException | NullPointerException e) {
            e.printStackTrace();
        } finally {
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void doGet(Request request, Response response) throws IOException {
        //1.能够支持返回一个 html 文件
        if(request.getUrl().startsWith("/index.html")){
            // 让代码读取一个 /index.html 这样的文件
            // 要想读文件,需要先知道文件路径 (只知道文件名)
            // 此时 html 文件所属的路径,可以自己约定
            // 把文件内容写到响应的 body 中
			response.setStatus(200);
            response.setMessage("OK");
            response.setHeader("Content-Type","text/html; charset=utf-8");
            // HttpServerV3.class获取一个类对象
            // getClassLoader() 获取当前类的类加载器
            InputStream inputStream = HttpServerV3.class.getClassLoader().getResourceAsStream("index.html");
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            //按行读取内容,把数据写入到 response 中
            String lint = " ";
            while ((lint = bufferedReader.readLine()) != null){
                response.writeBody(lint + "\n");
            }
            bufferedReader.close();
        }
    }

    private void doPost(Request request, Response response) {

    }

    public static void main(String[] args) throws IOException {
        HttpServerV3 httpServerV3 = new HttpServerV3(6060);
        httpServerV3.start();
    }
}

要想能够支持返回一个 html 文件,就需要让代码读取一个 类似 /index.html 这样的文件,要想读文件,需要先知道文件路径 (但我们只知道文件名)

解决方法:

new 一个 和 src 同级的 Directory

右击新创建的 xxx (Directory):Mark Directory as — xxx Root

然后 new 一个 file:index.html

<html>
<head>
    <title>登录界面</title>
    <meta charset="UTF-8">
</head>

<body>
    <!-- /login操作服务器端还没有实现 -->
    <form method="post" action="/login">
        <div style="margin-bottom: 8px">
            <input type="text" name="username" placeholder="请输入用户名">
        </div>
        <div style="margin-bottom: 8px">
            <input type="password" name="password" placeholder="请输入密码">
        </div>
        <div>
            <input type="submit" value="登录">
        </div>
    </form>
</body>

</html>

写到这里,服务器已经可以返回一个指定的静态页面了
这个页面中包含了一个 form 表单,借助表单来实现登录操作

此时,启动服务器

由于表单是把数据提交到 /login 这个 path 中
服务器紧接着就要实现 POST 请求下的 /login 的处理

doPost 方法:

private void doPost(Request request, Response response) {
    //2.实现 /login 的处理
    if(request.getUrl().startsWith("/login")) {
        //读取用户提交的用户名和密码
        String userName = request.getParameter("username");
        String password = request.getParameter("password");
        System.out.println("userName: " + userName);
        System.out.println("password: " + password);
    }
}

再次重启服务器,使用 fiddler 抓包:

流程:

接着,请求到达服务器上,解析成 Request 对象
用户名和密码这种键值对,就保存到 body 中,同时会把 body 中的键值对解析保存到 parameters 表中

doPost 方法获取参数:

在这个基础上,在继续实现验证用户名密码是否正确

private void doPost(Request request, Response response) {
    //2.实现 /login 的处理
    if(request.getUrl().startsWith("/login")) {
        //读取用户提交的用户名和密码
        String userName = request.getParameter("username");
        String password = request.getParameter("password");
        //登录逻辑需要验证用户名密码是否正确
        // 此处为了简单,把用户名和密码在代码中写死
        // 更科学的处理方式: 从数据库中读取用户名对应密码,检验密码是否一致
        if("hh".equals(userName) && "666".equals(password)){
            // 登陆成功
            response.setStatus(200);
            response.setMessage("OK");
            response.setHeader("Content-Type","text/html;charset=utf-8");
            response.writeBody("<html>");
            response.writeBody("<div>欢迎您!" + userName + "</div>");
            response.writeBody("</html>");
        }
        else{
            // 登陆失败
            response.setStatus(403);
            response.setMessage("Forbidden");
            response.setHeader("Content-Type","text/html;charset=utf-8");
            response.writeBody("<html>");
            response.writeBody("<div>登陆失败</div>");
            response.writeBody("</html>");
        }
    }
}

重启服务器,进入页面:

使用 fiddler 抓包验证:

对于页面来说,若登录成功之后,刷新页面,自己仍然处于登陆状态
访问该网站的其他页面,此时仍然处在登录状态

如何实现上述功能? 就需要使用 Cookie

在 doPost 中再加上:

response.setHeader("Set-Cookie","username=" + userName);

此时我们就在 Http 的响应中加上了 Set-Cookie 这样的字段,浏览器就会自动存储刚才的这个 Cookie 数据

重启服务器,观察抓包效果:

Http 响应报文中就多了 Set-Cookie 字段:

此时浏览器端就保存了一个 Cookie

接下来访问服务器的其他页面时,Cookie 就会自动带上

刷新页面,查看请求报文:

进而服务器就可以根据这个 Cookie 的内容来判断当前页面对应的登陆状态

上述 Cookie 内容是 username=666,此时再访问 index/html 就不需要再登录
修改代码,实现上述功能:

private void doGet(Request request, Response response) throws IOException {
    //1.能够支持返回一个 html 文件
    if(request.getUrl().startsWith("/index.html")){
        String userName = request.getCookie("username");
        // userName == null 表示当前用户尚未登陆
        if(userName == null){
            // 让代码读取一个 /index.html 这样的文件
            // 要想读文件,需要先知道文件路径 (只知道文件名)
            // 此时 html 文件所属的路径,可以自己约定
            // 把文件内容写到响应的 body 中
            response.setStatus(200);
            response.setMessage("OK");
            response.setHeader("Content-Type","text/html; charset=utf-8");
            // HttpServerV3.class获取一个类对象
            // getClassLoader() 获取当前类的类加载器
            InputStream inputStream = HttpServerV3.class.getClassLoader().getResourceAsStream("index.html");
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            //按行读取内容,把数据写入到 response 中
            String lint = " ";
            while ((lint = bufferedReader.readLine()) != null){
                response.writeBody(lint + "\n");
            }
            bufferedReader.close();
        }
        // 已经登陆了
        else {
            response.setStatus(200);
            response.setMessage("OK");
            response.setHeader("Content-Type","text/html; charset=utf-8");
            response.writeBody("<html>");
            response.writeBody("您已登录,无需再次登录!用户名: " + userName);
            response.writeBody("</html>");
        }
    }
}

重启服务器,刷新页面:

刷新页面,此时的 http 请求中带有了 Cookie 信息,服务器分析了 Cookie 之后,认为已经登陆成功,此时就不需要再继续登录

借助 Cookie 已经实现了登录保持功能,但用户信息在 Cookie 中,每次数据传输都要把这个 Cookie 再发给服务器,那么 Cookie 中的信息就很容易泄露
应该把用户的敏感信息保存到服务器端,在服务器端登陆成功时,把用户信息保存到一个哈希表中(value),同时生成一个 key (保证唯一性的字符串) SessionId,最终只要把 SessionId 写回到 Cookie 中即可
后续用户访问页面时,Cookie 中带有的内容就是 SessionId,SessionId 就是一个没规律的字符串(不涉及敏感信息),但服务器可以通过 SessionId 进一步找到用户的信息

再次修改代码,实现上述功能:

//保存用户的相关信息
static class User {
    public String username;
    public int age;
    public String school;
}
// sessions 会话 —— 指的是同一个用户的一组访问服务器的操作,归类到一起,就是一个会话
private HashMap<String,User> sessions = new HashMap<>();

重启服务器,删除浏览器 Cookie,刷新页面:

首次访问的时候,由于用户浏览器中没有 Cookie,此时看到的页面是一个未登录的状态

请求中没有 Cookie

输入用户名密码,提交表单,进行验证登录:

请求中也没有 Cookie

响应中带有 Set-Cookie字段

进而浏览器就会保存这个内容 (sessionId 内容)

接下来再次访问服务器时,此时浏览器就会带有 Cookie

服务器会根据此处的 sessionId 就能获取到当前用户的登陆状态

sessionId 在 Cookie 中存在,并且拿着 sessionId 也能在 sessions 中找到对应的 User 信息,从而就可以确认现在是登陆成功状态
用户信息都在服务器中,不涉及泄露问题~

但是: 若别人拿到 sessionId,短期内是可以冒充该用户的
因此 session 一般要搭配"过期机制",可以在 session 中保存一个该用户的 session 何时创建和何时过期;可以使用一个单独的线程不停的扫描 sessions,只要发现过期的 session 就自动删掉,后续再使用这个过期的 session 去访问,此时就会让你重新登陆

附最终全部代码:

Request 类

public class Request {
    private String method;
    private String url;
    private String version;
    private Map<String,String> headers = new HashMap<>();
    // url 中的参数和 body中的参数都放在 parameters 哈希表里
    private Map<String,String> parameters = new HashMap<>();
    private Map<String,String> cookies = new HashMap<>();
    private String body;

    public static Request build(InputStream inputStream) throws IOException {
        Request request = new Request();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        //1.处理首行
        String fistLine = bufferedReader.readLine();
        String[] firstLineTokens = fistLine.split(" ");
        request.method = firstLineTokens[0];
        request.url = firstLineTokens[1];
        request.version = firstLineTokens[2];
        //2.解析 url
        int pos = request.url.indexOf("?");
        if(pos != -1){
            String queryString = request.url.substring(pos + 1);
            parseKV(queryString,request.parameters);
        }
        //3.循环处理 header 部分
        String line = "";
        while ((line = bufferedReader.readLine()) != null && line.length() != 0){
            String[] headerTokens = line.split(": ");
            request.headers.put(headerTokens[0],headerTokens[1]);
        }
        //4.解析 cookie
        String cookie = request.headers.get("Cookie");
        if(cookie != null){
            // 把 cookie 进行解析
            parseCookie(cookie,request.cookies);
        }
        //5.解析 body
        if("POST".equalsIgnoreCase(request.method)
            || "PUT".equalsIgnoreCase(request.method)) {
            // 其他方法暂时不考虑
            // 需要把 body 读取出来
            // 此处的长度单位是 "字节"
            int contentLength = Integer.parseInt(request.headers.get("Content-Length"));
            // contentLength 长度单位是字节
            // contentLength 为100, body中有100个字节
            // 创建的缓冲区长度是 100个char (相当于200个字符)
            char[] buffer = new char[contentLength];
            int len = bufferedReader.read(buffer);
            request.body = new String(buffer,0,len);
            // body 中的格式形如: username=huahua&password=666
            parseKV(request.body, request.parameters);
        }
        return request;
    }

    private static void parseCookie(String cookie, Map<String, String> cookies) {
        //1.按照 "; " 拆分成多个键值对
        String[] KVTokens = cookie.split(": ");
        //2.按照 = 拆分每个键和值
        for(String kv : KVTokens){
            String[] result = kv.split("=");
            cookies.put(result[0],result[1]);
        }
    }

    private static void parseKV(String queryString, Map<String, String> parameters) {
        //1.按照 & 拆分成多个键值对
        String[] KVTokens = queryString.split("&");
        //2.按照 = 拆分每个键和值
        for(String kv : KVTokens){
            String[] result = kv.split("=");
            parameters.put(result[0],result[1]);
        }
    }

    public String getMethod() {
        return method;
    }

    public String getUrl() {
        return url;
    }

    public String getVersion() {
        return version;
    }

    public String getBody() {
        return body;
    }

    public String getParameter(String key) {
        return parameters.get(key);
    }

    public String getHeader(String key) {
        return headers.get(key);
    }

    public String getCookie(String key) {
        return cookies.get(key);
    }
}

Response 类

public class Response {
    private String version = "HTTP//1.1";
    private int status;
    private String message;
    private Map<String,String> headers = new HashMap<>();
    private StringBuilder body = new StringBuilder(); //方便拼接
    private OutputStream outputStream = null;

    //工厂方法
    public static Response build(OutputStream outputStream){
        Response response = new Response();
        response.outputStream = outputStream;
        return response;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public void setHeader(String key,String value) {
        headers.put(key,value);
    }

    public void writeBody(String content) {
        body.append(content);
    }

    public void flush() throws IOException {
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
        bufferedWriter.write(version + " " + status + " " + message + "\n");
        headers.put("Content-Length", body.toString().getBytes().length + " ");
        for (Map.Entry<String,String> entry : headers.entrySet()) {
            bufferedWriter.write(entry.getKey() + ": " + entry.getValue() + "\n");
        }
        bufferedWriter.write("\n");
        bufferedWriter.write(body.toString());
        bufferedWriter.flush();
    }
}

主类

public class HttpServerV3 {
    //保存用户的相关信息
    static class User {
        public String username;
        public int age;
        public String school;
    }
    private ServerSocket serverSocket = null;
    // sessions 会话 —— 指的是同一个用户的一组访问服务器的操作,归类到一起,就是一个会话
    private HashMap<String,User> sessions = new HashMap<>();

    public HttpServerV3(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动...");
        ExecutorService executorService = Executors.newCachedThreadPool();
        while (true) {
            Socket clientSocket = serverSocket.accept();
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    process(clientSocket);
                }
            });
        }
    }
    public void process(Socket clientSocket){
        try {
            //1.读取请求并解析
            Request request = Request.build(clientSocket.getInputStream());
            Response response = Response.build(clientSocket.getOutputStream());
            //2.根据请求计算响应 按照不同的 Http 方法,拆分成不同的逻辑
            if("GET".equalsIgnoreCase(request.getMethod())){
                doGet(request,response);
            }
            else if("POST".equalsIgnoreCase(request.getMethod())){
                doPost(request,response);
            }
            else{
                // 其他方法,返回一个405
                response.setStatus(405);
                response.setMessage("Method Not Allowed");
            }
            //3.把响应写回客户端
            response.flush();
        } catch (IOException | NullPointerException e) {
            e.printStackTrace();
        } finally {
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void doGet(Request request, Response response) throws IOException {
        //1.能够支持返回一个 html 文件
        if(request.getUrl().startsWith("/index.html")){
            String sessionId = request.getCookie("sessionId");
            // userName == null 表示当前用户尚未登陆
            User user = sessions.get(sessionId);
            if(sessionId == null || user == null){
                // 让代码读取一个 /index.html 这样的文件
                // 要想读文件,需要先知道文件路径 (只知道文件名)
                // 此时 html 文件所属的路径,可以自己约定
                // 把文件内容写到响应的 body 中
                response.setStatus(200);
                response.setMessage("OK");
                response.setHeader("Content-Type","text/html; charset=utf-8");
                // HttpServerV3.class获取一个类对象
                // getClassLoader() 获取当前类的类加载器
                InputStream inputStream = HttpServerV3.class.getClassLoader().getResourceAsStream("index.html");
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                //按行读取内容,把数据写入到 response 中
                String lint = " ";
                while ((lint = bufferedReader.readLine()) != null){
                    response.writeBody(lint + "\n");
                }
                bufferedReader.close();
            }
            // 已经登陆了
            else {
                response.setStatus(200);
                response.setMessage("OK");
                response.setHeader("Content-Type","text/html; charset=utf-8");
                response.writeBody("<html>");
                response.writeBody("<div>" + "您已登录,无需再次登录!用户名: " + user.username + "</div>");
                response.writeBody("<div>" + user.age + "</div>");
                response.writeBody("<div>" + user.school + "</div>");
                response.writeBody("</html>");
            }
        }
    }

    private void doPost(Request request, Response response) {
        //2.实现 /login 的处理
        if(request.getUrl().startsWith("/login")) {
            //读取用户提交的用户名和密码
            String userName = request.getParameter("username");
            String password = request.getParameter("password");
// System.out.println("userName: " + userName);
// System.out.println("password: " + password);
            //登录逻辑需要验证用户名密码是否正确
            // 此处为了简单,把用户名和密码在代码中写死
            // 更科学的处理方式: 从数据库中读取用户名对应密码,检验密码是否一致
            if("hh".equals(userName) && "666".equals(password)){
                // 登陆成功
                response.setStatus(200);
                response.setMessage("OK");
                response.setHeader("Content-Type","text/html;charset=utf-8");
                // 原来登录成功是给浏览器写了一个 cookie,cookie中保存的是用户名
                //response.setHeader("Set-Cookie","username=" + userName);

                // 现有的对于登录成功的处理,给这次登录的用户分配了一个 session
                // (在 hash 中新增了一个键值对),key是随机生成的,value 就是用户的身份信息
                // 身份信息保存到服务器中,此时就不再有泄露信息的问题
                // 给浏览器返回的 Cookie 中只需要包含 sessionId
                String sessionId = UUID.randomUUID().toString(); // 会生成一个随机的字符串,能够保证每次调用这个方法,生成的字符串都不一样
                User user = new User();
                user.username = "hh";
                user.age = 3;
                user.school = "Coding666";
                sessions.put(sessionId,user);
                response.setHeader("Set-Cookie","sessionId=" + sessionId);

                response.writeBody("<html>");
                response.writeBody("<div>欢迎您!" + userName + "</div>");
                response.writeBody("</html>");
            }
            else{
                // 登陆失败
                response.setStatus(403);
                response.setMessage("Forbidden");
                response.setHeader("Content-Type","text/html;charset=utf-8");
                response.writeBody("<html>");
                response.writeBody("<div>登陆失败</div>");
                response.writeBody("</html>");
            }
        }
    }

    public static void main(String[] args) throws IOException {
        HttpServerV3 httpServerV3 = new HttpServerV3(6060);
        httpServerV3.start();
    }
}

相关文章