「SpringBoot Demo」01.SpringBoot+Filter模拟实现无状态登录

这个Demo用SpringBoot实现一个小小的登录功能,同时这个登录功能没有使用Session,用token代替,实现了服务器的无状态。

主要参考了下面4篇文章,

  1. 干掉状态:从session到token
  2. 设计一个可扩展的用户登录系统 (1)
  3. 设计一个可扩展的用户登录系统 (2)
  4. 设计一个可扩展的用户登录系统 (3)

一、创建SpringBoot项目,实现简单的页面访问功能

1.创建一个简单的Maven项目

1.png

2.修改pom文件,添加web组件和Freemarker组件

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-freemarker</artifactId>
    </dependency>
</dependencies>

3.创建控制器,Freemarker页面和启动类
IndexController.java

package org.mkh.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * 
 * @author mo
 * @date 2019/06/21
 */
@Controller
public class IndexController {

    @GetMapping("/index")
    public String index(Model model) {
        return "index";
    }
}

index.ftl

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Welcome</title>
</head>
<body>
Welcome!
</body>
</html>

App.java

package org.mkh;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

/**
 * 
 * @author mo
 * @date 2019/06/20
 */
@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

}

4.测试运行通过,demo框架搭建完毕。

2.png

二、实现User的业务逻辑

本Demo只做一个模拟登录功能,所以不做数据持久化,用HashMap存放数据代替。

1.创建实体类User

package org.mkh.entity;

public class User {

    private Integer id;
    private String username;
    private String nickname;
    private String password;

    public User() {}

    public User(Integer id, String username, String nickname, String password) {
        this.id = id;
        this.username = username;
        this.nickname = nickname;
        this.password = password;
    }

    @Override
    public String toString() {
        return "User [id=" + id + ", username=" + username + ", nickname=" + nickname + ", password=" + password + "]";
    }

   //省略Getter,Setter
}

2.定义UserServive接口,提供3个方法

package org.mkh.service;

import java.util.List;

import org.mkh.entity.User;

public interface UserService {

    // 通过username获取用户
    User getUserByUsername(String username);

    // 通过userId获取用户
    User getUserbyId(Integer userId);

    // 查找所有用户
    List<User> findAllUser();

}

3.实现UserService接口

package org.mkh.service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.mkh.entity.User;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {

    private static Map<String, User> users = new HashMap<>();

    static {
        users.put("zs", new User(99991001, "zs", "张三", "zs123"));
        users.put("ls", new User(99991002, "ls", "李四", "ls123"));
        users.put("ww", new User(99991003, "ww", "王五", "ww123"));
        users.put("zl", new User(99991004, "zl", "赵六", "zl123"));
        users.put("tq", new User(99991005, "tq", "田七", "tq123"));
    }

    @Override
    public User getUserByUsername(String username) {
        return users.get(username);
    }

    @Override
    public List<User> findAllUser() {
        return new ArrayList<User>(users.values());
    }

    @Override
    public User getUserbyId(Integer userId) {
        for (String key : users.keySet()) {
            User user = users.get(key);
            if (user.getId().intValue() == userId) {
                return user;
            }
        }
        return null;
    }

}

4.实现UserController

package org.mkh.controller;

import org.mkh.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/users")
    public String users(Model model) {
        model.addAttribute("users", userService.findAllUser());
        return "user-list";
    }
}

5.实现user-list页面

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Welcome</title>
</head>
<body>
    <table border="1" cellspacing="0" cellpadding="0" width="500" align="center">
        <thead>
            <tr>
                <th>ID</th>
                <th>Username</th>
                <th>Nickname</th>
                <th>Password</th>
            </tr>
        </thead>
        <tbody>
            <#list users as user>
                <tr>
                    <td>${user.id}</td>
                    <td>${user.username}</td>
                    <td>${user.nickname}</td>
                    <td>${user.password}</td>
                </tr> 
            </#list>
        </tbody>
    </table>
</body>
</html>

6.启动项目,查看用户列表

3.png

三、实现简单的登录

1.实现登录控制器,登录成功跳转index页面,失败跳转signin-error页面

package org.mkh.controller;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;

import org.mkh.entity.User;
import org.mkh.service.UserService;
import org.mkh.util.CookieUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

/**
 * 
 * @author mo
 * @date 2019/06/20
 */
@Controller
public class SigninController {

    @Autowired
    private UserService userService;

    @GetMapping("/signin")
    public String signInput() {
        return "signin";
    }

    @PostMapping("/signin")
    public String sign(String username, String password, HttpServletResponse resp) {
        System.out.println(username + "-" + password);
        User user = userService.getUserByUsername(username);
        if (user != null && user.getPassword().equals(password)) {            
            return "redirect:/index";
        }
        return "signin-error";
    }
}

