概述
背景:传统的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值
这时,无“锁”图标的接口,在访问的时候默认不会携带设置的请求头信息