文章目录

  • 一、什么是JWT
  • 二、JWT能做什么
  • 1.授权(使用JWT最常见的方案)
  • 2.信息交换
  • 三、为什么是JWT
  • 基于传统的session认证
  • 基于JWT认证
  • 四、JWT的结构是什么?
  • 五、使用JWT
  • 1.引入依赖
  • 2.生成token
  • 3.根据令牌和签名解析数据
  • 六、封装工具类
  • 七、整合springboot
  • 0.搭建springboot+mybatis+jwt环境
  • 1.开发数据库
  • 2.开发entity
  • 3.开发DAO接口和mapper.xml
  • 4.开发Service接口以及实现类
  • 5.开发Controller层
  • 6.问题?
  • 7.配置拦截器
  • 8.postman检验



单点登录概述:


多系统共存下,用户在一处地方登录,得到其他所有系统的信任,无需再次登录。

自己的理解:在前端用户点击登录触发后端登录接口,登录成功的时候,后端jwt生成一个token,后端将token返给前端,前端把token放进需要进行验证的接口的请求头(header)中去,用户带着这个token去进行验证。(token设置过期时间),验证成功则拦截器放行,否则拦截

一、什么是JWT

JWT全称 Json·Web·Token,通俗地讲,也就是通过JSON形式作为Web应用中的令牌,用于在各⽅之间安全地将信息作为JSON对象传输,在数据传输地过程中还可以完成数据加密、签名等相关处理,是目前最流⾏的跨域身份解决⽅案

二、JWT能做什么

1.授权(使用JWT最常见的方案)

一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。(即JWT能过够实现跨域)

2.信息交换

JWT是在各方之间安全地传输信息地好方法。因为可以对JWT进行签名(比例使用公钥/私钥对),所以可以确保发件人是他们所说的人。

三、为什么是JWT

基于传统的session认证

1.认证方式

Session 方案中,登录成功后,服务端将用户的身份信息存储在 Session 里,并且服务端会存储session,并将Session ID通过Cookie传递给客户端。后续的数据请求都会带上 Cookie,服务端根据 Cookie 中携带的 Session ID 来得辨别用户身份。在session方案的单点登录中,考虑如何同步 Session 和共享Cookie,所有应用都从身份验证服务同步 Session。注销的时候只要删掉session中对应的记录就可以,这样的操作在客户端几乎是无感知的。

Jedis springboot 整合 springboot整合jwt入门_spring


2.暴露问题

这种基于服务器的身份认证⽅式存在⼀些问题:

  • 1.Seesions:每次认证用户发起请求时,服务器需要去创建一个记录来存储信息。通常而言session都是保存在内存中,当越来越多的用户发请求时,内存的开销也会不断增加。
  • 2.可扩展性:由于sessions 存放在服务器内存中,这意味着用户下次请求还必须在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡的能力,这也意味着限制了应用的扩展能力。
  • 3.CORS (跨域资源共享):当我们扩展应用程序,让数据能够从不同设备上访问时,跨域资源的共享会是一个让人头疼的问题,不方便集群应用。
  • 4.CSRF (跨站请求伪造):因为是基于cookie来进行用户识别的,cookie如果被截获,用户就很容易受到跨站请求的攻击。

cookie大小只有4kb,不允许跨域访问,这一点对token机制不存在。

补充
1.那么究竟什么是跨站请求伪造呢?简单地说就是用你的身份去发送一些对你不友好的请求。
举个简单的例子:
小壮登录了某网上银行,他来到了网上银行的帖子区,看到一个帖子下面有一个链接写着“科学理财,年盈利率过万”,小壮好奇的点开了这个链接,结果发现自己的账户少了10000元。这是这么回事呢?原来黑客在链接中藏了一个请求,这个请求直接利用小壮的身份给银行发送了一个转账请求,也就是通过你的 Cookie 向银行发出请求。
导致这个问题很大的原因就是: Session 认证中 Cookie 中的 session_id 是由浏览器发送到服务端的,借助这个特性,攻击者就可以通过让用户误点攻击链接,达到攻击效果。
那么为什么token不存在这种问题呢?
一般情况下我们使用 JWT 的话,在我们登录成功获得 token 之后,一般会选择存放在 local storage 中。然后我们在前端通过某些方式会给每个发到后端的请求加上这个 token,这样就不会出现 CSRF 漏洞的问题。因为,即使有个你点击了非法链接发送了请求到服务端,这个非法请求是不会携带 token 的,所以这个请求将是非法的。(网上找的,个人有点不理解,坑待填)

2.注意区分跨站请求伪造与数字签名(个人理解,可能不对,只是看到CSRF,就突然想到了数字签名)
跨站请求伪造是用你的身份去发送一些对你不友好的请求,这个是基于请求信息上的攻击。而数字签名是防止别人伪造自己的身份发送信息,这个是基于身份验证上的。

基于JWT认证

1.认证流程

Jedis springboot 整合 springboot整合jwt入门_User_02

