Spring security之JWT

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 就会找到对应的用户数据详细的信息。

认证流程

image-20230808162008667

  1. 客户端:发起一个认证请求
  2. 服务器端:认证通过存储一个session的用户信息;把sessionid写入客户端cookie
  3. 客户端再次访问:自动携带sessionid,找到对应session

问题分析

  • 通常来说session保存在服务器的内存中,随着认证用户增多,会加大服务器的开销

  • 因为session保存在服务器内存中,所以对于分布式应用来说,限制了应用的扩展能力

  • cookie被截取,会容易受造跨站请求伪造攻击

  • 在前后端分离的系统中更加痛苦,比如后端是集群分布,需要设计共享机制、

5.2基于JWT的验证方式

Spring security之JWT_第1张图片

  1. 客户端: 发起一个请求认证,带有用户名,密码
  2. 服务器:认证通过,生成JWT,响应给客户端
  3. 客户端:将Token保存到本地

日后客户端再次认证

  1. 客户端:携带JWT发送给服务器
  2. 服务器:进行验证,通过,展示数据;否则返回错误信息

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可能不是最佳选择。

你可能感兴趣的:(spring,java,后端)