SpringMVC-登录校验

发布于:2025-03-17 ⋅ 阅读:(21) ⋅ 点赞:(0)

1.会话技术

  • 前因:HTTP是无状态协议,每次请求都是独立的,服务器不会自动保存任何前一次请求的上下文信息,无法满足需要“记住”用户状态的场景(如登录),需要通过会话技术(Cookie、Session、JWT)实现状态管理。

1.1 Cookie

  • cookie是客户端会话跟踪技术它是存储在客户端浏览器的,我们使用 cookie 来跟踪会话,我们就可以在浏览器第一次发起请求来请求服务器的时候,我们在服务器端来设置一个cookie。

    比如第一次请求了登录接口,登录接口执行完成之后,我们就可以设置一个cookie,在 cookie 当中我们就可以来存储用户相关的一些数据信息。比如我可以在 cookie 当中来存储当前登录用户的用户名,用户的ID。

    服务器端在给客户端在响应数据的时候,会自动的将 cookie 响应给浏览器,浏览器接收到响应回来的 cookie 之后,会自动的将 cookie 的值存储在浏览器本地。接下来在后续的每一次请求当中,都会将浏览器本地所存储的 cookie 自动地携带到服务端。

    在这里插入图片描述

    刚才在介绍流程的时候,用了 3 个自动:

    • 服务器会 自动 的将 cookie 响应给浏览器。

    • 浏览器接收到响应回来的数据之后,会 自动 的将 cookie 存储在浏览器本地。

    • 在后续的请求当中,浏览器会 自动 的将 cookie 携带到服务器端。

    为什么这一切都是自动化进行的?

    是因为 cookie 它是 HTP 协议当中所支持的技术,而各大浏览器厂商都支持了这一标准。在 HTTP 协议官方给我们提供了一个响应头和请求头:

    • 响应头 Set-Cookie :设置Cookie数据的
    • 请求头 Cookie:携带Cookie数据的

    在这里插入图片描述

    代码实现:

@Slf4j
@RestController
public class SessionController {

    //设置Cookie
    @GetMapping("/c1")
    public Result cookie1(HttpServletResponse response){
        response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie
        return Result.success();
    }
	
    //获取Cookie
    @GetMapping("/c2")
    public Result cookie2(HttpServletRequest request){
        Cookie[] cookies = request.getCookies();
        for (Cookie cookie : cookies) {
            if(cookie.getName().equals("login_username")){
                System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
            }
        }
        return Result.success();
    }
}   

A. 访问c1接口,设置Cookie,http://localhost:8080/c1

在这里插入图片描述

我们可以看到,设置的cookie,通过响应头Set-Cookie响应给浏览器,并且浏览器会将Cookie,存储在浏览器端。

在这里插入图片描述

B. 访问c2接口 http://localhost:8080/c2,此时浏览器会自动的将Cookie携带到服务端,是通过请求头Cookie,携带的。

在这里插入图片描述


优缺点

  • 优点:HTTP协议中支持的技术(像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携带,都是浏览器自动进行的,是无需我们手动操作的)。
  • 缺点:
    • 移动端APP(Android、IOS)中无法使用Cookie。
    • 不安全,用户可以自己禁用Cookie。
    • Cookie不能跨域

跨域介绍:

在这里插入图片描述

  • 现在的项目,大部分都是前后端分离的,前后端最终也会分开部署,前端部署在服务器 192.168.150.200 上,端口 80,后端部署在 192.168.150.100上,端口 8080
  • 我们打开浏览器直接访问前端工程,访问url:http://192.168.150.200/login.html
  • 然后在该页面发起请求到服务端,而服务端所在地址不再是localhost,而是服务器的IP地址192.168.150.100,假设访问接口地址为:http://192.168.150.100:8080/login
  • 那此时就存在跨域操作了,因为我们是在 http://192.168.150.200/login.html 这个页面上访问了http://192.168.150.100:8080/login 接口
  • 此时如果服务器设置了一个Cookie,这个Cookie是不能使用的,因为Cookie无法跨域