2.signin页面

<html>
<head>
    <meta charset="utf-8">
    <title>sign</title>
</head>
<body>
    <form action="/signin" method="post">
        Username:<input type="text" name="username" /><br>
        Password:<input type="password" name="password" /><br>
        <input type="submit" value="submit"/>
    </form>
</body>
</html>

3.signin-error页面

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Error</title>
</head>
<body>
Login failed! username incorrected or password incorrected! 
</body>
</html>

至此,已完成了一个简单的登录功能,但是这个登录功能非常简陋,没有过滤器,没有存Session等等。

四、实现无状态的登录

所谓无状态就是服务器不记录用户的会话,每个请求对于服务器来说都是一个新的会话。HTTP协议是一个无状态的协议。无状态的关键在于取消Session,用token代替。服务器只负责生成token,发放token和验证token的合法性,服务器并不存储token,token交给客户端存储,客户端每发一个请求都会把token发送到服务器。
原理部分,大家可以看看上面的4篇文章。下面开始实现。

1.编写工具类ApplicationContextUtil,用于把Spring容器管理的Bean注入到普通Java类中

package org.mkh.util;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * 
 * @author mo
 * @date 2019/06/21
 */
@Component
public class ApplicationContextUtil implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return context;
    }

    public static Object getBean(String name) {
        return context.getBean(name);
    }

    public static <T> T getBean(Class<T> clz) {
        return context.getBean(clz);
    }

    public static <T> T getBean(String name, Class<T> clz) {
        return context.getBean(name, clz);
    }

}

2.编写工具类CookieUtil,用于生成Cookie和验证Cookie的合法性

package org.mkh.util;

import java.time.Instant;

import javax.servlet.http.Cookie;

import org.mkh.auth.LocalCookieAuthenticator;
import org.mkh.entity.User;
import org.springframework.util.DigestUtils;

public class CookieUtil {

    // 服务器密钥
    private static final String SECRET_KEY = "secret";
    // Cookie的组成部分的数目
    private static final short PART_NUM = 3;

    /**
     * 生成cookie
     * 
     * @param user
     * @return
     */
    public static Cookie generateCookie(User user) {
        int uid = user.getId();
        String password = user.getPassword();
        long expires = Instant.now().plusSeconds(60).toEpochMilli(); // 60秒过期

        String value = uid + ":" + password + ":" + expires + ":" + SECRET_KEY;

        // 使用MD5加密,生成签名
        String signature = DigestUtils.md5DigestAsHex(value.getBytes());

        String cookieVal = uid + ":" + expires + ":" + signature;

        return new Cookie(LocalCookieAuthenticator.USER_AUTH_COOKIE, cookieVal);
    }

    /**
     * 验证Cookie合法性
     * 
     * @param cookie
     * @return
     */
    public static String[] splitCookie(String cookie) {
        String[] values = cookie.split(":");
        if (values.length != PART_NUM) {
            System.out.println("cookie不合法,可能被篡改");
            return null;
        }

        for (String v : values) {
            if (v == null || "".equals(v)) {
                System.out.println("cookie数据缺失");
                return null;
            }
        }

        return values;
    }

    /**
     * 验证cookie是否过期
     * 
     * @param expires
     * @return
     */
    public static boolean validateCookieExpire(long expires) {
        if (expires <= System.currentTimeMillis()) {
            System.out.println("cookie 已过期");
            return false;
        }
        return true;
    }

    public static boolean validateCookieSignature(int userId, long expires, User user, String clientSignature) {
        // 用客户端传过来的数据生成签名
        String value = userId + ":" + user.getPassword() + ":" + expires + ":" + SECRET_KEY;
        String serverSignature = DigestUtils.md5DigestAsHex(value.getBytes());
        System.out.println("server:" + serverSignature);
        System.out.println("client:" + clientSignature);

        // 比较用户上传的签名与生成的签名是否一样
        if (serverSignature.equals(clientSignature)) {
            System.out.println("cookie验证成功");
            return true;
        }
        return false;
    }
}

3.定义一个认证的接口,认证成功返回认证用户。可以有多种不同的认证方式,实现该接口即可

package org.mkh.auth;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.mkh.entity.User;
import org.mkh.ex.AuthenticateException;

public interface Authenticator {

    // 认证成功返回User,认证失败抛出异常,无认证信息返回null:
    User authenticate(HttpServletRequest request, HttpServletResponse response) throws AuthenticateException;

}

4.实现上面的接口,用Cookie来认证

