Springboot+JWT

1.什么是JWT

JWT(JSON WEB TOKEN)是一种标准,用来在前后端或者系统间以JSON对象安全的传输信息,该信息是可以被验证和信任的,因为它是数字签名的。可以使用HMAC或RSA或ECDSA的公钥/私钥进行签名。

2.JWT能做什么

  1. 授权
    一旦登录,每个后续请求都包括JWT,用户就可以访问该令牌允许的路由、服务和资源
  2. 信息交换
    可以对JWT签名,确保收件人是对的人,还可以验证内容是否篡改

3.为什么用JWT

3.1 传统登录基于session认证

Springboot+JWT_第1张图片

  1. 如何实现的
    • 客户端发送账号密码到后端
    • 账号密码验证通过,用户登录成功以后,返回一个sessionId给客户端,客户端每次请求时候,都携带这个sessionId,就表示它已经登陆过了,不需要再输入用户名密码。
  2. 存在问题
    • 每个用户都要在服务端保存一个sessionId来保存它的登录状态,一般session都是保存在内存中,用户越多,服务器压力越大。
    • 用户认证之后,如果session存在对应服务器的内存中,下次用户访问必须还是这台服务器,但是现在分布式的应用,负载均衡后没法确定是访问哪台服务器。
    • 基于cookie来识别用户,如果被篡改,容易收到跨站请求伪造攻击

3.2 基于JWT认证

  1. 如何实现的
    • 客户端发送账号密码到后端,
    • 账号密码验证通过后,将用户的id等其他信息作为Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(token),格式如xxx.xxx.xxx,由.分割,三部分组成head,payload,singurater
    • 客户端接收到JWT后保存在本地,退出登录时,就删除本地的JWT
    • 前端每次请求将JWT放在HTTP Header的Atuthorization中,解决XSS和XSRF。
    • 后端收到请求,就验证JWT的有效性,是否过期,签名是否正确等。
    • 验证通过后,后端使用JWT中包含的用户信息进行对应操作。
  2. 优势
    • 可以通过URL、post参数或header发送,数据量小,传输速度快。
    • 负载中包含了用户信息,避免多次查询数据库
    • token以JSON加密的形式保存在客户端,所以可以跨语言,原则上任何web形式都支持。
    • 不保存在服务端,适合分布式微服务。

4.JWT的结构

令牌的组成

  • 标头(Header)
  • 有效载荷(Payload)
  • 签名(Signature)

格式如xxx.xxx.xxx,由.分割,三部分组成

4.1 Header

标头,标头通常由两部分组成,令牌的类型(JWT)和所使用的签名算法,例如HMAC、SHA256或RAS。

明文信息,json字符串

{
	"alg":"HS256",
	"typ":"JWT"
}

使用Base64编译为 xxx 这种格式,作为JWT的第一部分

4.2 Payload

有效负载,其中包含声明,声明是有关信息,也会使用Base64转为xxx,作为JWT的第二部分

{
	"id":"sad854dsfewr1",
	"name":"zhangsan"
}

需要注意的是,不要在这里放入敏感信息,例如密码,因为Base64是可逆的

4.3 Signature

签名,对头部及负载内容进行签名,保证JWT没有被篡改

5.JWT的使用

  • 引入依赖

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>
    
  • 创建令牌

        @Test
        void contextLoads() {
            String secretKey="asdf1213$1f.sad";     //密钥
    
            //设置时间20s
            Calendar instance = Calendar.getInstance();
            instance.add(Calendar.SECOND,20);
    
            String token = JWT.create()
                    //.withHeader(map)      不设置头的话,默认就是alg:HMAC256  typ:JWT
                    .withClaim("id", "asd123456")   //设置id
                    .withClaim("name", "zhangsan")  //设置用户名
                    .withExpiresAt(instance.getTime())  //设置过期时间
                    .sign(Algorithm.HMAC256(secretKey));//使用密钥设置签名
    
            System.out.println(token);
        }
    

    输出结果eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiemhhbmdzYW4iLCJpZCI6ImFzZDEyMzQ1NiIsImV4cCI6MTY5Nzc2OTMxNn0.MwxzzpvvCmCXB6JYWWVsMnRffo4RkpW413aDRndkbCc

  • 验证令牌
    这里密钥要和加密的密钥一致,否则签名验证不通过

    @Test
    void test(){
        String secretKey="asdf1213$1f.sad";     //密钥
    
        //创建验证对象
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secretKey)).build();
        DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiemhhbmdzYW4iLCJleHAiOjE2OTc3NzEyMTB9.Ro9YSdTmuqdrisQhXRVDxPT-pv4FZLb5nnQkNRUrMjc");
        //获取负载中name的值,存什么类型的值,就要使用对应的asInt或者asString
        System.out.println(verify.getClaim("name").asString());
        //获取过期时间
        System.out.println(verify.getExpiresAt());
    
    }
    
  • 常见异常
    Springboot+JWT_第2张图片

    • SignatureVerificationException 签名不一致
    • TokenExpiredException 令牌过期
    • AlgorithmMismatchException 算法不匹配
    • InvalidClaimException 失效的payload

