暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

JWT介绍与SpringBoot整合使用方式

风雪留客 2021-03-06
388

简介

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

应用场景

1.Authorization (授权)

这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。

2. Information Exchange (信息交换)

对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWT可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。

为什么使用jwt

传统的session认证

我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。

但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来.

基于session认证所显露的问题

Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。

扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。

CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

基于token的鉴权机制

基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

优点

json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。它不需要在服务端保存会话信息, 所以它易于应用的扩展

jwt认证流程

用户使用用户名密码来请求服务器服务器进行验证服务器通过验证发送给用户一个token客户端存储token,并在每次请求时附送上这个token值服务端验证token值,并返回数据

JWT结构

JWT是由三段信息构成的,分别为:header(头).playload(有效负荷).signature(签名),这三段信息文本用"."链接一起就构成了Jwt字符串。类似于:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTQ1MTk5MjksImFnZSI6MTgsInVzZXJuYW1lIjoi5byg5LiJIn0.UK4JTHsLzan2DF33cgUUuhT7anP59MtHZ5m7A1TQBPM

header

标头通常由两部分组成:令牌的类型(DJWT) 和所使用的签名算法,例如HMAC SHA256或RSA。它会使用Base64编码组成JWT结构的第一一部分。需要注意的是:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。

例如:

{
'typ': 'JWT', # 令牌类型
'alg': 'HS256' # 加密算法
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

playload

令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用Base64 编码组成JWT结构的第二部分,该部分内容尽量不是敏感信息

playload包含三个部分:

标准中注册的声明公共的声明私有的声明

标准中注册的声明 (建议但不强制使用) :

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

公共的声明 :

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

私有的声明 :

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

例如:

{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

signature

前面两部分都是使用Base64 进行编码的,即前端可以解开知道里面的信息。Signature需要使用编码后的header和payload以及我们提供的一个密钥,然后使用header 中指定的签名算法(HS256) 进行签名。签名的作用是保证JWT没有被篡改过 HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload),secret)

最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。

JWT使用

引入依赖

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

生成token

    

@Test
void contextLoads() {


        HashMap<String, Object> map = new HashMap<>();
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.HOUR_OF_DAY,1);


String token = JWT.create()
.withHeader(map) // 第一部分header
.withClaim("username", "张三") // 第二部分,即playload
.withClaim("age", 18)
.withExpiresAt(calendar.getTime()) // 指定令牌的过期时间
.sign(Algorithm.HMAC256("\"!@#$%^&*()_+\""));// 签名
System.out.println(token);


}


// 第一部分可以省略不写
@Test
void contextLoads() {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.HOUR_OF_DAY,1);


String token = JWT.create()
.withClaim("username", "张三") // 第二部分,即playload
.withClaim("age", 18)
.withExpiresAt(calendar.getTime()) // 指定令牌的过期时间
.sign(Algorithm.HMAC256("\"!@#$%^&*()_+\""));// 签名
System.out.println(token);


}

输入结果:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTQ1MTk5MjksImFnZSI6MTgsInVzZXJuYW1lIjoi5byg5LiJIn0.UK4JTHsLzan2DF33cgUUuhT7anP59MtHZ5m7A1TQBPM

校验token

   

 @Test
void test(){
// 构建验证对象,签名算法需要一致
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("\"!@#$%^&*()_+\"")).build();
// 获取验证结果
DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTQ1MjA3NzcsInVzZXJpZCI6IjE0MzMyMjMiLCJhZ2UiOjE4LCJ1c2VybmFtZSI6IuW8oOS4iSJ9.kjojt6HBGmWkl9XjIT7_zm7XhAn9Nl-E8RSk5sIXUmI");
// 结果为字符串时,需要执行asString()
System.out.println(verify.getClaim("username").asString());
System.out.println(verify.getClaim("userid").asString());
// 结果为整数时,需要执行asInt()
System.out.println(verify.getClaim("age").asInt());
// 获取令牌过期时间
System.out.println(verify.getExpiresAt());
}

常见异常

AlgorithmMismatchException: 算法不匹配异常

InvalidClaimException: 无效的Claim异常

JWTCreationException: JWT创建异常

JWTDecodeException: JWT解码异常

JWTVerificationException:JWT验证异常

TokenExpiredException: 令牌过期异常

封装工具类

    

private static final String SIGN = "\"!@#$%^&*()_+\"";