区分跨域的维度:

  • 协议
  • IP/协议
  • 端口

只要上述的三个维度有任何一个维度不同,那就是跨域操作

举例:

​ http://192.168.150.200/login.html ----------> https://192.168.150.200/login [协议不同,跨域]

​ http://192.168.150.200/login.html ----------> http://192.168.150.100/login [IP不同,跨域]

​ http://192.168.150.200/login.html ----------> http://192.168.150.200:8080/login [端口不同,跨域]

​ http://192.168.150.200/login.html ----------> http://192.168.150.200/login [不跨域]

1.2 Session

  • Session是服务端会话技术,所以它是存储在服务器端的。而 Session 的底层其实就是基于我们刚才所介绍的 Cookie 来实现的。

    1. 创建Session:如果是第一次请求Session ,会话对象是不存在的,这个时候服务器会自动的创建一个会话对象Session 。而每一个会话对象Session ,它都有一个ID(示意图中Session后面括号中的1,就表示ID),我们称之为 Session 的ID。

      在这里插入图片描述

    2. 响应Cookie (JSESSIONID):接下来,服务器端在给浏览器响应数据的时候,它会将 Session 的 ID 通过 Cookie 响应给浏览器。其实在响应头当中增加了一个 Set-Cookie 响应头。这个 Set-Cookie 响应头对应的值是不是cookie? cookie 的名字是固定的 JSESSIONID 代表的服务器端会话对象 Session 的 ID。浏览器会自动识别这个响应头,然后自动将Cookie存储在浏览器本地。

      在这里插入图片描述

    3. 查找Session:接下来,在后续的每一次请求当中,都会将 Cookie 的数据获取出来,并且携带到服务端。接下来服务器拿到JSESSIONID这个 Cookie 的值,也就是 Session 的ID。拿到 ID 之后,就会从众多的 Session 当中来找到当前请求对应的会话对象Session。

      在这里插入图片描述


      代码实现:

      @Slf4j
      @RestController
      public class SessionController {
      
          @GetMapping("/s1")
          public Result session1(HttpSession session){
              log.info("HttpSession-s1: {}", session.hashCode());
      
              session.setAttribute("loginUser", "tom"); //往session中存储数据
              return Result.success();
          }
      
          @GetMapping("/s2")
          public Result session2(HttpServletRequest request){
              HttpSession session = request.getSession();
              log.info("HttpSession-s2: {}", session.hashCode());
      
              Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
              log.info("loginUser: {}", loginUser);
              return Result.success(loginUser);
          }
      }
      

优缺点

  • 优点:Session是存储在服务端的,安全

  • 缺点:

    • 每个用户的登录信息都会保存到服务端器中,当用户增多将会增加服务端压力。
    • 服务器集群环境下无法直接使用Session
    • 对于非浏览器端包括移动端APP(Android、IOS)中无法使用Cookie
    • 用户可以自己禁用Cookie,Cookie如果被截获也有安全风险
    • Cookie不能跨域

PS:Session 底层是基于Cookie实现的会话跟踪,如果Cookie不可用,则该方案,也就失效了。所以大多数情况使用下述介绍的JWT进行用户认证。

1.3 JWT

1.3.1 JWT简介

  1. 定义:JWT(Json Web Token)通过数字签名的方式,以Json为载体,定义了一种简洁的、自包含的,用于在通信双方安全传输信息的技术。

  2. 工作流程

    1. 用户登录:客户端使用用户名和密码请求登录,服务端收到请求,验证用户名和密码。
    2. 生成JWT:登录成功,服务端生成保护用户关键信息的JWT返回给前端。
    3. 保存JWT:客户端接收到JWT后将JWT存储起来,后续客户端每次向服务端请求资源时需要携带服务端签发的JWT,可以在cookie或者header中携带。退出登录时删除保存的JWT。
    4. 校验JWT:服务端统一拦截请求,校验JWT(有效性、时效性、方法签名等)。
    5. 解析JWT:后端解析出JWT中包含的用户信息,根据用户信息进行权限校验、数据处理等相关操作,返回结果给前端。
  3. 优势:

    1. 支持跨域访问,解决了cookie无法跨域的问题,跨域后不会存在信息丢失问题,适用于分布式微服务。
    2. 减轻服务端压力,JWT本身包含了登录用户的信息,服务端无需刻意存储JWT。
    3. 适用性广,当客户端是非浏览器平台时,cookie是不被支持的,而JWT支持大多数web应用。
    4. 安全性高,通过非对称加密算法和数字签名技术,防止JWT被篡改。
    5. JWT基于Json,方便解析和拓展,可以在令牌中轻松的添加自定义内容。
  4. 劣势:

    1. JWT不支持会话管理,一旦签发无法撤回或修改,也不能主动使令牌失效,除非到了过期时间。
    2. JWT中的信息可以在客户端解码,因此敏感信息不应该存储在JWT中