6.封装JWT工具类

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;

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

public class JWTUtils {

    //密钥
    private static final String SECRET_KEY = "asdf1213$1f.sad";

    /**
     * 生成token
     * @param map   负载中存储的值
     * @return  生成的token值
     */
    public static String getToken(Map<String, String> map) {

        //设置过期时间
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.DATE, 7);  //默认7天

        //创建JWT builder
        JWTCreator.Builder builder = JWT.create();
        //存储payload
        map.forEach((key, value) -> builder.withClaim(key, value));
        //sign
        String token = builder.withExpiresAt(instance.getTime())  //设置过期时间
                .sign(Algorithm.HMAC256(SECRET_KEY));//使用密钥设置签名

        return token;
    }

    /**
     * 验证token
     * @param token
     * @return
     */
    public static DecodedJWT verify(String token){
        //创建验证对象
        return JWT.require(Algorithm.HMAC256(SECRET_KEY)).build().verify(token);
    }
}

7.SpringBoot整合JWT

目录结构
Springboot+JWT_第3张图片

7.1 搭建环境

springboot+jwt+mybatis

  • 引入依赖

        <dependencies>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
    
            
            <dependency>
                <groupId>com.auth0groupId>
                <artifactId>java-jwtartifactId>
                <version>3.4.0version>
            dependency>
            
            <dependency>
                <groupId>org.mybatis.spring.bootgroupId>
                <artifactId>mybatis-spring-boot-starterartifactId>
                <version>3.0.0version>
            dependency>
            
            <dependency>
                <groupId>org.projectlombokgroupId>
                <artifactId>lombokartifactId>
                <version>1.18.12version>
            dependency>
            
            <dependency>
                <groupId>com.alibabagroupId>
                <artifactId>druidartifactId>
                <version>1.1.19version>
            dependency>
            
            <dependency>
                <groupId>mysqlgroupId>
                <artifactId>mysql-connector-javaartifactId>
                <version>8.0.11version>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-testartifactId>
                <scope>testscope>
            dependency>
        dependencies>
    
  • 配置文件

    server.port=8999
    
    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/aaa?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
    spring.datasource.username=root
    spring.datasource.password=root
    
    mybatis.type-aliases-package=com.jwt.entity
    mybatis.mapper-locations=classpath:com/jwt/mapper/*.xml
    
  • 实体

    package com.jwt.entity;
    
    import lombok.Data;
    import lombok.experimental.Accessors;
    
    @Data
    @Accessors(chain = true)
    public class User {
    
        private String username;
    
        private String password;
        
    }
    
  • Dao和Mapper

    • UserDao
      package com.jwt.dao;
      
      import com.jwt.entity.User;
      import org.apache.ibatis.annotations.Mapper;
      
      @Mapper
      public interface UserDao {
          User login(User user);
      }
      
    • UserDaoMapper.xml
      
      DOCTYPE mapper
              PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
              "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
      <mapper namespace="com.jwt.dao.UserDao">
          <select id="login" parameterType="User" resultType="User">
              select * from user where username=#{username} and password=#{password}
          select>
      mapper>
      
  • Service

    package com.jwt.service;
    
    import com.jwt.dao.UserDao;
    import com.jwt.entity.User;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.util.ObjectUtils;
    
    @Service
    public class UserServiceImpl {
    
        @Autowired
        private UsersDao usersDao;
    
        public Users login(User user){
    
            User user = userDao.login(user);
    
            if(ObjectUtils.isEmpty(user)){
                throw new RuntimeException("登录失败");
            }
            return user;
        }
    
    }
    
    
  • controller

    package com.jwt.controller;
    
    import com.auth0.jwt.exceptions.AlgorithmMismatchException;
    import com.auth0.jwt.exceptions.SignatureVerificationException;
    import com.auth0.jwt.exceptions.TokenExpiredException;
    import com.auth0.jwt.interfaces.DecodedJWT;
    import com.jwt.entity.Users;
    import com.jwt.service.UserServiceImpl;
    import com.jwt.utils.JWTUtils;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.HashMap;
    import java.util.Map;
    
    @RestController
    @Slf4j
    public class UserController {
    
        @Autowired
        private UserServiceImpl userService;
    
        @RequestMapping("/user/login")
        public Map<String,Object> login(Users user){
            System.out.println(user.getUsername());
            log.info("username:[{}]",user.getUsername());
            log.info("password:[{}]",user.getPassword());
    
            Map<String,Object> map=new HashMap<>();
    
    
            try {
                Users userDb = userService.login(user);
                Map<String,String> payload = new HashMap<>();
                payload.put("username",userDb.getUsername());
                payload.put("password",userDb.getPassword());
                String token = JWTUtils.getToken(payload);
    
                map.put("state",true);
                map.put("msg","登录成功");
                map.put("token",token);
            } catch (Exception e) {
                map.put("state",false);
                map.put("msg",e.getMessage());
            }
    
            return map;
        }
    
        @RequestMapping("/user/verify")
        public Map<String,Object> verify(String token){
            Map<String,Object> map = new HashMap<String,Object>();
            log.info("token:{}",token);
            try {
                DecodedJWT decodedJWT = JWTUtils.verify(token); //验证令牌
                map.put("state",true);
                map.put("msg","验证成功");
                return map;
            } catch (SignatureVerificationException e) {
                e.printStackTrace();
                map.put("msg","无效签名");
            }catch (TokenExpiredException e){
                e.printStackTrace();
                map.put("msg","token过期");
            }catch (AlgorithmMismatchException e) {
                e.printStackTrace();
                map.put("msg","token算法不一致");
            }catch (Exception e) {
                e.printStackTrace();
                map.put("msg","token无效");
            }
            map.put("state",false);
            return map;
        }
    
    }
    
    

访问/user/login,首先从数据库查询,user对象是否存在,如果存在,就生成token,返回给前端,
Springboot+JWT_第4张图片
然后再访问/user/verify,验证这个token是否有效
Springboot+JWT_第5张图片

7.2 拦截器验证token

如果对每个接口都做token认证,代码冗余太高,单体应用的话,可以使用拦截器验证,分布式应用可以使用网关验证

  • 创建拦截器
    token放在请求头header中

    package com.jwt.interceptors;
    
    import com.auth0.jwt.exceptions.AlgorithmMismatchException;
    import com.auth0.jwt.exceptions.SignatureVerificationException;
    import com.auth0.jwt.exceptions.TokenExpiredException;
    import com.auth0.jwt.interfaces.DecodedJWT;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.jwt.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;
    
    public class JWTinterceptors implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 从 http 请求头中取出 token
            String token = request.getHeader("token");
    
            Map<String,Object> map = new HashMap<String,Object>();
            try {
                DecodedJWT decodedJWT = JWTUtils.verify(token); //验证令牌
                map.put("state",true);
                map.put("msg","验证成功");
                return true;    //验证通过,放行请求
            } catch (SignatureVerificationException e) {
                e.printStackTrace();
                map.put("msg","无效签名");
            }catch (TokenExpiredException e){
                e.printStackTrace();
                map.put("msg","token过期");
            }catch (AlgorithmMismatchException e) {
                e.printStackTrace();
                map.put("msg","token算法不一致");
            }catch (Exception e) {
                e.printStackTrace();
                map.put("msg","token无效");
            }
            map.put("state",false);
            //map转为json
            String json = new ObjectMapper().writeValueAsString(map);
    
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().print(json);
    
            return false;
        }
    
    }
    
  • 配置拦截器

    package com.jwt.config;
    
    import com.jwt.interceptors.JWTinterceptors;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.stereotype.Component;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class InterceptorConfig implements WebMvcConfigurer {
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new JWTinterceptors())
                .addPathPatterns("/user/verify")     //拦截测试验证接口
                .excludePathPatterns("/user/login");   //放行登录接口
        }
    }
    
    
  • 修改7.1中的代码,

    • 修改验证接口/user/verify,去除手动验证的方法,因为拦截器已经实现了
          @RequestMapping("/user/verify")
          public Map<String, Object> verify(String token) {
              Map<String, Object> map = new HashMap<String, Object>();
      
              map.put("state", true);
              map.put("msg", "请求成功");
      
              return map;
          }
      
  • 验证
    先访问登录接口,登录成功
    Springboot+JWT_第6张图片
    再访问验证接口,现在token就不是以参数的形式传进去了,需要放在请求头header中,
    先验证个没带token的,因为controller的代码已经改了,这个token无效提示不是controller的了,是拦截器中的提示
    Springboot+JWT_第7张图片
    再验证个正确的token
    Springboot+JWT_第8张图片

你可能感兴趣的:(java,spring,boot,后端,java)