a.首先,前端通过web表单将自己的用户名和密码发送到后端的接口。
b.后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成
一个JWT(token),形成的JWT就是一个形同111.zzz.xxx的字符串
c.后端将JWT字符串 作为登录成功的返回结果返回给前端,前端可以返回的结果保存在localStorage 或sessionStorage上,退出登录
时前端删除保存的JWT即可。
d.前端在每次请求时将JWT放入HTTP Header中的Authoriztion 位。(放在Header中的原因是解决XSS和XSRF问题) HEADER.
e.后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)
f。验证通过后,后端使用JWT包含的用户信息进行其他逻辑操作,返回相应结果。

2.jwt优势
⽆状态和可拓展性:Token 存储在客户端,完全⽆状态,可拓展,特别适用于分布式微服务。我们的负载均衡器可以将⽤户传递到任意服务器,因为在任何地⽅都没有状态或会话信息。

简洁:每次请求的时候token都会被发送,可以作为请求参数发送,可以放在请求头⾥⾯发送,也可以放在cookie⾥⾯被发送,因为数据量小,传输速度也很快。

自包含:负载中包含了所有用户所需要的信息,避免了多次查询数据库。

补充:我刚开始不太理解为什么JWT在浏览器本地存储比session在服务器端存储在负载方面要好,这里我给出自己的理解:
服务器端保存所有的用户的数据,所以服务器端的开销较大,而浏览器端保存则把不同用户需要的数据分布保存在用户各自的浏览器中。

四、JWT的结构是什么?

jwt由三部分组成,他们之间⽤圆点(·)连接,这三部分分别是:

  • 1.标头Header
  • 2.有效负载Payload
  • 3.签名Signature

Header由两部分信息组成:
type:声明类型,这⾥是jwt
alg:声明加密的算法 通常直接使⽤ HMAC SHA256

Payload就是存放有效信息的地⽅(不强制)
iss: jwt签发者
sub: jwt所⾯向的⽤户
aud: 接收jwt的⼀⽅
exp: jwt的过期时间,这个过期时间必须要⼤于签发时间
nbf: 定义在什么时间之前,该jwt都是不可⽤的
iat: jwt的签发时间
jti: jwt的唯⼀身份标识,主要⽤来作为⼀次性token,从⽽回避重放攻击
claim:jwt存放信息的地⽅

Signature就是签名信息
签名目的:
最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被篡改。

因此,⼀个典型的JWT看起来是这个样⼦的:
xxxxxxxx·yyyyyyyyyy·zzzzzzzzzzz

五、使用JWT

1.引入依赖

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

2.生成token

@Test
    public void createJwtToken(){
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.SECOND,300);
        //生成令牌
        String token = JWT.create()
                .withClaim("username", "张三")//设置自定义用户名
                .withExpiresAt(instance.getTime())//设置过期时间
                .sign(Algorithm.HMAC256("token!123ab"));//设置签名 保密 复杂
        //输出令牌
        System.out.println(token);
    }

生成结果:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDQwMjY5ODUsInVzZXJuYW1lIjoi5byg5LiJIn0.cZf24DSxSX4LR-2Z6pTKufAg6kOSJiMANzuXgKz2BrE

3.根据令牌和签名解析数据

@Test
    public void freeJwt(){
        String token="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDQwMjY5ODUsInVzZXJuYW1lIjoi5byg5LiJIn0.cZf24DSxSX4LR-2Z6pTKufAg6kOSJiMANzuXgKz2BrE";
        //创建验证对象
        JWTVerifier verifier = JWT.require(Algorithm.HMAC256("token!123ab")).build();
        DecodedJWT decodedJWT = verifier.verify(token);
        System.out.println("用户名:"+decodedJWT.getClaim("username").asString());
        System.out.println("过期时间;"+decodedJWT.getExpiresAt());
    }

六、封装工具类

package com.example.demo.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.junit.Test;

import java.util.Calendar;
import java.util.Map;

/**
 * @author chen
 * @version 1.0
 * @date 2020/10/30 10:25
 */
public class JwtUtils {

    private static final String secret="token!123ab";

    //生成token
    public static String createJwtToken(Map<String,String> map){
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.SECOND,300);
        //生成令牌
        JWTCreator.Builder builder = JWT.create();
        for(Map.Entry<String,String> entry : map.entrySet())
            builder.withClaim(entry.getKey(),entry.getValue());
        builder.withExpiresAt(instance.getTime());
        return builder.sign(Algorithm.HMAC256(secret));
    }

    //验证token合法性
    public static DecodedJWT verify(String token){
        //创建验证对象,验证通过则正常执行,不通过则会报错(抛出异常)
        return JWT.require(Algorithm.HMAC256(secret)).build().verify(token);
    }


//    //获取token信息(前提是token验证通过)
//    public static DecodedJWT getTokenInfo(String token){
//        return JWT.require(Algorithm.HMAC256(secret)).build().verify(token);
//    }
}

