自学内容网 自学内容网

Web day09 会话技术 & JWT令牌 & Filter & Interceptor

目录

会话技术:

1.Cookie:

2.Session:

3.令牌技术:

JWT令牌:

生成JWT令牌:

校验JWT令牌(解析生成的令牌)

登陆时下发令牌:

过滤器Filter:

拦截器Interceptor:

1). 自定义拦截器

2).注册配置拦截器

当Filter和Intercepter都存在时的执行逻辑:


会话技术:

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

由于HTTP是无状态协议,在后面请求中怎么拿到前一次请求生成的数据 就需要在一次会话的多次请求之间进行数据共享

1.Cookie:

Cookie(客户端会话跟踪技术):数据存储在客户端浏览器当中

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

服务器端在给客户端在响应数据的时候,会自动的将 cookie 响应给浏览器,浏览器接收到响应回来的 cookie 之后,会自动的将 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();
    }
}    

用cookie 配合拦截器 可以判断 用户是否已经登录  从而判断是否可以放行 

cookie的缺点:

  • 缺点:

    • 移动端APP(Android、IOS)中无法使用Cookie

    • 不安全,用户可以自己禁用Cookie

    • Cookie不能跨域  现在大多为集群开发 前后端系统不在一个服务器则无法使用Cookie

     区分跨域的维度(三个维度有任何一个维度不同,那就是跨域操作):

  • 协议

  • IP/协议

  • 端口

2.Session:

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

如果我们现在要基于 Session 来进行会话跟踪,浏览器在第一次请求服务器的时候,我们就可以直接在服务器当中来获取到会话对象Session。如果是第一次请求Session ,会话对象是不存在的,这个时候服务器会自动的创建一个会话对象Session 。而每一个会话对象Session ,它ID我们称之为 Session 的ID。

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

接下来,在后续的每次请求时,都会将Cookie的值在Cookie请求头中,携带到服务端,那服务端呢,接收到Cookie之后,会自动的根据JSESSIONID的值,找到对应的会话对象Session

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

  • Cookie存储在服务器中

  • 缺点:

    • 服务器集群环境下无法直接使用Session

    • 移动端APP(Android、IOS)中无法使用Cookie

    • 用户可以自己禁用Cookie

    • Cookie不能跨域

 @GetMapping("/s1")
    public Result session1(HttpSession session){

        session.setAttribute("loginUser", "tom"); //往session中存储数据
        return Result.Success("设置session成功");
    }

    @GetMapping("/s2")
    public Result session2(HttpServletRequest request){
        HttpSession session = request.getSession();

        Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
        return Result.Success(loginUser);
    }

3.令牌技术:

令牌就是一个 字符串

如果通过令牌技术来跟踪会话,我们就可以在浏览器发起请求。在请求登录接口的时候,如果登录成功,我就可以生成一个令牌,令牌就是用户的合法身份凭证。接下来我在响应数据的时候,我就可以直接将令牌响应给前端。

接下来我们在前端程序当中接收到令牌之后,就需要将这个令牌存储起来。这个存储可以存储在 cookie 当中,也可以存储在其他的存储空间(比如:localStorage)当中。

接下来,在后续的每一次请求当中,都需要将令牌携带到服务端。携带到服务端之后,接下来我们就需要来校验令牌的有效性。如果令牌是有效的,就说明用户已经执行了登录操作,如果令牌是无效的,就说明用户之前并未执行登录操作。

此时,如果是在同一次会话的多次请求之间,我们想共享数据,我们就可以将共享的数据存储在令牌当中就可以了。

优点:

  • 支持PC端、移动端

  • 解决集群环境下的认证问题

  • 减轻服务器的存储压力(无需在服务器端存储)

注意: token会存储在浏览器本地数据库中 再下一次请求的请求头中 会有 Token:存储 令牌字符串   所以接受到请求头 以后 可以使用 getHeader("token") 获取token值 

JWT令牌最典型的应用场景就是登录认证:

  1. 在浏览器发起请求来执行登录操作,此时会访问登录的接口,如果登录成功之后,我们需要生成一个jwt令牌,将生成的 jwt令牌返回给前端。

  2. 前端拿到jwt令牌之后,会将jwt令牌存储起来。在后续的每一次请求中都会将jwt令牌携带到服务端。

  3. 服务端统一拦截请求之后,先来判断一下这次请求有没有把令牌带过来,如果没有带过来,直接拒绝访问,如果带过来了,还要校验一下令牌是否是有效。如果有效,就直接放行进行请求的处理。