package org.mkh.auth;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.mkh.entity.User;
import org.mkh.ex.AuthenticateException;
import org.mkh.service.UserService;
import org.mkh.util.ApplicationContextUtil;
import org.mkh.util.CookieUtil;

public class LocalCookieAuthenticator implements Authenticator {

    public static final String USER_AUTH_COOKIE = "UserInfo";

    @Override
    public User authenticate(HttpServletRequest request, HttpServletResponse response) throws AuthenticateException {
        String cookie = getCookieFromRequest(request, USER_AUTH_COOKIE);
        if (cookie == null) {
            return null;
        }
        return getUserByCookie(cookie);
    }

    // 从request中获取指定名称的Cookie
    private String getCookieFromRequest(HttpServletRequest request, String cookieName) {
        Cookie[] cookies = request.getCookies();
        if (cookies == null) {
            return null;
        }
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals(cookieName)) {
                return cookie.getValue();
            }
        }

        return null;
    }

    /**
     * 通过cookie查找用户
     * 
     * @param cookie
     * @return
     */
    private User getUserByCookie(String cookie) {

        // 验证cookie的格式是否符合规范
        String[] values = CookieUtil.splitCookie(cookie);
        if (values == null) {
            throw new AuthenticateException(10001, "认证失败");
        }

        // 验证数据的合法性
        Integer userId = Integer.parseInt(values[0]);
        long expires = Long.parseLong(values[1]);
        String clientSignature = values[2];

        // cookie 过期
        if (!CookieUtil.validateCookieExpire(expires)) {
            throw new AuthenticateException(10002, "会话过期");
        }

        // 获取用户
        // 通过ApplicationContextUtil注入userService
        UserService userService = ApplicationContextUtil.getBean(UserService.class);
        User user = userService.getUserbyId(userId);
        if (user == null) {
            System.out.println("没有此用户");
            throw new AuthenticateException(10003, "认证失败");
        }

        // 验证签名
        if (!CookieUtil.validateCookieSignature(userId, expires, user, clientSignature)) {
            System.out.println("cookie验证失败");
            throw new AuthenticateException(10004, "认证失败");
        }

        return user;
    }

}

5.实现SigninFilter拦截未认证用户的访问

package org.mkh.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.mkh.auth.Authenticator;
import org.mkh.auth.LocalCookieAuthenticator;
import org.mkh.entity.User;
import org.mkh.entity.UserContext;
import org.mkh.ex.AuthenticateException;

@WebFilter(filterName = "SigninFilter", urlPatterns = {"/index", "/users"})
public class SigninFilter implements Filter {

    private Authenticator[] authenticators = initAuthenticators();

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // TODO Auto-generated method stub

    }

    private Authenticator[] initAuthenticators() {
        Authenticator[] authenticators = new Authenticator[] {new LocalCookieAuthenticator()};
        return authenticators;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
        System.out.println("start filter................");
        HttpServletResponse resp = (HttpServletResponse)response;
        HttpServletRequest req = (HttpServletRequest)request;
        User user = tryGetAuthenticatedUser(req, resp);

        if (user == null) {
            resp.sendRedirect("/signin");
            return;
        }

        try (UserContext context = new UserContext(user)) {
            chain.doFilter(request, response);
        }

    }

    public User tryGetAuthenticatedUser(HttpServletRequest request, HttpServletResponse response) {
        User user = null;
        for (Authenticator authenticator : authenticators) {
            try {
                user = authenticator.authenticate(request, response);
            } catch (AuthenticateException e) {
            }
            if (user != null) {
                break;
            }
        }
        return user;
    }

    @Override
    public void destroy() {
        // TODO Auto-generated method stub

    }

}

6.修改启动类,加入注解扫描

@SpringBootApplication
@ServletComponentScan(basePackages = {"org.mkh.filter"})
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

7.修改登录的Controller,登录成功后把Cookie写到客户端

    @PostMapping("/signin")
    public String sign(String username, String password, HttpServletResponse resp) {
        System.out.println(username + "-" + password);
        User user = userService.getUserByUsername(username);
        if (user != null && user.getPassword().equals(password)) {
            // 生成cookie
            Cookie cookie = CookieUtil.generateCookie(user);

            System.out.println("write new cookie:" + cookie.getValue());
            // 将cookie写入客户端
            resp.addCookie(cookie);
            return "redirect:/index";
        }
        return "signin-error";
    }

8.启动项目,直接访问http://localhost:8080/users,由于没有登录,过滤器会拦截请求,重定向到登录页面,登录后可以正常访问,查看Cookie,如图,Cookie正常写入。

4.png

由于设置了cookie的过期时间为60秒,一分钟后再次访问,请求会再次被拦截。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容