文章目录
JWT
1、什么是JWT
官网:https://jwt.io/introduction
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
JSON Web Token (JWT) 是一个开放标准 ( RFC 7519 ),它定义了一种紧凑且自包含的方式,用于在各方之间作为 JSON 对象安全地传输信息。该信息可以被验证和信任,因为它是经过数字签名的。JWT 可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
一句话:JWT用户分布式系统的单点登录SSO场景,主要用来做用户身份鉴别或者资源(接口)安全性的一种技术或者一种机制。
- 身份鉴别
- 资源接口安全性校验,保护服务器资源不被泄漏
Token
1、什么是Token?
Token是服务器端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。
服务器的接口安全性问题
1、token生成和获取的阶段:一般来说都是在登录的时候,就生成token
2、然后在未来的每一次请求需要进行安全校验的情况下都需要携带token到服务端进行对比和比较
3、传统的做法一般可以使用Map或者session来完成, 但是这个会消耗大量服务资源。
4、 现在比较主流的最佳使用解决方案是:JWT。因为他是无状态并且不会去消耗太多的服务器资源的一种解决方案。
2、JWT能做什么
1、授权
- 这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用,。
2、信息交换
- JSON Web Token是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如,使用公钥/私钥),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改。
3、为什么是JWT
传统的session认证
HTTP是一种没有状态的协议,也就是它并不知道是谁访问应用。这里我们把用户看成客户端,客户端使用用户名还有密码通过了身份验证,不过下回这个客户端再发送请求时候,还得再验证一下。
解决的方法就是,当用户请求登录的时候,如果没有问题,我们在服务端生成一条记录,这个记录可以说明一下登录的用户是谁,然后把这条记录的ID好发送给客户端,客户端收到以后把这个 ID 号存储在 Cookie 里,下次这个用户再向服务端发送请求的时候,可以带着这个 Cookie ,这样服务端会验证一个这个 Cookie 里的信息,看啊看能不能在服务端里面找到对应的记录,如果可以,说明用户已近通过了身份验证,就把用户请求的数据返回给客户端。
上面说的就是 Session,我们需要在服务端为登录的用户生成 Session,这些 Session 可能会存储在内存,磁盘,或者数据库里。我们可能需要在服务端定期的去清理过期的 Session。
显露的问题
**Session:**每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
**扩展性:**用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
**CSRF:**因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
基于JWT认证
基于Token的身份验证法,在服务端不需要存储用户的登录记录。流程是这样的:
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个Token。返给客户端,存放在Cookies里或者Local Storage里
- 客户端每次想服务端请求资源的时候需要带着Token(将token放入HTTP Header中的Authorization )
- 服务端验证token是否正确、是否过期,如果验证成功,就向客户端返回请求的数据;失败返回错误信息,让他重新登录。
jwt优势
- 简洁(Compact):可以通过URL,POST参数或者在HTTP header发送,因为数量小,传输速度也很快
- 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
- 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持
- 不需要在服务端保存回话信息,特别适用于分布式微服务
4、JWT 的结构
4.1、令牌组成
实际的 JWT 大概就像下面这样。
它是一个很长的字符串,中间用点(.
)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。
JWT 的三个部分依次如下。
- Header(头部)
- Payload(负载)
- Signature(签名)
写成一行,就是下面的样子。
xxxxx.yyyyy.zzzzz
Header.Payload.Signature
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o9fBlDce-1639747081324)(https://www.wangbase.com/blogimg/asset/201807/bg2018072303.jpg)]
下面依次介绍这三个部分。
4.2、Header
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{
"alg": "HS256",
"typ": "JWT"
}
上面代码中,alg
属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ
属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
。
最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。
4.3、Payload
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。
4.4、Signature
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.
)分隔,就可以返回给用户。
4.5、Base64URL
前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。
JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+
、/
和=
,在 URL 里面有特殊含义,所以要被替换掉:=
被省略、+
替换成-
,/
替换成_
。这就是 Base64URL 算法。
5、JWT 的几个特点
(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。
(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
6、JWT 的使用方式
1、引入依赖
dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
2、生成token
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND, 60);
// 生成令牌
String token = JWT.create()
.withClaim("userId", 21)
.withClaim("username", "张三") // 设置自定义用户名
.withExpiresAt(instance.getTime()) // 设置过期时间
.sing(Algorithm.HMAC256("token!15ef65efwe")); // 设置签名 保密 复杂
System.out.println(token);
3、根据令牌和签名解析数据
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("token!15ef65efwe")).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
System.out.println("id:" + decodedJWT.getClaim("userId").asInt());
System.out.println("用户名:" + decodedJWT.getClaim("username").asString());
System.out.println("过期时间:" + decodedJWT.getExpiresAt());
常见异常信息
- SignatureVerificationException: 签名不一致异常
- TokentExpiredException: 令牌过期异常
- AlgorithmMismatchException: 算法不匹配异常
- InvalidClaimException: 失效的payload异常
封装工具类
public class JWTUtils {
private static final String SIGN = "token!15ef65efwe";
/**
* 生成token
*
* @param map
* @return
*/
public static String getToken(Map<String, Object> map){
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE, 7); // 默认7天过期
// 生成令牌
JWTCreator.Builder builder = JWT.create();
map.forEach((k,v)->{
builder.withClaim(k,v);
});
String token = builder.withExpiresAt(instance.getTime()) // 设置过期时间
.sing(Algorithm.HMAC256(SIGN)); // 设置签名
return token;
}
/**
* 验证token
*
* @param token
*/
public static void verify(String token){
JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
}
/**
* 获取token信息
* @param token
* @return
*/
public static DecodedJWT getTokenInfo(String token){
DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
return decodedJWT;
}
}
springboot整合jwt
1、引入依赖
<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>
2、创建数据库
3、添加配置
server.port=8989
spring.application.name=jwt
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/jwt?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=UTC&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
mybatis.type-aliases-package=com.chilly.entity
mybatis.mapper-locations=classpath:com/chilly/mapper/*.xml
logging.level.com.chilly.dao=debug
4、编写代码
实体类
@Data
@Accessors(chain = true)
public class User {
private String id;
private String name;
private String password;
}
dao
@Mapper
public interface UserDAO {
User login(User user);
}
<?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.chilly.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);
}
@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 = JWTUtils.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 = JWTUtils.verify(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;
}
}
5、添加拦截器
@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 {
JWTUtils.verify(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;
}
}
注入到springboot
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/user/test")
.excludePathPatterns("/user/login")
;
}
}
测试