JWT
这里写目录标题
- JWT
- 一级目录
- 二级目录
- 三级目录
- 1.什么是JWT
- 2.JWT的组成部分
- 3.编码/解码
- 4.特点
- 5. 为什么使用JWT
- 5.1传统的验证方式
- 5.2基于JWT的验证方式
- 6.JWT进行登录验证
- 6.1依赖安装
- 6.2编写UserDetailServiceImpl类
- 6.3编写UserDetailsImpl类
- 6.4 实现config.SecurityConfig类
- 6.5实现utils.JwtUtil类
- 6.6实现config.filter.JwtAuthenticationTokenFilter类
- 6.7配置config.SecurityConfig类
- 7.常见的面试题
一级目录
二级目录
三级目录
1.什么是JWT
- 全称 JSON Web Token,简称JWT,是一个基于RFC7519的爱看i发数据标准,定义了一种宽松且紧凑的数据组合方式
- 是一种加密后的数据载体,可在各应用之间进行数据传输
- 最常应用于授权认证,一旦用户登录,每个请求都将包含JWT,系统每次处理用户请求都会及逆行JWT安全检验,通过之后在进行处理
2.JWT的组成部分
JWT由3部分组成,使用.
拼接
-
Header
- 声明类型,默认JWT
- 声明加密算法,HMAC,RSA,ECDSA等常用算法
{ "alg": "HS256", "typ": "JWT" }
- alg:表示签名的算法,默认HMAC SHA256(写成HS256)
- type:表示声明类型,默认JWT
使用Base64编码后构成JWT,的第一部分
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
-
Payload
- 存放有效信息的地方
标准载荷
建议但不强制使用
标准载荷 介绍 iss (issuer) 签发人(谁签发的) exp (expiration time) 过期时间,必须要大于签发时间 sub (subject) 主题(用来做什么) aud (audience) 受众(给谁用的)比如:http://www.xxx.com nbf (Not Before) 生效时间 iat (Issued At) 签发时间 jti (JWT ID) 编号,JWT 的唯一身份标识 自定义载荷
可以添加任何信息,一般添加用户的相关信息或业务所需要的必要信息。但不建议添加敏感信息
{ //第一部分用户信息 "user_info": [ { "id": "1"//id }, { "name": "dafei"//姓名 }, { "age": "18"//年龄 } ], "iat": 1681571257,//签发时间 "exp": 1682783999,//过期时间 "aud": "xiaofei",//受众 "iss": "dafei",//签发人 "sub": "alluser"//主题 }
使用Base64编码后构成了第二部分
eyJ1c2VyX2luZm8iOlt7ImlkIjoiMSJ9LHsibmFtZSI6ImRhZmVpIn0seyJhZ2UiOiIxOCJ9XSwiaWF0IjoxNjgxNTcxMjU3LCJleHAiOjE2ODI3ODM5OTksImF1ZCI6InhpYW9mZWkiLCJpc3MiOiJkYWZlaSIsInN1YiI6ImFsbHVzZXIifQ
-
signature
- 私钥:只有服务器才知道
- 公钥:按照以下算法产生
signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
用Base64编码,构成第三部分
将三部分拼接成一个字符串,便是jwt了
由于密钥的存在,即使调用方修改了前两部分内容,在验证不放呢会出现签名不一致的情况,保证了全性
3.编码/解码
编码工具
https://tooltt.com/jwt-encode/
解码工具
https://tool.box3.cn/jwt.html
4.特点
优点
-
无状态
JWT 不需要在服务端存储任何状态,客户端可以携带 JWT 来访问服务端,从而使服务端变得无状态。这样,服务端就可以更轻松地实现扩展和负载均衡。
-
自定义
JWT 的载荷部分可以自定义,可以存储任何 JSON 格式的数据。这意味着我们可以使用 JWT 来实现一些自定义的功能,例如存储用户喜好、配置信息等等。
-
扩展性强
JWT 有一套标准规范,因此很容易在不同平台和语言之间共享和解析。此外,开发人员可以根据需要自定义声明(claims)来实现更加灵活的功能。
-
调试性好
由于 JWT 的内容是以 Base64 编码后的字符串形式存在的,因此非常容易进行调试和分析。
缺点
-
安全性取决于密钥管理
JWT 的安全性取决于密钥的管理。如果密钥被泄露或者被不当管理,那么 JWT 将会受到攻击。因此,在使用 JWT 时,一定要注意密钥的管理,包括生成、存储、更新、分发等等。
-
无法撤销
由于 JWT 是无状态的,一旦 JWT 被签发,就无法撤销。如果用户在使用 JWT 认证期间被注销或禁用,那么服务端就无法阻止该用户继续使用之前签发的 JWT。因此,开发人员需要设计额外的机制来撤销 JWT,例如使用黑名单或者设置短期有效期等等。
-
需要缓存到客户端
由于 JWT 包含了用户信息和授权信息,一般需要客户端缓存,这意味着 JWT 有被窃取的风险。
-
载荷大小有限制
由于 JWT 需要传输到客户端,因此载荷大小也有限制。一般不建议载荷超过 1KB,会影响性能。
5. 为什么使用JWT
5.1传统的验证方式
- 传统的认证方式,是基于Session的认证
Cookie和Session
- Cookie:一种让程序员在本地存储数据的能力,让数据在客户端这边更持久化
- Cookie里面存的是键值对的格式数据,键值对用“;”分割,键和值之间用“,”分割
登录的认证
Session
-
session是在服务器的一种机制,因为cookie是客户端保存的数据,而这些数据又是跟用户强烈相关联的,显然保存在客户端这边就不太合适(太多,也占资源),所以把数据都保存在服务器这边就比较的合适;保存的方式就是通过session的方式来进行保存的。键值对结构
-
使用之后,客户端只需要保存 sessionId就可以了,后续的请求带上 sessionId,服务器就会根据 sessionId 就会找到对应的用户数据详细的信息。
认证流程
- 客户端:发起一个认证请求
- 服务器端:认证通过存储一个session的用户信息;把sessionid写入客户端cookie
- 客户端再次访问:自动携带sessionid,找到对应session
问题分析
-
通常来说session保存在服务器的内存中,随着认证用户增多,会加大服务器的开销
-
因为session保存在服务器内存中,所以对于分布式应用来说,限制了应用的扩展能力
-
cookie被截取,会容易受造跨站请求伪造攻击
-
在前后端分离的系统中更加痛苦,比如后端是集群分布,需要设计共享机制、
5.2基于JWT的验证方式
- 客户端: 发起一个请求认证,带有用户名,密码
- 服务器:认证通过,生成JWT,响应给客户端
- 客户端:将Token保存到本地
日后客户端再次认证
- 客户端:携带JWT发送给服务器
- 服务器:进行验证,通过,展示数据;否则返回错误信息
6.JWT进行登录验证
6.1依赖安装
spring-boot-starter-security
jjwt-api
jjwt-impl
jjwt-jackson
meaven仓库地址:Maven Repository: Search/Browse/Explore (mvnrepository.com)
6.2编写UserDetailServiceImpl类
- 继承
UserDetailsService
接口 - 作用:接入数据库信息,主要是用来根据用户名查找用户
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<User> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("username",username);
User user=userMapper.selectOne(queryWrapper);
if(user==null){
throw new RuntimeException("用户不存在");
}
return new UserDetailsImpl(user);
}
}
6.3编写UserDetailsImpl类
- 使用户的一个接口类,用来判断用户的密码,权限是否失效,过期。
package com.kob.backend.service.impl.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import com.kob.backend.pojo.User;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserDetailsImpl implements UserDetails {
private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
6.4 实现config.SecurityConfig类
- 用来实现用户密码的加密存储
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
6.5实现utils.JwtUtil类
- 是jwt工具类的一种
- 作用:用来创建,解析jwt token
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
@Component
public class JwtUtil {
public static final long JWT_TTL = 60 * 60 * 1000L * 24 * 14; // 有效期14天
public static final String JWT_KEY = "SDFGjhdsfalshdfHFdsjkdsfds121232131afasdfac";
public static String getUUID() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if (ttlMillis == null) {
ttlMillis = JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid)
.setSubject(subject)
.setIssuer("sg")
.setIssuedAt(now)
.signWith(signatureAlgorithm, secretKey)
.setExpiration(expDate);
}
public static SecretKey generalKey() {
byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256");
}
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(jwt)
.getBody();
}
}
6.6实现config.filter.JwtAuthenticationTokenFilter类
- 进行jwt token的验证,如果成功,将用户信息注入上下文中
import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User;
import com.kob.backend.service.impl.utils.UserDetailsImpl;
import com.kob.backend.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserMapper userMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
token = token.substring(7);
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
throw new RuntimeException(e);
}
User user = userMapper.selectById(Integer.parseInt(userid));
if (user == null) {
throw new RuntimeException("用户名未登录");
}
UserDetailsImpl loginUser = new UserDetailsImpl(user);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
6.7配置config.SecurityConfig类
- 放行登录,注册页面
import com.kob.backend.config.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//在这里方形的登录注册页面,填写访问地址
.antMatchers("/user/account/token/", "/user/account/register/").permitAll()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
7.常见的面试题
问:什么是JWT?解释一下它的结构。
JWT是一种开放标准,用于在网络上安全地传输信息。它由三部分组成:头部、载荷和签名。头部包含令牌的元数据,载荷包含实际的信息(例如用户ID、角色等),签名用于验证令牌是否被篡改。
问:JWT的优点是什么?它与传统的session-based身份验证相比有什么优缺点?
JWT的优点包括无状态、可扩展、跨语言、易于实现和良好的安全性。相比之下,传统的session-based身份验证需要在服务端维护会话状态,使得服务端的负载更高,并且不适用于分布式系统。
问:在JWT的结构中,分别有哪些部分?每个部分的作用是什么?
JWT的结构由三部分组成:头部、载荷和签名。头部包含令牌类型和算法,载荷包含实际的信息,签名由头部、载荷和密钥生成。
问:JWT如何工作?从开始到验证过程的完整流程是怎样的?
JWT的工作流程分为三个步骤:生成令牌、发送令牌、验证令牌。在生成令牌时,服务端使用密钥对头部和载荷进行签名。在发送令牌时,将令牌发送给客户端。在验证令牌时,客户端从令牌中解析出头部和载荷,并使用相同的密钥验证签名。
问:什么是JWT的签名?为什么需要对JWT进行签名?如何验证JWT的签名?
JWT的签名是由头部、载荷和密钥生成的,用于验证令牌是否被篡改。签名使用HMAC算法或RSA算法生成。在验证JWT的签名时,客户端使用相同的密钥和算法生成签名,并将生成的签名与令牌中的签名进行比较。
问:什么是JWT的令牌刷新?为什么需要这个功能?
令牌刷新是一种机制,用于解决JWT过期后需要重新登录的问题。在令牌刷新中,服务端生成新的JWT,并将其发送给客户端。客户端使用新的JWT替换旧的JWT,从而延长令牌的有效期。
问:JWT是否加密?如果是,加密的部分是哪些?如果不是,那么它如何保证数据安全性?
JWT本身并不加密,但可以在载荷中包含敏感信息。为了保护这些信息,可以使用JWE(JSON Web Encryption)对载荷进行加密。如果不加密,则需要在生成JWT时确保不在载荷中包含敏感信息。
问:在JWT中,如何处理Token过期的问题?有哪些方法可以处理?
JWT过期后,客户端需要重新获取新的JWT。可以通过在JWT中包含过期时间或使用refresh token等机制来解决过期问题。
问:JWT和OAuth2有什么关系?它们之间有什么区别?
JWT和OAuth2都是用于身份验证和授权的开放标准。JWT是一种身份验证机制,而OAuth2是一种授权机制。JWT用于在不同的系统中安全地传输信息,OAuth2用于授权第三方应用程序访问受保护的资源。
问:JWT在什么场景下使用较为合适?它的局限性是什么?
JWT在单体应用或微服务架构中的使用比较合适。它的局限性包括无法撤销、令牌较大、无法处理并发等问题。在需要针对每次请求进行访问控制或需要撤销令牌的情况下,JWT可能不是最佳选择。