1.3.2 JWT结构

  • JWT由Header(标头),PayLoad (负载),Signature(签名)三个部分组成,通过Base64编码方式分别转换成字符串,三个字符串之间使用.的点来分割。

    JWTString=Base64(Header).Base64(Payload).HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
    
  1. Header(标头):包含令牌类型签名算法,是一个描述JWT元数据的JSON对象,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。

    {
      "alg": "HS256",
      "typ": "JWT"
    }
    
  2. PayLoad(负载):有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择,除了默认字段外可以自定义私有字段(一般是包含用户信息的字段)。

    iss:发行人
    exp:到期时间
    sub:主题
    aud:用户
    nbf:在此之前不可用
    iat:发布时间
    jti:JWT ID用于标识该JWT
    
    {
      "sub": "1234567890",
      "name": "Helen",
      "admin": true
    }
    
  3. Signature(签名)防止JWT被篡改、确保安全性,需要使用Base64编码后的header和payload数据,通过指定的算法生成哈希。首先,需要指定一个密钥(secret)。该密钥仅仅为保存在服务器中,并且不能向用户公开。然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名:

    HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
    

    在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用.分隔,就构成整个JWT对象

    在这里插入图片描述


  • JWT各个部分的作用:

    在服务端接收到客户端发送过来的JWT token之后:

    • header和payload可以直接利用base64解码出原文,从header中获取哈希签名的算法,从payload中获取有效数据。
    • signature由于使用了不可逆的加密算法,无法解码出原文它的作用是校验token有没有被篡改。服务端获取header中的加密算法之后,利用该算法加上secretKey对header、payload进行加密,比对加密后的数据和客户端发送过来的是否一致。注意secretKey只能保存在服务端,而且对于不同的加密算法其含义有所不同,一般对于MD5类型的摘要加密算法,secretKey实际上代表的是盐值。
  • JWT是如何将原始的JSON格式数据,转变为字符串的呢?

    其实在生成JWT令牌时,会对JSON格式的数据进行一次编码:进行base64编码

    Base64:是一种基于64个可打印的字符来表示二进制数据的编码方式。既然能编码,那也就意味着也能解码。所使用的64个字符分别是A到Z、a到z、 0- 9,一个加号,一个斜杠,加起来就是64个字符。任何数据经过base64编码之后,最终就会通过这64个字符来表示。当然还有一个符号,那就是等号。等号它是一个补位的符号

    需要注意的是Base64是编码方式,而不是加密方式

  • 到目前为止,jwt的签名算法有三种:

    1. HMAC【哈希消息验证码(对称)】:HS256/HS384/HS512
    2. RSASSA【RSA签名算法(非对称)】(RS256/RS384/RS512)
    3. ECDSA【椭圆曲线数据签名算法(非对称)】(ES256/ES384/ES512)

