springboot——jwt(java web token)一个开放标准(RFC 7519)

概述

背景:传统的Web应用中,使用session来存在用户的信息,每次用户认证通过以后,服务器需要创建一条记录
保存用户信息,通常是在内存中。

随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大,由于Session是在内存中的,这就带来一些扩展性的问题

servlet依赖于web容器

描述:JSON Web Token (JWT,token的一种),是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

JWT存放在客户端(前端),每次请求的请求头中,携带此JWT发送给服务器,服务器端负责接收
和验证
服务器端可以不用存储JWT,这样可以降低服务器的内存的开销
JWT和语言无关,扩展起来非常方便,无论是PC端还是移动端,都可以很容易的使用
不受cookie的限制

session和JWT的主要区别就是保存的位置,session是保存在服务端的,而JWT是保存在客户端的

JWT就是一个固定格式的字符串,jwt的官网是:https://jwt.io/

结构

JWT固定各种的字符串,由三部分组成:
Header,头部
Payload,载荷
Signature,签名
把这三部分使用点(.)连接起来,就是一个JWT字符串

头部

header一般的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。

JWT里验证和签名使用的算法列表如下:
在这里插入图片描述

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

载荷

payload主要用来包含声明(claims ),这个声明一般是关于实体(通常是用户)和其他数据的声明。

声明有三种类型:

registered
public
private

具体如下:

Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。

iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击

Public claims : 可以随意定义

自定义数据:存放在token中存放的key-value值

Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明

{
    "iss": "briup",
    "iat": 1446593502,
    "exp": 1446594722,
    "aud": "www.briup.com",
    "sub": "briup@briup.com",
    "username": "tom"
}

注意,不要在JWT的payload或header中放置敏感信息,除非它们是加密的

把头部和载荷分别进行Base64编码之后得到两个字符串,然后再将这两个编码后的字符串用英文句号.连接在一起(头部在前),形成新的字符串:aaa.bbb

签名

最后,将上面拼接完的字符串用HS256算法进行加密,在加密的时候,还需要提供一个密钥(secret)。加密后的内容也是一个字符串,这个字符串就是签名。

把这个签名拼接在刚才的字符串后面就能得到完整的JWT字符串。
header部分和payload部分如果被篡改,由于篡改者不知道密钥是什么,也无法生成新的signature部分,
服务端也就无法通过。

在JWT中,消息体是透明的,使用签名可以保证消息不被篡改。

确保密钥不会泄露,否则会被篡改

例如,使用HMACSHA256加密算法,配合秘钥,将前俩部进行加密,生成签名

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

例如,将Header、Payload、Signature三部分使用点(.)连接起来

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MmI2OWNlZC02YWNlLTRmYzAtOTk5MS00Y WUwMjIxODQ0OTciLCJleHAiOjE2MDYwNTQzNjl9.DNVhr36j66JpQBfcYoo64IRp84dKiQeaq7axHTBcP9 E

例如,使用官网提供的工具,可以对该JWT进行验证和解析
在这里插入图片描述
我们使用JWT封装的工具类,也可以完成此操作

整合

依赖

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

工具类

public class JwtUtil {
	/**
     * 过期时间5分钟
     */
    private static final long EXPIRE_TIME = 5 * 60 * 1000;
    /**
     * jwt 密钥
     */
    private static final String SECRET = "jwt_secret";

    /**
     * 生成签名,五分钟后过期
     * @param userId
     * @param info,Map的value只能存放的值的类型为:Map, List, Boolean, Integer, Long, Double, String and Date
     * @return
     */
    public static String sign(String userId,Map<String,Object> info) {
        try {
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            return JWT.create()
                    // 将 user id 保存到 token 里面
                    .withAudience(userId)
                    // 存放自定义数据
                    .withClaim("info", info)
                    // 五分钟后token过期
                    .withExpiresAt(date)
                    // token 的密钥
                    .sign(algorithm);
        } catch (Exception e) {
        	e.printStackTrace();
            return null;
        }
    }

    /**
     * 根据token获取userId
     * @param token
     * @return
     */
    public static String getUserId(String token) {
        try {
            String userId = JWT.decode(token).getAudience().get(0);
            return userId;
        } catch (JWTDecodeException e) {
            return null;
        }
    }
    
    /**
     * 根据token获取自定义数据info
     * @param token
     * @return
     */
    public static Map<String,Object> getInfo(String token) {
        try {
            return JWT.decode(token).getClaim("info").asMap();
        } catch (JWTDecodeException e) {
            return null;
        }
    }
    

    /**
     * 校验token
     * @param token
     * @return
     */
    public static boolean checkSign(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            JWTVerifier verifier = JWT.require(algorithm)
                    // .withClaim("username", username)
                    .build();
            verifier.verify(token);
            return true;
        } catch (JWTVerificationException exception) {
            throw new RuntimeException("token 无效,请重新获取");
        }
    }
}

拦截器

// 拦截认证资源
public class JwtInteceptors implements HandlerInterceptor{

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		// 判断是不是认证资源(根据拦截器配置 auth/**) 不是的话直接放行
		//System.out.println(handler); //class org.springframework.web.method.HandlerMethod
		if(!(handler instanceof HandlerMethod)){
			return true;
		}
		// 从请求头中获取token
		String token = request.getHeader("token");
		// 判断token是否为空 空直接抛异常 
		if(token == null) {
			throw new RuntimeException("无token,请登录");
		}
		// 校验token
		JwtUtil.checkSign(token);
		// 取出token中的信息
		String userId = JwtUtil.getUserId(token);
		System.out.println(userId);
		Map<String, Object> info = JwtUtil.getInfo(token);
		info.forEach((k,v)->{
			System.out.println(k+"="+v);
		});
		// 放行
		return true;
	}
	
}