JWT令牌:

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

    • 简洁:是指jwt就是一个简单的字符串。可以在请求参数或者是请求头当中直接传递。

    • 自包含:指的是jwt令牌,看似是一个随机的字符串,但是我们是可以根据自身的需求在jwt令牌中存储自定义的数据内容。如:可以直接在jwt令牌中存储用户的相关信息。

    • 简单来讲,jwt就是将原始的json数据格式进行了安全的封装,这样就可以直接基于jwt在通信双方安全的进行信息传输了。

JWT的组成: (JWT令牌由三个部分组成,三个部分之间使用英文的点来分割)

  • 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{"alg":"HS256","type":"JWT"}

  • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{"id":"1","username":"Tom"}

注意: 有效载荷中的数据 要以 键值 json的格式存储

  • 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。

签名的目的就是为了防jwt令牌被篡改,而正是因为

jwt令牌最后一个部分数字签名的存在,所以整个jwt 令牌是非常安全可靠的。一旦jwt令牌当中任何一个部分、任何一个字符被篡改了,整个令牌在校验的时候都会失败,所以它是非常安全可靠的

生成JWT令牌:

1) 导入JWT依赖:

<!-- JWT依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

2). 生成JWT代码实现:

@Test
public void testGenJwt() {
    Map<String, Object> claims = new HashMap<>();
    claims.put("id", 10);
    claims.put("username", "itheima");

    String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "aXRjYXN0")
        .addClaims(claims)
        .setExpiration(new Date(System.currentTimeMillis() + 12 * 3600 * 1000))
        .compact();

    System.out.println(jwt);
}

aXRjYXN0 为 自己定义的 密码 

claims 为Payload(有效载荷),携带一些自定义信息、默认信息 claims要以json(map集合)方式传递

SignatureAlgorithm.HS256 为指定的签名算法

校验JWT令牌(解析生成的令牌)

3). 实现了JWT令牌的生成,下面我们接着使用Java代码来校验JWT令牌(解析生成的令牌):

@Test
public void testParseJwt() {
    Claims claims = Jwts.parser().setSigningKey("aXRjYXN0")
        .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAsInVzZXJuYW1lIjoiaXRoZWltYSIsImV4cCI6MTcwMTkwOTAxNX0.N-MD6DmoeIIY5lB5z73UFLN9u7veppx1K5_N_jS9Yko")
        .getBody();
    System.out.println(claims);
}

aXRjYXN0 为自己定义的密码

登陆时下发令牌:

首先现在util 包下创建 TokenUtil 工具类 此类为 根据 传入的 有效载荷数据信息  生成 token 令牌 或者 根据令牌 解析 令牌中携带的数据

@Component
public class TokenUtil {

    @Value("${jwt.secret}")
    private String jwtSecret;

    @Value("${jwt.timeout}")
    private long jwtTimeout;
    
    public String generateToken(Map<String, Object> claims) {

        String token = Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS256, jwtSecret)
                .setExpiration(new Date(System.currentTimeMillis() + jwtTimeout * 1000 * 60))
                .compact();
        return token;
    }
    
    public Map<String, Object> parseJwtToken(String jwt) {
        Claims claims = Jwts.parser()
                .setSigningKey(jwtSecret)
                .parseClaimsJws(jwt)
                .getBody();
        return claims;
    }
}

 把工具类加入 @Component 加入IOC容器管理是为了@Value 可以从yml文件中动态注入 密码值和时间

完善 EmpServiceImpl中的 login 方法逻辑, 登录成功,生成JWT令牌并返回

查到用户时返回令牌 在响应体中 前端会操作浏览器 数据库 保存令牌 

public LoginInfo login(Emp emp) {

        Emp loginEmp = empMapper.getUsernameAndPassword(emp);

        if (loginEmp != null) {
            Map<String, Object> stringObjectHashMap = new HashMap<>();
            stringObjectHashMap.put("id", loginEmp.getId());
            stringObjectHashMap.put("username", loginEmp.getUsername());
            String s = tokenUtil.generateToken(stringObjectHashMap);
            return new LoginInfo(loginEmp.getId(), loginEmp.getUsername(), loginEmp.getName(), s);
        } else
            return null;
    }

LoginController层中:

@RestController
@RequestMapping("/login")
public class LoinController {

    @Autowired
    private EmpService empService;

    @PostMapping
    public Result login(@RequestBody Emp emp) {
        LoginInfo loginInfo = empService.login(emp);
        if (loginInfo !=null){
            return Result.Success(loginInfo);
        }else
            return new Result(0, "用户名或密码错误", null);
    }
}

