JWT详解

2023-12-20 06:55:47

1、CSRF

1.1 概述

CSRF全称为跨站请求伪造(Cross-site request forgery),是一种网络攻击方式,也被称为 one-click attack 或者 session riding。

1.2 原理

CSRF攻击利用网站对于用户网页浏览器的信任,挟持用户当前已登陆的Web应用程序,去执行并非用户本意的操作。网站是通过cookie来实现登录功能的,而cookie只要存在浏览器中,那么浏览器在访问这个cookie的服务器的时候,就会自动的携带cookie信息到服务器上去。那么这时候就存在一个漏洞了,如果你访问了一个别有用心或病毒网站,这个网站可以在网页源代码中插入js代码,使用js代码给其他服务器发送请求(比如ICBC的转账请求)。那么因为在发送请求的时候,浏览器会自动的把cookie发送给对应的服务器,这时候相应的服务器(比如ICBC网站),就不知道这个请求是伪造的,就被欺骗过去了。从而达到在用户不知情的情况下,给某个服务器发送了一个请求(比如转账)。

1.3 解决方案

CSRF攻击的要点就是在向服务器发送请求的时候,相应的cookie会自动的发送给对应的服务器。造成服务器不知道这个请求是用户发起的还是伪造的。这时候,我们可以在用户每次访问有表单的页面的时候,在网页源代码中加一个随机的字符串叫做csrf_token,在cookie中也加入一个相同值的csrf_token字符串。以后给服务器发送请求的时候,必须在body中以及cookie中都携带csrf_token,服务器只有检测到cookie中的csrf_tokenbody中的csrf_token都相同,才认为这个请求是正常的,否则就是伪造的。那么黑客就没办法伪造请求了。

前后端项目不需要担心csrf,因为我们用了token。

2、JWT

2.1 什么是token

Token在计算机身份认证中是令牌(临时)的意思,在词法分析中是标记的意思。一般作为邀请、登录系统使用。

2.2 什么是JWT

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。

  • 用户使用账号和密码发出登录请求

  • 服务器验证并创建一个jwt

  • 服务器返回这个jwt给浏览器

  • 浏览器将该jwt串在请求头中向服务器发送请求

  • 服务器验证该jwt

  • 返回响应的资源给浏览器

2.4 为什么使用JWT
2.4.1 传统Session认证的弊端

在用户首次登录成功后,在服务器存储一份用户登录的信息(session),这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这是传统的基于session认证的过程。

然而,传统的session认证有如下的问题:

  • 每个用户的登录信息都会保存到服务器的session中,随着用户的增多,服务器开销会明显增大

  • 对于非浏览器的客户端、手机移动端等不适用,因为session依赖于cookie,而移动端经常没有cookie

  • 因为session认证本质基于cookie,所以如果cookie被截获,用户很容易收到跨站请求伪造攻击。并且如果浏览器禁用了cookie,这种方式也会失效

  • 由于基于Cookie,而cookie无法跨域,所以session的认证也无法跨域,对单点登录不适用

2.4.2 JWT认证的优势
  • JWT Token数据量小,传输速度也很快

  • 因为JWT Token是以JSON加密形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持

  • 不需要在服务端保存会话信息,也就是说不依赖于cookie和session,所以没有了传统session认证的弊端

  • 单点登录友好:使用Session进行身份认证的话,由于cookie无法跨域,难以实现单点登录。但是,使用token进行认证的话, token可以被保存在客户端的任意位置的内存中,不一定是cookie,所以不依赖cookie,不会存在这些问题

  • 适合移动端应用:使用Session进行身份认证的话,需要保存一份信息在服务器端,而且这种方式会依赖到Cookie(需要 Cookie 保存 SessionId),所以不适合移动端

身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递。 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。

2.4 JWT结构

JWT由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将JWT的3部分分别进行Base64编码后用.进行连接形成最终传输的字符串

2.5.1 Header

JWT头是一个描述JWT元数据的JSON对象,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256)

2.5.2 Payload

有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。

默认情况下JWT是未加密的,因为只是采用base64算法,拿到JWT字符串后可以转换回原本的JSON数据,任何人都可以解读其内容,因此不要构建隐私信息字段,比如用户的密码一定不能保存到JWT中,以防止信息泄露。JWT只是适合在网络中传输一些非敏感的信息

2.5.3 Signature

签名部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改。首先,需要指定一个密钥(secret)。该密钥仅仅为保存在服务器中,并且不能向用户公开。然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名

操作

? ? ? ? 模拟一个查询用户的登录流程

? ? ? ? 依赖

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>

? ? ? ? 导入mybatis

? ? ? ? 在resource/application.yml加上


# 配置数据源
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mashang_xiaomi_store?userSSL=false;serverTimezone=Asia/Shanghai
    username: root
    password: 123456