mvc配置

@Configuration
public class MvcConfig implements WebMvcConfigurer{

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(new JwtInteceptors())
		.addPathPatterns("/auth/**");
	}
}

资源

@RestController
@RequestMapping("/auth")
@Api(tags = "测试模块")
public class AuthController {
	@GetMapping("/hello")
	public String hello() {
		return "hello";
	}
}

测试

直接访问
在这里插入图片描述
获取token
在这里插入图片描述
携带请求头
在这里插入图片描述

3
gender=0
username=wangsidandan

修改token,故意写错后,再访问测试

{"code":500,"msg":"服务器异常: token 无效,请重新获取","data":null}

获取新的token测试通过后,等待5分钟后,再次访问

{"code":500,"msg":"服务器异常: token 无效,请重新获取","data":null}

swagger

@RestController
@RequestMapping("/auth")
@Api(tags = "测试模块")
public class AuthController {
	@ApiOperation(value = "测试",notes = "token放请求头")
	@ApiImplicitParams({
		@ApiImplicitParam(name = "token",value = "token值",dataType = "string",paramType = "header",required = true)
	})
	@GetMapping("/hello")
	public String hello() {
		return "hello";
	}
}

每个需要拦截的资源都要手动配置token?

全局配置

方式1

第一种设置全局变量的方式:

注意,复制之前springboot-swagger项目,再添加少量修改即可

修改配置类SwaggerConfig2.java

package com.briup.cms.config;

import java.util.ArrayList;
import java.util.List;

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

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class Swagger2Config {
	// ApiSelectorBuilder select() api选择器
	// 指定处理器 任何路径
	@Bean
	public Docket createDocket() {
		//配置全局参数
				ParameterBuilder tokenPar = new ParameterBuilder();  
				Parameter param = tokenPar.name("token")
							        	  .description("JWT令牌")
							        	  .modelRef(new ModelRef("string"))
							        	  .parameterType("header")
							        	  .required(false)
							        	  .build();  
		        
		        List<Parameter> pars = new ArrayList<Parameter>();  
		        pars.add(param);  
		
		return new Docket(DocumentationType.SWAGGER_2)
				.apiInfo(apiInfo())
				.select()
				.apis(RequestHandlerSelectors.basePackage("com.briup.cms.controller"))
				.paths(PathSelectors.any())
				.build()
				.globalOperationParameters(pars)
				.ignoredParameterTypes(HttpServletRequest.class,HttpServletResponse.class);
	}
	
	// swagger页面中显示的基本信息
	//@Bean
	private ApiInfo apiInfo() {
		return new ApiInfoBuilder()
				.title("cms")
				.description("新闻发布系统")
				.version("1.0")
				.contact(new Contact("vanse", "http://wangsidandan.github.io", "wangsidandan@gmail.com"))
				.build();
	}
	
	


}


在这里插入图片描述
在这里插入图片描述
此时在swagger中的接口,都自动添加了这个全局参数token
并且请求时也携带了这个token

不需要认证的接口也添加了token,比如登录接口,所以这样做不满足实际使用

方式2

复制之前springboot-swagger项目,少量修改

package com.briup.cms.config;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

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

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;

import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.Contact;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class Swagger2Config {
	// ApiSelectorBuilder select() api选择器
	// 指定处理器 任何路径
	@Bean
	public Docket createDocket() {
		return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
				.apis(RequestHandlerSelectors.basePackage("com.briup.cms.controller")).paths(PathSelectors.any())
				.build().securitySchemes(security()).securityContexts(securityContexts())
				.ignoredParameterTypes(HttpServletRequest.class, HttpServletResponse.class);
	}

	// swagger页面中显示的基本信息
	// @Bean
	private ApiInfo apiInfo() {
		return new ApiInfoBuilder().title("cms").description("新闻发布系统").version("1.0")
				.contact(new Contact("vanse", "http://wangsidandan.github.io", "wangsidandan@gmail.com")).build();
	}

	/**
	 * 设置认证中显示的显示的基本信息
	 */
	private List<ApiKey> security() {
		return Collections.singletonList(new ApiKey("Authorization", "token", "header"));
	}

	/**
	 * 设置认证规则
	 */
	private List<SecurityContext> securityContexts() {

		List<String> antPaths = new ArrayList<String>();
		antPaths.add("/auth/**");

		return Collections.singletonList(SecurityContext.builder().securityReferences(defaultAuth())
				.forPaths(antPathsCondition(antPaths)).build());
	}

	/**
	 * 返回认证路径的条件
	 */
	private Predicate<String> antPathsCondition(List<String> antPaths) {

		List<Predicate<String>> list = new ArrayList<>();

		antPaths.forEach(path -> list.add(PathSelectors.ant(path)));

		return Predicates.or(list);

	}

	/**
	 * 设置认证的范围,以及认证的类型
	 */
	private List<SecurityReference> defaultAuth() {
		AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
		AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
		authorizationScopes[0] = authorizationScope;
		return Collections.singletonList(new SecurityReference("Authorization", authorizationScopes));
	}

}


这里比之前新增的代码如下:
在这里插入图片描述
新增代码如图所示,以及下面新增的几个方法
在这里插入图片描述
这里会显示一个“锁”的图标,表示这里有些接口是需要认证的
展开模块后,访问路径符合要求接口,也会显示“锁”的图标
在这里插入图片描述
点击锁的图标,添加请求头中的统一认证信息(token):
在这里插入图片描述

在这里插入图片描述
点击认证按钮后,进行访问测试:
在这里插入图片描述
这时候,有“锁”图标的接口,在访问的时候,都会携带刚刚设置的请求头中的认证信息token值
在这里插入图片描述
这时,无“锁”图标的接口,在访问的时候默认不会携带设置的请求头信息

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值