/**
* @description: 获取token
* @param: map
* @return: java.lang.String
* @author yangc
* @date: 2021/02/28 21:36
*/
public static String getToken(Map<String,String> map){
Calendar calendar = Calendar.getInstance();
// 七天过期
calendar.add(Calendar.DATE,7);
JWTCreator.Builder builder = JWT.create();
// 设置Claim
map.forEach((k,v)->{
builder.withClaim(k,v);
});
// 设置过期时间及签名
String token = builder.withExpiresAt(calendar.getTime())
.sign(Algorithm.HMAC256(SIGN));
return token;
}




/**
* @description: 验证token,返回验证数据
* @param: token
* @return: com.auth0.jwt.interfaces.DecodedJWT
* @author yangc
* @date: 2021/02/28 21:36
*/
public static DecodedJWT getTokenInfo(String token){
return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}

与Spring Boot 整合

新建数据库

CREATE TABLE `user` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

引入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.chilly</groupId>
<artifactId>springboot-jwt-2020</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-jwt-2020</name>
<description>Demo project for Spring Boot</description>


<properties>
<java.version>1.8</java.version>
</properties>


<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>


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


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


<!--引入mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>


<!--引入druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.23</version>
</dependency>


<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>


<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>


</project>

application.yml

server:
port: 11808
spring:
datasource:
name: root
password: 123456
url: jdbc:mysql://localhost:3306/jwt?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver


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


logging:
level: com.minio.demo.dao=debug

创建Dao

@Mapper
public interface UserDAO {
User login(User user);
}

编写mapper.xml

<?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.minio.demo.dao.UserDAO">
<select id="login" parameterType="User" resultType="User">
select id,name,password from user where name =#{name} and password=#{password}
</select>
</mapper>

Service

public interface UserService {


/**
* 登录接口
*
* @param user 表单中的user
* @return 数据库中查询到的User
*/
User login(User user);


}

ServiceImpl

@Service
public class UserServiceImpl implements UserService {


@Resource
private UserDAO userDao;


@Override
@Transactional(propagation = Propagation.SUPPORTS)
public User login(User user) {
User userDB = userDao.login(user);
if (userDB != null) {
return userDB;
}
throw new RuntimeException("认证失败");
}
}

Controller

@RestController
@Slf4j
public class UserController {


@Resource
private UserService userService;


@GetMapping("/user/login")
public Map<String, Object> login(User user) {
log.info("用户名:{}", user.getName());
log.info("password: {}", user.getPassword());


Map<String, Object> map = new HashMap<>();


try {
User userDB = userService.login(user);


Map<String, String> payload = new HashMap<>();
payload.put("id", userDB.getId());
payload.put("name", userDB.getName());
String token = JWTUtil.getToken(payload);
map.put("state", true);
map.put("msg", "登录成功");
map.put("token", token);
return map;
} catch (Exception e) {
e.printStackTrace();
map.put("state", false);
map.put("msg", e.getMessage());
map.put("token", "");
}
return map;
}


@PostMapping("/user/test")
public Map<String, Object> test(HttpServletRequest request) {
String token = request.getHeader("token");
DecodedJWT verify = JWTUtil.getTokenInfo(token);
String id = verify.getClaim("id").asString();
String name = verify.getClaim("name").asString();
log.info("用户id:{}", id);
log.info("用户名: {}", name);


//TODO 业务逻辑
Map<String, Object> map = new HashMap<>();
map.put("state", true);
map.put("msg", "请求成功");
return map;
}


}

JWTInterceptor

@Slf4j
public class JWTInterceptor implements HandlerInterceptor {


@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {


//获取请求头中的令牌
String token = request.getHeader("token");
log.info("当前token为:{}", token);


Map<String, Object> map = new HashMap<>();
try {
JWTUtil.getTokenInfo(token);
return true;
} catch (SignatureVerificationException e) {
e.printStackTrace();
map.put("msg", "签名不一致");
} catch (TokenExpiredException e) {
e.printStackTrace();
map.put("msg", "令牌过期");
} catch (AlgorithmMismatchException e) {
e.printStackTrace();
map.put("msg", "算法不匹配");
} catch (InvalidClaimException e) {
e.printStackTrace();
map.put("msg", "失效的payload");
} 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().println(json);
return false;
}
}


InterceptorConfig

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {


@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/user/test")
.excludePathPatterns("/user/login");
}
}

测试获取token

验证token


文章转载自风雪留客,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论