JWT 登录校验

会话技术

会话:用户打开浏览器,访问 web 服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应

会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据

会话跟踪方案:

  • 客户端会话跟踪技术:Cookie
  • 服务端会话跟踪技术:Session
  • 令牌技术

Cookie 是 HTTP 协议支持的技术,在浏览器第一次访问时,服务端请求头中设置 Cookie Set-Cookie: your_cookie,浏览器会自动把 Cookie 存在本地,并在下次访问时自动在请求头中加上 Cookie Cookie: your_cookie

@RestController
public class SessionController {
    // 设置Cookie
    @GetMapping("/c1")
    public Result cookie1(HttpServletResponse response){
        response.addCookie(new Cookie("login_name", "yexca"));
        return Result.success();
    }

    // 获取Cookie
    @GetMapping("/c2")
    public Result cookie2(HttpServletRequest request){
        Cookie[] cookies = request.getCookies();
        for (Cookie c : cookies){
            if(c.getName().equals("login_name")){
                System.out.println("login_name:"+c.getValue());
            }
        }
        return Result.success();
    }
}

不过移动端不能使用 Cookie,并且 Cookie 无法跨域

相同域:同协议、IP/域名、端口

Session

基于 Cookie 实现,在浏览器第一次请求时生成一个 Session,然后在响应头返回 Session 的 ID Set-Cookie: JSESSIONID=session_id,然后浏览器在下次请求时自动带上该 ID

@RestController
public class SessionController {
    // 往HttpSession中存储值
    @GetMapping("/s1")
    public Result session1(HttpSession session){
        log.info("HttpSession_set:{}", session.hashCode());
        session.setAttribute("login_user","yexca");
        return Result.success();
    }

    // 从HttpSession中获取值
    @GetMapping("/s2")
    public Result session2(HttpServletRequest request){
        HttpSession session = request.getSession();
        log.info("HttpSession_get:{}", session.hashCode());

        Object loginUser = session.getAttribute("login_user");
        log.info("loginUser:{}", loginUser);
        return Result.success();
    }
}

而 Session 的问题在于若服务端使用负载均衡,即有多台服务端,若第一次响应的为服务器一,而第二次响应为服务器二,由于服务器二里没有 Session,所以无法使用

JWT 令牌

令牌技术支持 PC 端、移动端,可以解决集群环境下的认证问题,减轻服务器端存储压力,但需要自己实现

JWT 全称 JSON Web Token,官网: https://jwt.io/ ,定义了一种简洁的、自包含的格式,用于在通信双方以 JSON 数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的

组成:

  • 第一部分:Header,记录令牌类型、签名算法等
  • 第二部分:Payload (有效载荷),记录一些自定义信息、默认信息等
  • 第三部分:Signature (签名),防止 Token 被篡改、确保安全性。将 Header、Payload 和指定密钥,通过指定签名算法计算而来

引入依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

生成与解析

class WebApplicationTests {
    // JWT生成
    @Test
    public void jwtGenTest(){

        Map<String,Object> claims = new HashMap<>();
        claims.put("id", "233");
        claims.put("user", "yexca");
        String jwt = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, "yexca") // 签名算法
                .setClaims(claims) //自定义内容(载荷)
                .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) // 有效期为1h
                .compact();
        System.out.println(jwt);
    }
    
// 输出
// eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIzMyIsImV4cCI6MTcwMjc5NzA4OCwidXNlciI6InlleGNhIn0.BqZDxEddGN4g4GyyfvkOtKYv7DVlIF6cWY9PGW2RbUU
    
    // JWT解析
    @Test
    public void jwtParseTest(){
        Claims claims = Jwts.parser()
                .setSigningKey("yexca") // 密钥
                .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIzMyIsImV4cCI6MTcwMjc5NzA4OCwidXNlciI6InlleGNhIn0.BqZDxEddGN4g4GyyfvkOtKYv7DVlIF6cWY9PGW2RbUU")
                .getBody(); // 获得第二部分
        System.out.println(claims);
    }
    
// 输出
// {id=233, exp=1702797088, user=yexca}
}

登录校验

过滤器 (Filter)

Filter 是早期 JavaWeb 三大组件 (Servlet, Filter, Listener) 之一。过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能,如登录校验、统一编码处理、敏感字符处理等

快速入门

创建 Filter 类

@WebFilter(urlPatterns = "/*") // 拦截路径
public class DemoFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
//        Filter.super.init(filterConfig);
        System.out.println("Filter 初始化");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("Filter 拦截");

        // 放行
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
//        Filter.super.destroy();
        System.out.println("Filter 销毁");
    }
}

在 Application 加上注解

@ServletComponentScan // 开启了对servlet组件的支持
@SpringBootApplication

拦截路径

可以根据需求调整,以下为示例