1.3.3 使用JWT

  1. 引入依赖:

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    
  2. JWT工具类:

    public class JwtUtil {
        /**
         * 生成jwt
         * 使用Hs256算法, 私匙使用固定秘钥,可将密钥存储在服务端
         *
         * @param secretKey jwt秘钥
         * @param ttlMillis jwt过期时间(毫秒)
         * @param claims    负载
         * @return
         */
        public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
            // 指定签名的时候使用的签名算法,也就是header那部分
            SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
            // 生成JWT的时间
            long expMillis = System.currentTimeMillis() + ttlMillis;
            Date exp = new Date(expMillis);
            // 设置jwt的body
            String jwt = Jwts.builder()
                    // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                    .setClaims(claims)
                    // 设置签名使用的签名算法和签名使用的秘钥
                    .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                    // 设置过期时间
                    .setExpiration(exp)
                    .compact();
            return jwt;
        }
    
        /**
         * Token解密
         *
         * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则signature就可以被伪造, 如果对接多个客户端建议改造成多个
         * @param token     加密后的token
         * @return
         */
        public static Claims parseJWT(String secretKey, String token) {
            // 得到DefaultJwtParser
            Claims claims = Jwts.parser()
                    // 设置签名的秘钥
                    .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                    // 设置需要解析的jwt
                    .parseClaimsJws(token)
                    .getBody();
            return claims;
        }
    }
    

2.拦截技术

  • 如果需要在服务端统一拦截指定请求从而实现一些特殊的功能,就需要用到拦截技术,其中主要包含过滤器(Filter)拦截器(Interceptor)

  • 常见场景:登录校验、统一编码处理、敏感字符处理等。

  • 拦截器和过滤器之间的区别:

    1. 归属不同:Filter属于Servlet技术,Interceptor属于SpringMVC技术。
    2. 拦截范围不同:Filter对所有访问进行增强,Interceptor仅针对SpringMVC的访问进行增强。
    3. 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。

2.1 过滤器(Filter)

  • Filter是 JavaWeb三大组件(Servlet、Filter、Listener)之一。

    在这里插入图片描述

2.1.1 快速上手

下面我们通过Filter快速入门程序掌握过滤器的基本使用操作:

  1. 定义过滤器:定义一个类,实现 Filter 接口,并重写其所有方法,并在Filter类上加 @WebFilter 注解,同时配置拦截资源的路径。

    @WebFilter(urlPatterns = "/*") //配置过滤器要拦截的请求路径( /*) 
    public class DemoFilter implements Filter { 
        //初始化方法, 只调用一次
        @Override 
        public void init(FilterConfig filterConfig) throws ServletException {
            System.out.println("DemoFilter.init初始化方法执行了");
        }
    	
        //拦截到请求之后调用, 调用多次
        @Override 
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            System.out.println("DemoFilter执行放行前逻辑");
            chain.doFilter(request,response); //放行操作,如果不放行无法访问后续资源
            System.out.println("DemoFilter执行放行后逻辑");
        }
    	
        //销毁方法, 只调用一次
        @Override 
        public void destroy() {
            System.out.println("DemoFilter.destroy销毁方法执行了");
        }
    }
    
  2. SpringBoot启动类上加上 @ServletComponentScan 开启Servlet组件支持。

    @ServletComponentScan
    @SpringBootApplication
    public class TliasWebManagementApplication {
        public static void main(String[] args) {
            SpringApplication.run(TliasWebManagementApplication.class, args);
        }
    }
    
  3. 登录校验过滤器案例

    @Slf4j
    @WebFilter(urlPatterns = "/*") //拦截所有请求
    public class LoginCheckFilter implements Filter {
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
            //前置:强制转换为http协议的请求对象、响应对象 (转换原因:要使用子类中特有方法)
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            HttpServletResponse response = (HttpServletResponse) servletResponse;
            //1.获取请求url
            String url = request.getRequestURL().toString();
            log.info("请求路径:{}", url); //请求路径:http://localhost:8080/login
            //2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
            if(url.contains("/login")){
                chain.doFilter(request, response);//放行请求
                return;//结束当前方法的执行
            }
            //3.获取请求头中的令牌(token)
            String token = request.getHeader("token");
            log.info("从请求头中获取的令牌:{}",token);
            //4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
            if(!StringUtils.hasLength(token)){
                log.info("Token不存在");
                Result responseResult = Result.error("NOT_LOGIN");
                //把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
                String json = JSONObject.toJSONString(responseResult);
                response.setContentType("application/json;charset=utf-8");
                //响应
                response.getWriter().write(json);
                return;
            }
    
            //5.解析token,如果解析失败,返回错误结果(未登录)
            try {
                JwtUtils.parseJWT(token);
            }catch (Exception e){
                log.info("令牌解析失败!");
                Result responseResult = Result.error("NOT_LOGIN");
                //把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
                String json = JSONObject.toJSONString(responseResult);
                response.setContentType("application/json;charset=utf-8");
                //响应
                response.getWriter().write(json);
                return;
            }
            //6.放行
            chain.doFilter(request, response);
        }
    }
    