七、整合springboot

0.搭建springboot+mybatis+jwt环境

引入依赖

<!--引入jwt-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.8.1</version>
        </dependency>

<!--引入mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>

<!--引入lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
<!--引入mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>   
          
<!--引入druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.22</version>
        </dependency>

编写配置

server.port=8989

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/jwt
spring.datasource.username=root
spring.datasource.password=123456

mybatis.type-aliases-package=com.example.demo.entity
mybatis.mapper-locations=classpath:com/example/demo/mapper/*.xml

1.开发数据库

这里采用最简单的表结构验证JWT使用

(这里我偷下懒,没有写sql语句,直接用navicat建表了)

Jedis springboot 整合 springboot整合jwt入门_java_03


Jedis springboot 整合 springboot整合jwt入门_Jedis springboot 整合_04

2.开发entity

@Data
public class User {
    private Integer id;
    private String name;
    private String password;
}

3.开发DAO接口和mapper.xml

@Mapper
public interface UserDAO {
    //直接根据用户名密码登录
    User login(User user);
}
<?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.dao.UserDAO">
    <select id="login" parameterType="com.example.demo.entity.User" resultType="com.example.demo.entity.User">
        select * from tb_user
        where name=#{name} and password =#{password}
    </select>
    
</mapper>

4.开发Service接口以及实现类

public interface UserService {
    User login(User user);
}
public class UserServiceImpl implements UserService{
    @Autowired
    private UserDAO userDAO;
    @Override
    public User login(User user) {
        return userDAO.login(user);
    }
}

5.开发Controller层

package com.example.demo.controller;

import com.auth0.jwt.interfaces.DecodedJWT;
import com.example.demo.Service.UserService;
import com.example.demo.entity.User;
import com.example.demo.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
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.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

/**
 * @author chen
 * @version 1.0
 * @date 2020/10/30 16:28
 */
@RestController
@Slf4j
public class UserController {
    @Autowired
    UserService userService;

    @GetMapping("user/login")
    public Map<String,Object> login(User user){
        Map<String,Object> map = new HashMap<>();
        User userDB = userService.login(user);
        if(userDB!=null){
            //认证成功后,生成JWT令牌
            Map<String,String> payload = new HashMap<>();
            payload.put("id",userDB.getId().toString());
            payload.put("name",userDB.getName());
            String jwtToken = JwtUtils.createJwtToken(payload);
            map.put("state",true);
            map.put("message","认证成功");
            map.put("token",jwtToken);//响应jwtToken
        }
        else{
            map.put("state",false);
            map.put("message","认证失败");
        }
        return map;
    }

    @PostMapping("user/test")
    public Map<String,Object> test(HttpServletRequest request){
        Map<String,Object> map = new HashMap<>();
        String token = request.getHeader("token");
        try{
            DecodedJWT verify = JwtUtils.verify(token);
            log.info("用户id:[{}]",verify.getClaim("id").asString());
            log.info("用户name:[{}]",verify.getClaim("name").asString());
            map.put("state",true);
           map.put("message","请求成功");
           return map;
       }catch(Exception e){
           e.printStackTrace();
           map.put("state",false);
           map.put("message","请求失败");
       }
       return map;
    }
}

6.问题?

—使用上述方式每次都要传递token数据,每个方法都需要验证token,会造成代码冗余,不够灵活的现象,那么如何优化它呢?
—使用拦截器进行优化
实现拦截器

package com.example.demo.interceptor;

import com.alibaba.fastjson.JSON;
import com.example.demo.utils.JwtUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * @author chen
 * @version 1.0
 * @date 2020/10/30 22:20
 */
public class JwtInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Map<String,Object> map=new HashMap<>();
        //获取请求头中的令牌
        String token = request.getHeader("token");
        try{
            JwtUtils.verify(token);//验证令牌
            return true;
        }catch(Exception e){
            e.printStackTrace();
            map.put("state",false);
            map.put("message","请求失败");
        }
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write(JSON.toJSON(map).toString());
        return false;
    }
}

7.配置拦截器

package com.example.demo.config;

import com.example.demo.interceptor.JwtInterceptor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author chen
 * @version 1.0
 * @date 2020/10/29 17:06
 */
//有关拦截器的配置
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //指定拦截器并添加相应策略(拦截或放行)
        registry.addInterceptor(new JwtInterceptor())
                .addPathPatterns("/**")//拦截(其他token验证)
                .excludePathPatterns("user/login");//所有用户都放行(登录接口不放行,无法进行登录)
    }
}

总结一下上面代码的思路:

用户登录时,验证用户的账户和密码
生成一个Token保存在数据库中,将Token写到Cookie中
将用户数据保存在Session中
请求时都会带上Cookie,检查有没有登录,如果已经登录则放行

8.postman检验

Jedis springboot 整合 springboot整合jwt入门_User_05

Jedis springboot 整合 springboot整合jwt入门_Jedis springboot 整合_06