拦截类型 urlPatterns 含义
拦截具体路径 /login 只有访问 /login 才会被拦截
目录拦截 /emps/* 访问 /emps 下所有资源都会被拦截,/emps 也会拦截
拦截所有 /* 访问所有资源都会被拦截

单个过滤器执行逻辑

浏览器发送请求 -> 请求被拦截 -> 执行放行前逻辑 -> 放行 -> 执行放行后逻辑 -> 浏览器收到响应

@WebFilter(urlPatterns = "/*") // 拦截路径
public class DemoFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
//        Filter.super.init(filterConfig);
        System.out.println("Filter 初始化");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("Filter 拦截,放行前逻辑");

        // 放行
        filterChain.doFilter(servletRequest, servletResponse);

        System.out.println("Filter 拦截,放行后逻辑");
    }

    @Override
    public void destroy() {
//        Filter.super.destroy();
        System.out.println("Filter 销毁");
    }
}

过滤器链

一个 Web 应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链

顺序:注解配置的 Filter 优先级是通过过滤器类名的自然排序

逻辑:浏览器发送请求 -> 请求被 A 拦截 -> 执行 A 放行前逻辑 -> A 放行 -> 请求被 B 拦截 -> 执行 B 放行前逻辑 -> B 放行 -> 执行 B 放行后逻辑 -> 执行 A 放行后逻辑 -> 浏览器收到响应

登录校验

@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        // 获取请求url
        String requestURI = request.getRequestURI();
        log.info("请求URL:{}",requestURI);

        // 判断是否有login
        if(requestURI.contains("login")){
            log.info("登录操作,放行");
            filterChain.doFilter(servletRequest, servletResponse);
            return; // 因为登录操作不需要以下逻辑
        }

        // 非登录,获取令牌
        String jwt = request.getHeader("token");

        // 判断令牌是否有效
        if(!StringUtils.hasLength(jwt)){
            log.info("请求头token为空");
            // 将对象转换为JSON
            Result error = Result.error("NOT_LOGIN");
            String notLogin = JSONObject.toJSONString(error);
            response.getWriter().write(notLogin);
            return;
        }

        // 校验令牌
        try {
            JwtUtils.parseJWT(jwt);
        }catch (Exception e){
            e.printStackTrace();
            log.info("解析令牌失败");
            // 将对象转换为JSON
            Result error = Result.error("NOT_LOGIN");
            String notLogin = JSONObject.toJSONString(error);
            response.getWriter().write(notLogin);
            return;
        }

        // 放行
        log.info("令牌合法,放行");
        filterChain.doFilter(servletRequest,servletResponse);
    }
}

拦截器 (Interceptor)

快速入门

创建拦截器

@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    @Override // 目标方法运行前运行,返回ture放行,false不放行
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        return HandlerInterceptor.super.preHandle(request, response, handler);
        System.out.println("preHandle");
        return true;
    }

    @Override // 目标方法运行后运行
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
        System.out.println("postHandle");
    }


    @Override // 视图渲染完成后运行,最后运行
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
        System.out.println("afterCompletion");
    }
}

创建配置

@Configuration // 配置类
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
//        WebMvcConfigurer.super.addInterceptors(registry);
        registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");

    }

}

拦截路径

路径 含义 例子
/* 一级路径 /emps, /login 不能匹配 /emps/1
/** 任意级路径 /emps, /emps/1, /emps/1/2
/emps/* /emps 下的一级路径 /emps/1 不能匹配 /emps, /emps/1/2
/emps/** /emps 下的任意级路径 /emps, /emps/1, /emps/1/2

拦截流程

如果同时存在过滤器和拦截器

浏览器访问 -> filter 放行前逻辑 -> filter 放行 -> DispatcherServlet -> Interceptor preHandle -> Controller -> postHandle -> afterCompletion -> DispatcherServlet -> filter 放行后逻辑 -> 响应浏览器

Filter 会拦截所有的请求,而 Interceptor 只会拦截 Spring 环境中的资源

登录校验

@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    @Override // 目标方法运行前运行,返回ture放行,false不放行
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        return HandlerInterceptor.super.preHandle(request, response, handler);
        System.out.println("preHandle");

        // 获取请求url
        String requestURI = request.getRequestURI();
        log.info("请求URL:{}",requestURI);

        // 判断是否有login
        if(requestURI.contains("login")){
            log.info("登录操作,放行");
            return true;
        }

        // 非登录,获取令牌
        String jwt = request.getHeader("token");

        // 判断令牌是否有效
        if(!StringUtils.hasLength(jwt)){
            log.info("请求头token为空");
            // 将对象转换为JSON
            Result error = Result.error("NOT_LOGIN");
            String notLogin = JSONObject.toJSONString(error);
            response.getWriter().write(notLogin);
            return false;
        }

        // 校验令牌
        try {
            JwtUtils.parseJWT(jwt);
        }catch (Exception e){
            e.printStackTrace();
            log.info("解析令牌失败");
            // 将对象转换为JSON
            Result error = Result.error("NOT_LOGIN");
            String notLogin = JSONObject.toJSONString(error);
            response.getWriter().write(notLogin);
            return false;
        }

        // 放行
        log.info("令牌合法,放行");
        return true;
    }

    @Override // 目标方法运行后运行
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
        System.out.println("postHandle");
    }


    @Override // 视图渲染完成后运行,最后运行
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
        System.out.println("afterCompletion");
    }
}