2.1.2 执行流程

首先我们先来看下过滤器的执行流程:

在这里插入图片描述

过滤器当中我们拦截到了请求之后,如果希望继续访问后面的web资源,就要执行放行操作,放行就是调用 FilterChain对象当中的doFilter()方法,在调用doFilter()这个方法之前所编写的代码属于放行之前的逻辑。

在放行后访问完 web 资源之后还会回到过滤器当中,回到过滤器之后如有需求还可以执行放行之后的逻辑,放行之后的逻辑我们写在doFilter()这行代码之后。

2.1.3 拦截路径

Filter可以根据需求,配置不同的拦截资源路径:

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

2.1.4 过滤器链

在一个web应用程序当中,可以配置多个过滤器,多个过滤器就形成了一个过滤器链。以注解方式配置的Filter过滤器,它的执行优先级是按时过滤器类名的自动排序确定的,类名排名越靠前,优先级越高

在这里插入图片描述

2.2 拦截器(Interceptor)

  • 拦截器是Spring框架中提供的,用来动态拦截控制器方法的执行,在指定方法调用前后,根据业务需要执行预先设定的代码。

2.2.1 快速上手

下面我们通过快速入门程序,来学习下拦截器的基本使用。拦截器的使用步骤和过滤器类似,也分为两步:

  1. 定义拦截器:实现HandlerInterceptor接口,并重写其所有方法。

    //自定义拦截器
    @Component
    public class LoginCheckInterceptor implements HandlerInterceptor {
        //目标资源方法执行前执行。 返回true:放行    返回false:不放行
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            System.out.println("LoginCheckInterceptor.preHandle .... ");
            return true; //true表示放行
        }
    
        //目标资源方法执行后执行
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            System.out.println("LoginCheckInterceptor.postHandle ... ");
        }
    
        //视图渲染完毕后执行,最后执行
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            System.out.println("LoginCheckInterceptor.afterCompletion .... ");
        }
    }
    
  2. 注册配置拦截器:实现WebMvcConfigurer接口,并重写addInterceptors方法。

    @Configuration  
    public class WebConfig implements WebMvcConfigurer {
        //自定义的拦截器对象
        @Autowired
        private LoginCheckInterceptor loginCheckInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
           //注册自定义拦截器对象
            registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
        }
    }
    

2.2.2 执行流程

介绍完拦截路径的配置之后,接下来我们再来介绍拦截器的执行流程。通过执行流程,大家就能够清晰的知道过滤器与拦截器的执行时机。

在这里插入图片描述

在这里插入图片描述

  • 当我们打开浏览器来访问部署在web服务器当中的web应用时,此时我们所定义的过滤器会拦截到这次请求。拦截到这次请求之后,它会先执行放行前的逻辑,然后再执行放行操作。而由于我们当前是基于springboot开发的,所以放行之后是进入到了spring的环境当中,也就是要来访问我们所定义的controller当中的接口方法。

  • Tomcat并不识别所编写的Controller程序,但是它识别Servlet程序,所以在Spring的Web环境中提供了一个非常核心的Servlet:DispatcherServlet(前端控制器),所有请求都会先进行到DispatcherServlet,再将请求转给Controller。

  • 当我们定义了拦截器后,会在执行Controller的方法之前,请求被拦截器拦截住。执行preHandle()方法,这个方法执行完成后需要返回一个布尔类型的值,如果返回true,就表示放行本次操作,才会继续访问controller中的方法;如果返回false,则不会放行(controller中的方法也不会执行)。

  • 在controller当中的方法执行完毕之后,再回过来执行postHandle()这个方法以及afterCompletion() 方法,然后再返回给DispatcherServlet,最终再来执行过滤器当中放行后的这一部分逻辑的逻辑。执行完毕之后,最终给浏览器响应数据。