# 配置mybatis
mybatis:
  # mapper配置文件
  mapper-locations: classpath:mapper/*.xml
  # resultType别名,没有这个配置resultType包名要写全,配置后只要写类名
  type-aliases-package: com.example.demo.entity

? ? ? ? 导入mybatis依赖

<!--mysql数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
            <version>8.0.33</version>
        </dependency>

        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>

? ? ? ? 实体类

package com.example.demo.entity;

import lombok.Data;

import java.util.Date;

@Data
public class MsUser {

    private Long userId;

    private String username;

    private String password;

    private String createBy;

    private Date createTime;

    private String updateBy;

    private Date updateTime;

    private String remark;
}

? ? ? ? mapper

? ? ? ? 根据账号查询用户信息

????????创建MsUserMapper,定义SelectByUserName,传入账号。因为每个账号都是唯一的,所以只要返回MsUserMapper对象就行,不用返回集合,密码是加密后的字符串,先用账号查询出来后再对比密码

package com.example.demo.mapper;

import com.example.demo.entity.MsUser;

public interface MsUserMapper {

    MsUser selectByUsername(String username);

}
配置扫描
package com.example.demo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.example.demo.mapper")
public class DemoApplication {

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

}

? ? ? ? 创建对应xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.MsUserMapper">

    <resultMap id="selectByUsernameMap" type="msUser">
        <id property="userId" column="user_id"></id>
        <result property="username" column="username"></result>
        <result property="password" column="password"></result>
    </resultMap>
    <select id="selectByUsername" resultMap="selectByUsernameMap">
        select user_id, username, password
            from ms_user
            where username = #{username}
    </select>

</mapper>

? ? ? ? service层

package com.example.demo.service;

import com.example.demo.entity.MsUser;

public interface IMsUserService {

    // token: 加密的字符串
    String createToken(String username, String password);

}

? ? ? ? 实现类

package com.example.demo.service.impl;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.example.demo.entity.MsUser;
import com.example.demo.mapper.MsUserMapper;
import com.example.demo.service.IMsUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Calendar;

@Service
public class MsUserServiceImpl implements IMsUserService {

    @Autowired
    private MsUserMapper msUserMapper;

    @Override
    public String createToken(String username, String password) {

        // 根据账号查询用户信息
        MsUser msUser = msUserMapper.selectByUsername(username);

        //设置过期时间
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.SECOND, 20);

        if(password.equals(msUser.getPassword())) {
            // 登录成功
            String token = JWT.create()
                    .withClaim("userId", msUser.getUserId()) // 要加密的数据
                    .withClaim("aaa", "123")
                    .withExpiresAt(calendar.getTime())
                    //通过加密方式可以解密,如果加上一串只有自己知道的密钥一起加密,别人就没法解密了
                    //一般密钥放在服务端,不能泄露
                    //一般不加密敏感字段,比如密码等
                    .sign(Algorithm.HMAC256("asbgsdnglmsh")); // 加密方式(密钥)
            return token;
        } else {
            // 登录失败
            return null;
        }
    }

}

controller

package com.example.demo.controller;

import com.example.demo.service.IMsUserService;
import com.example.demo.utils.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

    @Autowired
    private IMsUserService msUserService;

    @PostMapping
    public Result login(String username, String password) {

        String token = msUserService.createToken(username, password);

        if(token == null) {
            return Result.error("登录失败");
        } else {
            return Result.success(token);
        }
    }

}

????????通过这些方法只能拦截登入接口,要配置拦截器。对所有的请求都要进行过滤,有对应的权限才能放行

拦截器

????????创建类实现拦截器对象(HandlerInterceptor),重写preHandle方法

package com.example.demo.interceptor;

import com.alibaba.fastjson.JSON;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.example.demo.utils.Result;
import org.springframework.web.servlet.HandlerInterceptor;

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

public class AuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("进入了拦截器");

        // 验证token:如果验证通过就放行,如果验证不通过,就不放行
        // 一般token会放在请求头里面

        // 获取token
        String token = request.getHeader("token");
        System.out.println(token);

        if(token == null) {
            response.setContentType("application/json;charset=utf8");
            //这里要用到把对象转成字符串,导入依赖fastjon
            response.getWriter().write(JSON.toJSONString(Result.error("没有token")));
            return false;
        }

        // token不为空,验证token
        // 验证不通过,采用抛异常的形式处理
        try {
            JWT.require(Algorithm.HMAC256("asbgsdnglmsh"))
                    .build()
                    .verify(token);
        } catch (Exception e) {
            response.setContentType("application/json;charset=utf8");
            response.getWriter().write(JSON.toJSONString(Result.error("token不合法")));
            return false;
        }

        return true;
    }
}
????????fastjson?
<dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.12</version>
        </dependency>
? ? ? ? 配置生效
package com.example.demo.config;

import com.example.demo.interceptor.AuthInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

//注入到全局
@Configuration
public class AuthInterceptorConfig implements WebMvcConfigurer {

    @Override                   //拦截器的注册对象
    public void addInterceptors(InterceptorRegistry registry) {
        //这里面放的就是之前实现的HandlerInterceptor对象
        registry.addInterceptor(new AuthInterceptor())
                .addPathPatterns("/**") // 所有的请求都要经过AuthInterceptor拦截器
                .excludePathPatterns("/login");
    }
}

? ? ? ? //现在只有登入不被拦截,要先登入得到token,后续请求都在请求头带上这个token去请求其他接口,其他接口才不会被拦截

拿到解密数据

package com.example.demo.controller;

import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping
    public String test(HttpServletRequest request) {

        String token = request.getHeader("token");
        DecodedJWT decodedJWT = JWT.decode(token);
        Long userId = decodedJWT.getClaim("userId").asLong();
        String aaa = decodedJWT.getClaim("aaa").asString();
        System.out.println(userId);
        System.out.println(aaa);

        return "xxxxxx";
    }
    
}

????????查询用户的时候,不用传用户Id,调用接口的时候需要传token,可以通过token解密都到用户Id

密码这种敏感字段不加密进去

文章来源:https://blog.csdn.net/2302_76649176/article/details/135059884
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。