过滤器Filter:

  • Filter表示过滤器,是 JavaWeb三大组件(Servlet、Filter、Listener)之一。

注意Filter 并不属于 spring框架 所以 过滤器的执行优先级 会大于拦截器

过滤器的使用步骤:

  • 第1步,定义过滤器 :1.定义一个类,实现 Filter 接口,并重写其所有方法。

  • 第2步,配置过滤器:Filter类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加 @ServletComponentScan 开启Servlet组件支持。

1.定义过滤器:

public class DemoFilter implements Filter {
    //初始化方法, web服务器启动, 创建Filter实例时调用, 只调用一次
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("init ...");
    }

    //拦截到请求时,调用该方法,可以调用多次
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        System.out.println("拦截到了请求...");
    }

    //销毁方法, web服务器关闭时调用, 只调用一次
    public void destroy() {
        System.out.println("destroy ... ");
    }
}

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


@Component
@WebFilter("/*")
public class TokenFilter implements Filter {

    @Autowired
    private TokenUtil tokenUtil;

    public static int a = 1;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        a++;
        System.out.println(a);
        System.out.println("过滤器初始化");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String requestURI = request.getRequestURI();
        if (requestURI.equals("/login")) {
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }

        String token = request.getHeader("token");
        if (token == null) {
            System.out.println("token为空asdfsadfs");
            response.setStatus(401);
            return;
        }

        try {
            Map<String, Object> map = tokenUtil.parseJwtToken(token);
            System.out.println(map.get("username") + "登录成功!!!!!!!!!");
            filterChain.doFilter(servletRequest, servletResponse);
        } catch (Exception e) {
            System.out.println("token错误     " + e);
            response.setStatus(401);
            return;
        }

    }
}

filterChain.doFilter(servletRequest, servletResponse); 为放行方法

  • init方法:过滤器的初始化方法。在web服务器启动的时候会自动的创建Filter过滤器对象,在创建过滤器对象的时候会自动调用init初始化方法,这个方法只会被调用一次。

  • doFilter方法:这个方法是在每一次拦截到请求之后都会被调用,所以这个方法是会被调用多次的,每拦截到一次请求就会调用一次doFilter()方法。

  • destroy方法: 是销毁的方法。当我们关闭服务器的时候,它会自动的调用销毁方法destroy,而这个销毁方法也只会被调用一次。

拦截器Interceptor:

  • 是一种动态拦截方法调用的机制,类似于过滤器。

  • 拦截器是Spring框架中提供的,用来动态拦截控制器方法的执行。

  • 拦截器的作用:拦截请求,在指定方法调用前后,根据业务需要执行预先设定的代码。

拦截器的使用步骤和过滤器类似,也分为两步:

  1. 定义拦截器 实现 HandlerInterceptor 接口 重写  preHandle方法 

  2. 注册配置拦截器

1). 自定义拦截器

需要单独开一个LoginInterceptor 包来 存储 拦截器

因为配置类需要 拦截器对象 所以需要加上Component交给IOC容器管理

@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    TokenUtil tokenUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();

        String token = request.getHeader("token");
        if (token == null){
            System.out.println("token为空====> Intercepter拦截"+requestURI);
            response.setStatus(401);
            return false;
        }

        try {
            Map<String, Object> stringObjectMap = tokenUtil.parseJwtToken(token);
            System.out.println(stringObjectMap.get("username") + "登录成功!!!!!!!!!");
            return true;
        } catch (Exception e) {
            response.setStatus(401);
            return false;
        }
    }
}
  • preHandle方法:目标资源方法执行前执行。 返回true:放行 返回false:不放行

  • postHandle方法:目标资源方法执行后执行

  • afterCompletion方法:视图渲染完毕后执行,最后执行

2).注册配置拦截器

先单独创建conf包 防止 配置类对象 

需要实现WebMvcConfigurer接口 并 加@Configuration注解

@Configuration
public class WebConfig implements WebMvcConfigurer {

    //拦截器对象
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册拦截器对象
        registry.addInterceptor(loginInterceptor)
                //拦截所有请求
                .addPathPatterns("/**")
                //放行请求
                .excludePathPatterns("/login")
                .excludePathPatterns("/*.html");
    }
}

.addPathPatterns("/**") 为设置拦截路径

注意和filter规则不同

.excludePathPatterns("/login")为不拦截哪一uri路径

Filter/Intercepter 区别:

  • 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。

当Filter和Intercepter都存在时的执行逻辑:

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

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

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

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


原文地址:https://blog.csdn.net/moskidi/article/details/144328219

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!