JWT由3部分组成,使用.
拼接
Header
{ "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了
由于密钥的存在,即使调用方修改了前两部分内容,在验证不放呢会出现签名不一致的情况,保证了全性
编码工具
https://tooltt.com/jwt-encode/
解码工具
https://tool.box3.cn/jwt.html
优点
无状态
JWT 不需要在服务端存储任何状态,客户端可以携带 JWT 来访问服务端,从而使服务端变得无状态。这样,服务端就可以更轻松地实现扩展和负载均衡。
自定义
JWT 的载荷部分可以自定义,可以存储任何 JSON 格式的数据。这意味着我们可以使用 JWT 来实现一些自定义的功能,例如存储用户喜好、配置信息等等。
扩展性强
JWT 有一套标准规范,因此很容易在不同平台和语言之间共享和解析。此外,开发人员可以根据需要自定义声明(claims)来实现更加灵活的功能。
调试性好
由于 JWT 的内容是以 Base64 编码后的字符串形式存在的,因此非常容易进行调试和分析。
缺点
安全性取决于密钥管理
JWT 的安全性取决于密钥的管理。如果密钥被泄露或者被不当管理,那么 JWT 将会受到攻击。因此,在使用 JWT 时,一定要注意密钥的管理,包括生成、存储、更新、分发等等。
无法撤销
由于 JWT 是无状态的,一旦 JWT 被签发,就无法撤销。如果用户在使用 JWT 认证期间被注销或禁用,那么服务端就无法阻止该用户继续使用之前签发的 JWT。因此,开发人员需要设计额外的机制来撤销 JWT,例如使用黑名单或者设置短期有效期等等。
需要缓存到客户端
由于 JWT 包含了用户信息和授权信息,一般需要客户端缓存,这意味着 JWT 有被窃取的风险。
载荷大小有限制
由于 JWT 需要传输到客户端,因此载荷大小也有限制。一般不建议载荷超过 1KB,会影响性能。
Cookie和Session
- Cookie:一种让程序员在本地存储数据的能力,让数据在客户端这边更持久化
- Cookie里面存的是键值对的格式数据,键值对用“;”分割,键和值之间用“,”分割
登录的认证
Session
session是在服务器的一种机制,因为cookie是客户端保存的数据,而这些数据又是跟用户强烈相关联的,显然保存在客户端这边就不太合适(太多,也占资源),所以把数据都保存在服务器这边就比较的合适;保存的方式就是通过session的方式来进行保存的。键值对结构
使用之后,客户端只需要保存 sessionId就可以了,后续的请求带上 sessionId,服务器就会根据 sessionId 就会找到对应的用户数据详细的信息。
认证流程
- 客户端:发起一个认证请求
- 服务器端:认证通过存储一个session的用户信息;把sessionid写入客户端cookie
- 客户端再次访问:自动携带sessionid,找到对应session
问题分析
通常来说session保存在服务器的内存中,随着认证用户增多,会加大服务器的开销
因为session保存在服务器内存中,所以对于分布式应用来说,限制了应用的扩展能力
cookie被截取,会容易受造跨站请求伪造攻击
在前后端分离的系统中更加痛苦,比如后端是集群分布,需要设计共享机制、
- 客户端: 发起一个请求认证,带有用户名,密码
- 服务器:认证通过,生成JWT,响应给客户端
- 客户端:将Token保存到本地
日后客户端再次认证
- 客户端:携带JWT发送给服务器
- 服务器:进行验证,通过,展示数据;否则返回错误信息
spring-boot-starter-security
jjwt-api
jjwt-impl
jjwt-jackson
meaven仓库地址:Maven Repository: Search/Browse/Explore (mvnrepository.com)
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);
}
}
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;
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
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();
}
}
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);
}
}
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);
}
}
问:什么是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可能不是最佳选择。