2.2.3 拦截路径

  • 通过addPathPatterns("要拦截路径")方法,就可以指定要拦截哪些资源,调用excludePathPatterns("不拦截路径")方法,指定哪些资源不需要拦截。

    在拦截器中除了可以设置/**拦截所有资源外,还有一些常见拦截路径设置:

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

2.2.4 拦截器链

在一个Spring应用程序当中,可以配置多个拦截器,多个拦截器就形成了一个拦截器链,拦截器链的运行顺序参照拦截器添加顺序为准

在这里插入图片描述

preHandle:与配置顺序相同,必定运行

postHandle:与配置顺序相反,可能不运行

afterCompletion:与配置顺序相反,可能不运行

3.登入认证(案例)

  • 在实际的SpringBoot项目中,一般我们可以用如下流程做登录:

    1. 在登录验证通过后,给用户生成一个对应的随机token(注意这个token不是指jwt,可以用uuid等算法生成),然后将这个token作为key的一部分,用户信息作为value存入Redis,并设置过期时间,这个过期时间就是登录失效的时间。

    2. 将第1步中生成的随机token作为JWT的payload生成JWT字符串返回给前端。

    3. 前端之后每次请求都在请求头中的Authorization字段中携带JWT字符串。

    4. 后端定义一个拦截器,每次收到前端请求时,都先从请求头中的Authorization字段中取出JWT字符串并进行验证,验证通过后解析出payload中的随机token,然后再用这个随机token得到key,从Redis中获取用户信息,如果能获取到就说明用户已经登录。

      下述代码未使用redis暂存用户信息,只做简单示范

      @Component
      @Slf4j
      public class JwtTokenPatientInterceptor implements HandlerInterceptor {
          @Autowired
          private JwtProperties jwtProperties;
      
          @Override
          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
              //判断当前拦截到的是Controller的方法还是其他资源
              if (!(handler instanceof HandlerMethod)) {
                  //当前拦截到的不是动态方法,直接放行
                  return true;
              }
      
              // 1.从请求头中获取令牌
              String token = request.getHeader(jwtProperties.getUserTokenName());
      
              // 2.判断令牌是否存在,如果不存在,返回错误结果(未登录)
              if (StringUtils.isNull(token)) {
                  log.info("请求头token为空,返回未登录的信息");
                  response.setStatus(401);
                  return false;
              }
      
              // 3.校验令牌
              try {
                  log.info("jwt校验:{}", token);
                  Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
                  Long patientId = Long.valueOf(claims.get(JwtClaimsConstant.PATIENT_ID).toString());
                  log.info("当前用户的id:" + patientId);
                  BaseContext.setCurrentId(patientId);
                  //3、通过,放行
                  return true;
              } catch (Exception ex) {
                  //4、不通过,响应401状态码
                  response.setStatus(401);
                  return false;
              }
          }
      }
      
      @Component
      @ConfigurationProperties(prefix = "jwt")
      @Data
      public class JwtProperties {
          private String userSecretKey; //密钥
          private long userTtl; //过期时间
          private String userTokenName; //token名称
      }
      

      在实际开发中需要用下列手段来增加JWT的安全性:

      1. 因为JWT是在请求头中传递的,所以为了避免网络劫持,推荐使用HTTPS来传输,更加安全。
      2. JWT的哈希签名的密钥是存放在服务端的,所以只要服务器不被攻破,理论上JWT是安全的,因此要保证服务器的安全。
      3. JWT可以使用暴力穷举来破解,所以为了应对这种破解方式,可以定期更换服务端的哈希签名密钥(相当于盐值)。这样可以保证等破解结果出来了,你的密钥也已经换了。

参考博客

  1. JWT详解