Spring boot 整合 JWT
作者:mmseoamin日期:2023-12-05

系列文章目录

第一章 Java线程池技术应用

第二章 CountDownLatch和Semaphone的应用

第三章 Spring Cloud 简介

第四章 Spring Cloud Netflix 之 Eureka

第五章 Spring Cloud Netflix 之 Ribbon

第六章 Spring Cloud 之 OpenFeign

第七章 Spring Cloud 之 GateWay

第八章 Spring Cloud Netflix 之 Hystrix

第九章 代码管理gitlab 使用

第十章 SpringCloud Alibaba 之 Nacos discovery

第十一章 SpringCloud Alibaba 之 Nacos Config

第十二章 Spring Cloud Alibaba 之 Sentinel

第十三章 JWT

Spring boot 整合 JWT,在这里插入图片描述,第1张


文章目录

  • 系列文章目录
    • @[TOC](文章目录)
    • 前言
    • 1、概念
    • 2、与Cookie/session的对比
    • 3、JWT构成与交互流程
    • 3.1、Jwt的头部承载两部分信息
    • 4、缺点
    • 5、Spring boot与JWT整合
      • 5.1、添加模块
      • 5.2、添加依赖
      • 5.3、修改配置文件
      • 5.4、添加类 config
      • 5.5、添加controller
      • 5.6、gateway工程 改造
        • 5.6.1、新增线程类UrlThread
        • 5.6.2、新增GlobalBeanConf类
        • 5.6.3、全局过滤器改造
        • 总结

          前言

          JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案。JWT是一个开放标准(RFC 7519)定义的方式,用于在网络之间安全地传输信息。JWT是一个紧凑的,URL安全的手段,表示在双方之间转让的声明。这些声明可以被验证和信任,因为它们是数字签名的。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA公钥/私钥对对进行签名。

          1、概念

          • 基于JSON的开发标准
          • 用户信息加密到token里,服务器不保存任何用户信息

            2、与Cookie/session的对比

            • 在传统的用户登录认证中,因为http是无状态的,所以都是采用session方式。用户登录成功,服务端会保证一个session,当然会给客户端一个sessionId,客户端会把sessionId保存在cookie中,每次请求都会携带这个sessionId。
            • JWT方式校验方式更加简单便捷化,无需通过redis缓存,而是直接根据token取出保存的用户信息,以及对token可用性校验,单点登录,验证token更为简单。

              3、JWT构成与交互流程

              第一部分为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature)。【中间用 . 分隔】

              一个标准的JWT生成的token格式如下:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI1IiwiaWF0IjoxNTY1NTk3MDUzLCJleHAiOjE1NjU2MDA2NTN9.qesdk6aeFEcNafw5WFm-TwZltGWb1Xs6oBEk5QdaLzlHxDM73IOyeKPF_iN1bLvDAlB7UnSu-Z-Zsgl_dIlPiw

              3.1、Jwt的头部承载两部分信息

              声明类型,这里是jwt

              声明加密的算法,通常直接使用HMACSHA256,就是HS256了

              {

              “alg”: “HS256”,

              “typ”: “JWT”

              }

              然后将头部进行base64编码构成了第一部分:

              eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

              然后我们看第二部分:载荷

              载荷,即承载的意思。也就是说这里是承载消息具体内容的地方。

              (你在令牌上附带的信息:比如用户的姓名,这样以后验证了令牌之后就可以直接从这里获取信息而不用再查数据库了)

              内容又可以分为3种标准

              标准中注册的声明

              公共的声明

              私有的声明

              【标准声明】

              iss: jwt签发者

              sub: jwt所面向的用户

              aud: 接收jwt的一方

              exp: jwt的过期时间,这个过期时间必须要大于签发时间

              nbf: 定义在什么时间之前,该jwt都是不可用的.

              iat: jwt的签发时间

              jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

              【公共声明】

              公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密

              【私有声明】

              私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息

              对Payload进行Base64加密就得到了JWT第二部分的内容。

              JWT的第三部分是一个签证信息,这个签证信息由三部分组成:

              header (base64后的)

              payload (base64后的)

              secret

              第三部分需要base64加密后的header和base64加密后的payload使用 . 连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了JWT的第三部分。

              (对前两部分的签名,防止数据篡改)

              Spring boot 整合 JWT,在这里插入图片描述,第2张

              验证流程:

              ① 在头部信息中声明加密算法和常量, 然后把header使用json转化为字符串

              ② 在载荷中声明用户信息,同时还有一些其他的内容;再次使用json 把载荷部分进行转化,转化为字符串

              ③ 使用在header中声明的加密算法和每个项目随机生成的secret来进行加密, 把第一步分字符串和第二部分的字符串进行加密, 生成新的字符串。词字符串是独一无二的。

              ④ 解密的时候,只要客户端带着JWT来发起请求,服务端就直接使用secret进行解密。

              4、缺点

              • 安全性
              • 性能
              • 一次性

                5、Spring boot与JWT整合

                5.1、添加模块

                authority-service

                5.2、添加依赖

                
                    io.jsonwebtoken
                    jjwt
                    0.7.0
                
                
                    com.auth0
                    java-jwt
                    3.4.0
                
                
                    com.alibaba.cloud
                    spring-cloud-starter-alibaba-nacos-discovery
                    2021.0.4.0
                
                

                5.3、修改配置文件

                server:
                  port: 7777
                spring:
                  application:
                    name: auth-service
                  cloud:
                    nacos:
                      discovery:
                        server-addr: localhost:8848  #Nacos server 的地址
                config:
                  jwt:
                    # 加密密钥
                    secret: tigerkey
                    # token有效时长
                    expire: 3600
                    # header 名称
                    header: token
                

                5.4、添加类 config

                package com.xxxx.springCloud.auth.config;
                import io.jsonwebtoken.Claims;
                import io.jsonwebtoken.Jwts;
                import io.jsonwebtoken.SignatureAlgorithm;
                import lombok.Data;
                import org.springframework.boot.context.properties.ConfigurationProperties;
                import org.springframework.stereotype.Component;
                import java.util.Date;
                @Component
                @ConfigurationProperties(prefix = "config.jwt")
                @Data
                public class JwtConfig {
                    /**
                     * 密钥
                     */
                    private String secret;
                    /**
                     * 过期时间
                     */
                    private Long expire;
                    /**
                     * 头部
                     */
                    private String header;
                    /**
                     * 生成token
                     * @param subject
                     * @return
                     */
                    public String createToken(String subject){
                        Date nowDate = new Date();
                        Date expireDate = new Date(nowDate.getTime() + expire * 1000);
                        return Jwts.builder()
                                .setHeaderParam("typ","JWT")
                                .setSubject(subject)
                                .setIssuedAt(nowDate)
                                .setExpiration(expireDate)
                                .signWith(SignatureAlgorithm.HS512,secret)
                                .compact();
                    }
                    /**
                     * 获取token中的注册信息
                     * @param token
                     * @return
                     */
                    public Claims getTokenClaim(String token){
                        try{
                            return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
                        }catch (Exception e){
                            return null;
                        }
                    }
                    /**
                     * 验证token是否过期
                     * @param expirationTime
                     * @return
                     */
                    public boolean isTokenExpired(Date expirationTime){
                        if(null == expirationTime){
                            return true;
                        }else{
                            return expirationTime.before(new Date());
                        }
                    }
                    /**
                     * 获取token的失效时间
                     * @param token
                     * @return
                     */
                    public Date getExpirationDateFromToken(String token){
                        Claims tokenClaim = this.getTokenClaim(token);
                        if(tokenClaim == null){
                            return null;
                        }else{
                            return this.getTokenClaim(token).getExpiration();
                        }
                    }
                    /**
                     * 获取token中的用户名
                     * @param token
                     * @return
                     */
                    public String getUserNameFromToken(String token){
                        return this.getTokenClaim(token).getSubject();
                    }
                    /**
                     * 获取token中发布时间
                     * @param token
                     * @return
                     */
                    public Date getIssuedDateFromToken(String token){
                        return this.getTokenClaim(token).getIssuedAt();
                    }
                }
                

                5.5、添加controller

                package com.xxxx.springCloud.auth.controller;
                import com.xxxx.springCloud.auth.config.JwtConfig;
                import com.xxxx.springCloud.common.entity.UserInfo;
                import org.springframework.beans.factory.annotation.Autowired;
                import org.springframework.web.bind.annotation.*;
                import java.util.HashMap;
                import java.util.Map;
                @RestController
                @RequestMapping("/auth")
                public class AuthController {
                    @Autowired
                    private JwtConfig jwtConfig;
                    @PostMapping("/login")
                    public Map login(@RequestBody UserInfo userInfo){
                        String token = jwtConfig.createToken(userInfo.getUserAccount());
                        Map map = new HashMap();
                        map.put("token",token);
                        return map;
                    }
                    /**
                     * token是否过期
                     * @param token
                     * @return
                     */
                    @PostMapping("/isTokenExpiration")
                    public Boolean isTokenExpiration(@RequestParam String token){
                        return this.jwtConfig.isTokenExpired(this.jwtConfig.getExpirationDateFromToken(token));
                    }
                }
                

                5.6、gateway工程 改造

                5.6.1、新增线程类UrlThread

                package com.xxxx.springCloud.gateway.hread;
                import org.springframework.cloud.client.ServiceInstance;
                import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
                import java.util.concurrent.Callable;
                public class UrlThread implements Callable {
                    private final LoadBalancerClient loadBalancerClient;
                    public UrlThread(LoadBalancerClient loadBalancerClient) {
                        this.loadBalancerClient = loadBalancerClient;
                    }
                    @Override
                    public String call() throws Exception {
                        ServiceInstance serviceInstance = this.loadBalancerClient.choose("auth-service");
                        return  "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/auth/isTokenExpiration";
                    }
                }
                

                5.6.2、新增GlobalBeanConf类

                @Configuration
                public class GlobalBeanConf {
                    @Bean
                    public RestTemplate restTemplate(){
                        return new RestTemplate();
                    }
                }
                

                5.6.3、全局过滤器改造

                //负载均衡获取微服务实例
                private final   LoadBalancerClient loadBalancerClient;
                //远程调用
                private final RestTemplate restTemplate;
                public DrfGlobalFilter(LoadBalancerClient loadBalancerClient, RestTemplate restTemplate) {
                    this.loadBalancerClient = loadBalancerClient;
                    this.restTemplate = restTemplate;
                }
                
                //启动获取url的线程
                FutureTask stringFutureTask = new FutureTask(new UrlThread(this.loadBalancerClient));
                new Thread(stringFutureTask).start();
                String url = stringFutureTask.get();
                Boolean aBoolean = this.restTemplate.postForObject(url+"?token="+token, null,Boolean.class);
                
                package com.xxxx.springCloud.gateway.filter;
                import com.xxxx.springCloud.common.dto.TokenDTO;
                import com.xxxx.springCloud.gateway.service.AuthService;
                import com.xxxx.springCloud.gateway.thread.ValidateTokenTask;
                import lombok.SneakyThrows;
                import org.apache.commons.lang3.StringUtils;
                import org.springframework.cloud.client.ServiceInstance;
                import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
                import org.springframework.cloud.gateway.filter.GatewayFilterChain;
                import org.springframework.cloud.gateway.filter.GlobalFilter;
                import org.springframework.core.Ordered;
                import org.springframework.http.HttpHeaders;
                import org.springframework.http.HttpStatus;
                import org.springframework.http.server.reactive.ServerHttpRequest;
                import org.springframework.http.server.reactive.ServerHttpResponse;
                import org.springframework.stereotype.Component;
                import org.springframework.web.server.ServerWebExchange;
                import reactor.core.publisher.Mono;
                import java.util.concurrent.ExecutorService;
                import java.util.concurrent.Executors;
                import java.util.concurrent.FutureTask;
                @Component
                public class DrfGlobalFilter implements GlobalFilter, Ordered {
                    private final AuthService authService;
                    private ExecutorService executorService;
                    public DrfGlobalFilter(AuthService authService) {
                        this.authService = authService;
                        this.executorService = Executors.newFixedThreadPool(5);
                    }
                    @SneakyThrows
                    @Override
                    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                        ServerHttpRequest request = exchange.getRequest();
                        //如果登录请求,不用验证token
                        String path = request.getURI().getPath();
                        if(!path.contains("login")){
                            HttpHeaders headers = request.getHeaders();
                            String token = headers.getFirst("token");
                            //token为空表示没有登录,否则已经登录
                            if(StringUtils.isBlank(token)){
                                ServerHttpResponse response = exchange.getResponse();
                                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                                return response.setComplete();
                            }else{
                                TokenDTO tokenDTO = new TokenDTO();
                                tokenDTO.setToken(token);
                                //token验证不通过,返回给前端401
                                FutureTask booleanFutureTask = new FutureTask<>(new ValidateTokenTask(this.authService, token));
                                this.executorService.submit(booleanFutureTask);
                                Boolean aBoolean = booleanFutureTask.get();
                                if(aBoolean){
                                    ServerHttpResponse response = exchange.getResponse();
                                    response.setStatusCode(HttpStatus.UNAUTHORIZED);
                                    return response.setComplete();
                                }
                            }
                        }
                        return chain.filter(exchange);
                    }
                    @Override
                    public int getOrder() {
                        return 0;
                    }
                }
                
                package com.xxxx.springCloud.gateway.service;
                import com.xxxx.springCloud.common.dto.TokenDTO;
                import org.springframework.cloud.openfeign.FeignClient;
                import org.springframework.web.bind.annotation.PostMapping;
                import org.springframework.web.bind.annotation.RequestBody;
                @FeignClient(value = "auth-service")
                public interface AuthService {
                    @PostMapping("/auth/validateToken")
                    public Boolean validateToken(TokenDTO tokenDTO);
                }
                package com.xxxx.springCloud.gateway.config;
                import org.springframework.beans.factory.ObjectProvider;
                import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
                import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
                import org.springframework.context.annotation.Bean;
                import org.springframework.context.annotation.Configuration;
                import org.springframework.http.converter.HttpMessageConverter;
                import java.util.stream.Collectors;
                @Configuration
                public class GlobalConf {
                    @Bean
                    @ConditionalOnMissingBean
                    public HttpMessageConverters messageConverters(ObjectProvider> converters) {
                        return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
                    }
                }
                
                package com.xxxx.springCloud.gateway.thread;
                import com.xxxx.springCloud.common.dto.TokenDTO;
                import com.xxxx.springCloud.gateway.service.AuthService;
                import java.util.concurrent.Callable;
                public class ValidateTokenTask implements Callable {
                    private final AuthService authService;
                    private final String token;
                    public ValidateTokenTask(AuthService authService, String token) {
                        this.authService = authService;
                        this.token = token;
                    }
                    @Override
                    public Boolean call() throws Exception {
                        TokenDTO tokenDTO = new TokenDTO();
                        tokenDTO.setToken(this.token);
                        return this.authService.validateToken(tokenDTO);
                    }
                }
                

                总结

                尽管JWT可以提供验证和完整性检查,但它本身并不提供加密功能。因此,如果您需要在JWT中发送敏感信息,可能需要额外的加密步骤(使用DES、AES等算法加密数据)来确保数据的安全性。