【分布式微服务专题】SpringSecurity快速入门

目录

  • 前言
  • 阅读对象
  • 阅读导航
  • 前置知识
  • 笔记正文
    • 一、Spring Security介绍
      • 1.1 什么是Spring Security
      • 1.2 它是干什么的
      • 1.3 Spring Security和Shiro比较
    • 二、快速开始
      • 2.1 用户认证
        • 2.1.1 设置用户名
          • 2.1.1.1 基于application.yml配置文件
          • 2.1.1.2 基于Java Config配置方式
        • 2.1.2 设置加密方式
          • 2.1.2.1 {id}encodedPassword
          • 2.1.2.2 使用PasswordEncoder加密
        • 2.1.3 自定义用户信息加载
        • 2.1.4 自定义登录页面
        • 2.1.5 前后端分离认证
        • 2.1.6 用户认证流程总结
      • 2.2 访问控制
        • 2.2.1 web授权: 基于url的访问控制
        • 2.2.2 方法授权:基于注解的访问控制
    • 三、Spring Security整合JWT实现自定义登录认证
      • 3.1 自定义登录认证业务流程
      • 3.2 JWT介绍
        • 3.2.1 什么是JWT
      • 3.3 JWT结构
        • 3.3.1 JWT头部header
        • 3.3.2 JWT载荷payload
        • 3.3.3 JWT签名signature
        • 3.3.4 组合在一起
        • 3.3.5 如何使用
      • 3.4 代码实现自定义登录
      • 3.5 JWT续期问题
        • 3.5.1 刷新令牌(Refresh Token)
        • 3.5.2 自动延长JWT有效期
  • 学习总结
  • 感谢

前言

阅读对象

阅读导航

系列上一篇文章:《

前置知识

笔记正文

一、Spring Security介绍

1.1 什么是Spring Security

官方介绍:Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于 Spring 的应用程序。
Spring Security 是一个框架,侧重于为 Java 应用程序提供身份验证和授权。与所有 Spring 项目一样,Spring 安全性的真正强大之处,在于它很容易扩展以满足定制需求

Spring Security是一个能够为基于Spring的企业应用系统提供声明式安全访问控制解决方案安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC、DI和AOP功能,类别是安全服务体系。

Spring Security对Web安全性的支持大量地依赖于Servlet过滤器。它可以提供应用程序层的安全解决方案,一个系统的安全还需要考虑传输层和系统层的安全,例如采用Htps协议、服务器部署防火墙等。

此外,Spring Security采用【安全层】的概念,使每一层都尽可能安全,连续的安全层可以达到全面的防护。在Controller层、Service层、DAO层等以加注解的方式来保护应用程序的安全。

1.2 它是干什么的

Spring Security 主要干的就两件事:

  1. Authentication:认证(who are you)。用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手机短信登录,指纹认证等方式
  2. Access Control:访问控制(what are you allowed to do)。授权是用户认证通过后,根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问

SpringSecurity在架构上将认证与授权分离,并提供了扩展点。

1.3 Spring Security和Shiro比较

在 Java 生态中,目前有 Spring Security 和 Apache Shiro 两个安全框架,可以完成认证和授权的功能。

Shiro:一个功能强大且易于使用的Java安全框架,提供了认证、授权、加密和会话管理

相同点不同点(以SpringSecurity出发)
SpringSecutiry认证功能
授权功能
加密功能
会话管理
缓存支持
rememberMe功能
优点:
1. SpringSecurity以Spring为基础,与Spring生态融合有天然的优势
2. SpringSecurity功能更加丰富些,例如安全防护
3. SpringSecurity社区资源比Shiro丰富

缺点:
1. SpringSecurity使用相对复杂,上手难度大
2. SpringSecurity依赖于Spring容器
Shiro

所以,网上有人说,对于常见的安全管理技术栈的组合是:

  • SSM + Shiro
  • Spring Boot/Spring Cloud +Spring Security

二、快速开始

接下来我们开始使用以下SpringSecutiry,在项目要开始之前,我们需要做一些项目准备。

1)快速新建一个SpringBoot项目
建议直接使用idea的Spring Initializer云上构建一个
在这里插入图片描述

2)添加pom依赖

        <!-- 接入spring security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!-- web配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

3)添加测试接口

    @GetMapping("/user/admin")
    public User admin() {
        User user = new User();
        user.setUserId(1);
        user.setName("admin");
        user.setPhone("10086");
        return user;
    }

4)启动项目,开始测试
引入Spring Security之后 ,访问任何API 接口时,需要首先进行登录,才能进行访问。如下所示:
在这里插入图片描述
默认用户名:user,密码可以查看控制台日志获取
在这里插入图片描述
输入账号密码之后,访问就正常了
在这里插入图片描述
5)退出
Spring security默认实现了logout退出,用户只需要向 Spring Security 项目中发送/logout请求即可

OK,准备工作做完之后,接下来开始进入正题。

2.1 用户认证

2.1.1 设置用户名
2.1.1.1 基于application.yml配置文件

配置内容如下:

spring:
  # Spring Security 配置项,对应 SecurityProperties 配置类
  security:
    user:
      name: user
      password: 123456
      roles:
        - admin

这个方式很容易理解,毕竟,我们在前面提到过了,SpringSecurity会有一个默认的用户。然而我们又没有在数据库里面记录它,所以,它肯定是存在于内存中。所以,SpringSecurity提供了这么一种机制给我们去修改它的默认账号密码。

2.1.1.2 基于Java Config配置方式

代码如下:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.builder()
                .username("shen")
                .password("{noop}123456")
                .roles("user")
                .build();

        UserDetails admin = User.builder()
                .username("admin")
                .password("{noop}123456")
                .roles("admin", "user")
                .build();

        return new InMemoryUserDetailsManager(user, admin);
    }
}

注意,上面的密码password("{noop}123456")中的{noop}是表示不需要加密,明文注册到内存中。至于加密,我会在后面提到。若没有上面这个配置,测试的时候会报错:There is no PasswordEncoder mapped for the id "null"

2.1.2 设置加密方式
2.1.2.1 {id}encodedPassword

这种方式是SpringSecurity提供的一种比较普适性的格式。整体分为两个部分:

  • {id}:设置的时候必须在{}花括号内。id为加密方式。可选的值如下图所示。大家只要记得{noop}是明文方式就好,其他代表的是不同的加密策略

具体见:org.springframework.security.crypto.factory.PasswordEncoderFactories
在这里插入图片描述

  • encodePassword:原始密码

简单的使用示例就是我在上个案例写到的,不过在这里我们修改一下shen用户的密码,采用{sha256}加密方式,明文为password

public class SecurityConfig {

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.builder()
                .username("shen")
                .password("{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0")
                .roles("user")
                .build();

        UserDetails admin = User.builder()
                .username("admin")
                .password("{noop}123456")
                .roles("admin", "user")
                .build();

        return new InMemoryUserDetailsManager(user, admin);
    }
}
2.1.2.2 使用PasswordEncoder加密

这个使用起来就更简单了,就是先注册声明一个Bean到Spring里面就好,比如使用bcrypt,通过上图PasswordEncoderFactories可以看见,需要注册的Bean如下:

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
2.1.3 自定义用户信息加载

在我们的开发中,通常是需要自定义的方式从数据库获取用户信息的。这种情况下,我们可以自行实现UserDetailsService接口:

@Service
public class ShenUserService implements UserDetailsService {

    @Resource
    private UserService userService;

    @Resource
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User byId = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getName, username));
        return org.springframework.security.core.userdetails.User
                .builder()
                .username(byId.getName())
                .password(passwordEncoder.encode("123456"))
                .roles("user")
                .build();
    }
}
2.1.4 自定义登录页面

Spring Security虽然给我们提供了默认登录页面,但通常我们都会自定义自己的登录页面,在项目中,我们想要自定义登录页面,只需要简单的两步:

1)编写登录页面
resources目录下新建static目录,然后在下面新增login.html文件,内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="/user/login" method="post">
        用户名:<input type="text" name="username"/><br/>
        密码:<input type="password" name="password"/><br/>
        <input type="submit" value="提交"/>
    </form>
</body>
</html>

2)配置Spring Security的过滤器链SecurityFilterChain

    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
                        .requestMatchers(new RequestMatcher() {
                            @Override
                            public boolean matches(HttpServletRequest request) {
                                String requestURI = request.getRequestURI();
                                return "/login.html".equals(requestURI);
                            }
                        }).permitAll() // loginPage 页面不需要身份认证 否则会无限重定向
                        .anyRequest().authenticated() // 其他请求都需要用户认证后访问
                )
                .formLogin((formLogin) -> formLogin
                        .loginPage("/login.html") // 自定义登录页面路径
                        .usernameParameter("username") // 定义从Form中获取用户名的key 与html中的form参数匹配
                        .passwordParameter("password") // 定义从Form中获取密码的key 与html中的form参数匹配
                        .loginProcessingUrl("/user/login") // 认证发起的URL,访问该URL则认证凭证 这样要与HTML中form的提交地址一致
                        .defaultSuccessUrl("/user/admin") // 认证成功之后跳转的路径
                )
                // 禁用httpBasic
                .httpBasic((httpBasic) -> httpBasic.disable())
                // 关闭跨站点请求伪造csrf防护
                .csrf((csrf) -> csrf.disable());
        return http.build();
    }

测试一下,你就会发现跳转到了自定义的界面

2.1.5 前后端分离认证

表单登录配置模块提供了successHandler()和failureHandler()两个方法,分别处理登录成功和登录失败的逻辑。携带当前登录用户名及其角色等信息;而failureHandler()方法携带一个AuthenticationException异常参数。

1)新增登录成功处理

    /**
     * 认证成功处理逻辑
     */
    public class LoginSuccessHandler implements AuthenticationSuccessHandler {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            response.setContentType("text/html;charset=utf-8");
            response.getWriter().write("登录成功");
        }
    }

2)新增登录失败处理

    /**
     * 认证失败处理逻辑
     */
    public class LoginFailureHandler implements AuthenticationFailureHandler {

        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            // TODO
            response.setContentType("text/html;charset=utf-8");
            response.getWriter().write("登录失败");
            exception.printStackTrace();
        }
    }

3)设置处理逻辑
修改上一步提到的过滤器链SecurityFilterChain代码,如下:

 .formLogin((formLogin) -> formLogin
                        .loginPage("/login.html") // 自定义登录页面路径
                        .usernameParameter("username") // 定义从Form中获取用户名的key 与html中的form参数匹配
                        .passwordParameter("password") // 定义从Form中获取密码的key 与html中的form参数匹配
                        .loginProcessingUrl("/user/login") // 认证发起的URL,访问该URL则认证凭证 这样要与HTML中form的提交地址一致
                        .defaultSuccessUrl("/user/admin") // 认证成功之后跳转的路径
                        .successHandler(new LoginSuccessHandler()) // 登录成功处理逻辑
                        .failureHandler(new LoginFailureHandler()) // 登录失败处理逻辑
2.1.6 用户认证流程总结

网上找的一个流程图,我跟着走了一遍,大概是这样的,只不过有些类需要自己走一下,确定一下
在这里插入图片描述

2.2 访问控制

授权的方式包括 web授权和方法授权,web授权是通过url拦截进行授权,方法授权是通过方法拦截进行授权。

2.2.1 web授权: 基于url的访问控制

Spring Security可以通过http.authorizeRequests()对web请求进行授权保护 ,Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。配置顺序会影响之后授权的效果,越是具体的应该放在前面,越是笼统的应该放到后面。
代码示例如下:

 @Bean
 public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {

     // 其余配置
     http.formLogin((formLogin) -> formLogin
                     .loginPage("/login.html") // 自定义登录页面路径
                     .usernameParameter("username") // 定义从Form中获取用户名的key 与html中的form参数匹配
                     .passwordParameter("password") // 定义从Form中获取密码的key 与html中的form参数匹配
                     .loginProcessingUrl("/user/login") // 认证发起的URL,访问该URL则认证凭证 这样要与HTML中form的提交地址一致
                     .defaultSuccessUrl("/user/admin") // 认证成功之后跳转的路径
                     .successHandler(new LoginSuccessHandler()) // 登录成功处理逻辑
                     .failureHandler(new LoginFailureHandler()) // 登录失败处理逻辑
             )
             // 禁用httpBasic
             .httpBasic(AbstractHttpConfigurer::disable)
             // 关闭跨站点请求伪造csrf防护
             .csrf(AbstractHttpConfigurer::disable);

     // 对请求进行访问控制
     doSetAccessControl(http);

     return http.build();
 }
    
 private void doSetAccessControl(HttpSecurity http) throws Exception {

        // 设置可以直接访问的资源
        http.authorizeHttpRequests((permitAll) -> permitAll.antMatchers(ignoreUrls).permitAll());

        // 设置admin特有资源,只有admin可以访问
        http.authorizeHttpRequests((adminApi) -> adminApi.antMatchers("/sys/**").hasRole("admin"));

        // 设置wallet接口访问权限
        http.authorizeHttpRequests((walletApi) -> walletApi.antMatchers("/wallet/**").hasAuthority("wallet:api"));

        // 其他请求都需要用户认证后访问
        http.authorizeHttpRequests((others) -> others.anyRequest().authenticated());
    }

为了方便测试,我写了一个简单的Controller,然后新增了一些限制API:

@RestController
public class TestController {

    @GetMapping("/test/get")
    public String test() {
        return "获取一个test资源";
    }

    @GetMapping("/sys/get")
    public String sysTest() {
        return "获取一个sys资源";
    }

    @GetMapping("/order/get")
    public String orderTest() {
        return "获取一个order资源";
    }
}

并且,固定我的登录用户只有user:api的权限:

 **/
@Service
public class ShenUserService implements UserDetailsService {

    @Resource
    private UserService userService;

    @Resource
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User byId = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getName, username));
        return org.springframework.security.core.userdetails.User
                .builder()
                .username(byId.getName())
                .password(passwordEncoder.encode("123456"))
                .authorities("user:api")
                .build();
    }
}

2.2.2 方法授权:基于注解的访问控制

Spring Security在方法的权限控制上支持三种类型的注解,JSR-250注解、@Secured注解和支持表达式的注解。这三种注解默认都是没有启用的,需要通过@EnableGlobalMethodSecurity来进行启用。
另外,Spring Security中定义了四个支持使用表达式的注解,分别是@PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter。其中前两者可以用来在方法调用前或者调用后进行权限检查,后两者可以用来对集合类型的参数或者返回值进行过滤。
示例代码如下:

/**
 * @author zhangshen
 * @date 2023/12/29 17:16
 * @slogan 编码即学习,注释断语义
 **/
@RestController
public class TestController {

    @PreAuthorize("hasRole('ROLE_order')")
    @GetMapping("/test/get")
    public String test() {
        return "获取一个test资源";
    }

    @Secured("ROLE_admin")
    @GetMapping("/sys/get")
    public String sysTest() {
        return "获取一个sys资源";
    }

    @Secured("ROLE_order")
    @GetMapping("/order/get")
    public String orderTest() {
        return "获取一个order资源";
    }
}

三、Spring Security整合JWT实现自定义登录认证

3.1 自定义登录认证业务流程

一个简单易用的登录认证授权,可以使用Spring Security + JWT实现。在这个框架下,认证授权流程通常如下:

  1. 用户调用登录接口获取token
  2. 服务端收到登录请求,校验账号密码
  3. 校验通过,服务端使用JWT生成token,并返回给用户
  4. 往后前端每次请求都带上token(在请求头上添加Authorization: Bearer Token)
  5. 服务端每次收到请求都校验token的合法性
  6. 校验通过,进行业务逻辑,返回业务结果

大概的UML活动图如下:
在这里插入图片描述

3.2 JWT介绍

官方传送门:《JWT介绍》

3.2.1 什么是JWT

JWT即JSON Web Token,它是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的【协议格式】,用于在通信双方【传递json对象】,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。

关键词:【协议格式】、【JSON对象】

它具有如下优点:
JWT令牌的优点:

  1. jwt基于json,非常方便解析
  2. 可以在令牌中自定义丰富的内容,易扩展
  3. 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高
  4. 源服务使用JWT可不依赖授权服务即可完成授权

缺点:

  1. JWT令牌较长,占存储空间比较大。
  2. 安全性取决于密钥管理。JWT 的安全性取决于密钥的管理。如果密钥被泄露或者被不当管理,那么 JWT 将会受到攻击。因此,在使用 JWT 时,一定要注意密钥的管理,包括生成、存储、更新、分发等等
  3. 无法撤销。由于 JWT 是无状态的,一旦 JWT 被签发,就无法撤销。如果用户在使用 JWT 认证期间被注销或禁用,那么服务端就无法阻止该用户继续使用之前签发的 JWT。因此,开发人员需要设计额外的机制来撤销 JWT,例如使用黑名单或者设置短期有效期等等。

使用 JWT 主要用来做下面两点:

  • 认证(Authorization):这是使用 JWT 最常见的一种情况,一旦用户登录,后面每个请求都会包含 JWT,从而允许用户访问该令牌所允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小。
  • 信息交换(Information Exchange):JWT 是能够安全传输信息的一种方式。通过使用公钥/私钥对 JWT 进行签名认证。此外,由于签名是使用 head 和 payload 计算的,因此你还可以验证内容是否遭到篡改。

3.3 JWT结构

一个JWT实际上就是一个由.分隔成三部分的字符串(有点拗口)。这三部分其实就是:头部(header)载荷(payload)签名(signature)
简洁形式的JWT字符串格式就像这样:xxxxxx.yyyyyy.zzzzzz。具体如下图所示:
在这里插入图片描述

3.3.1 JWT头部header

头部用于描述关于该JWT的最基本的信息,通常包含两部分:

  • 类型:在这里即JWT
  • 签名所用的算法:(如HMACSHA256或RSA)等。

例如:

{
  "alg": "HS256",
  "typ": "JWT"
}

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

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
3.3.2 JWT载荷payload

第二部分是载荷,就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:

  • 标准中注册的声明。这些是一组预定义的声明,它们不是强制性的,但推荐使用,以提供一组有用的、可互操作的声明。其中包括:iss(发行者)、exp(过期时间)、sub(主题)、aud(受众)等
  • 公共的声明。公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密
  • 私有的声明。私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息

一个简单的示例:

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

然后将其进行base64加密,得到JWT的第二部分:

ewogICJzdWIiOiAiMTIzNDU2Nzg5MCIsCiAgIm5hbWUiOiAiSm9obiBEb2UiLAogICJhZG1pbiI6IHRydWUKfQ==
3.3.3 JWT签名signature

JWT的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret(盐,一定要保密)

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了JWT的第三部分:

// base64Header + base64Payload 
String encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

// 以zhangshen作【盐】值,对上面的字符串做HS256加密
String signature = HMACSHA256(encodedString, 'zhangshen'); // khA7TNYc7_0iELcDyTc7gHBZ_xfIcgbfpzUNWwQtzME
3.3.4 组合在一起

将上面三部生成的三部分组成在一起,就构成了一个完整的JWT了:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.ewogICJzdWIiOiAiMTIzNDU2Nzg5MCIsCiAgIm5hbWUiOiAiSm9obiBEb2UiLAogICJhZG1pbiI6IHRydWUKfQ==.80398fd80672f162495e294c86debfaa9fac06788aa49810c7883451311d9b6d

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

3.3.5 如何使用

一般是在请求头里加入Authorization,并加上Bearer标注:

fetch('api/user/1', {
  headers: {
    'Authorization': 'Bearer ' + token
  }
})

3.4 代码实现自定义登录

现在我们来改造实现一下,自定义的JWT登录。需要以下4步:

步骤一:新增JWT工具类

package com.shen.jwt;

import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.signers.JWTSignerUtil;

import java.util.Map;

/**
 * @author zhanghuitong
 * @date 2024/1/1 18:13
 * @slogan 编码即学习,注释断语义
 **/
public class JwtUtil {
    private static final String SALT = "zhangshen";
    public static final Integer EXPIRE = 30;

    /**
     * 获取默认过期时间的jwt签名器
     */
    public static JWT getJwt() {
        return getJwt(EXPIRE);
    }

    /**
     * 获取自定义过期时间的jwt签名器
     */
    public static JWT getJwt(Integer expire) {
        return getJWT().setExpiresAt(DateUtil.offsetMinute(DateUtil.date(), expire));
    }

    /**
     * 生成jwt签名器
     *
     * @return
     */
    private static JWT getJWT() {
        return JWT.create().setSigner(JWTSignerUtil.hs256(SALT.getBytes()));
    }

    /**
     * 根据token生成jwt
     *
     * @param token
     * @return
     */
    public static JWT parse(String token) {
        return getJWT().parse(token);
    }

    /**
     * 解析token
     */
    public static JSONObject parseToken(String token) {
        return parse(token).getPayloads();
    }

 /**
     * 生成token
     *
     * @param claims payload声明
     */
    public static String token(Map<String, Object> claims) {
        return getJwt().addPayloads(claims).sign();
    }
}

上面比较重要的方法是,生成JWT以及token的方法

2)实现校验JWT token的过滤器

package com.shen.jwt;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
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;
import java.util.Date;

/**
 * @author zhanghuitong
 * @date 2024/1/1 17:58
 * @slogan 编码即学习,注释断语义
 **/
@Component
public class JwtAuthticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        // 1. 从请求头获取token
        String token = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (StrUtil.isEmpty(token)) {
            filterChain.doFilter(request, response);
            return;
        }

        // 2. 校验token
        String realToken = token.substring("bearer".length());
        JSONObject tokenObject = JwtUtil.parseToken(realToken);
        Date expireIn = tokenObject.getDate("expireIn");
        if (expireIn.before(new Date())) {

            // token 已经过期
            SecurityContextHolder.getContext().setAuthentication(null);
            filterChain.doFilter(request, response);
            return;
        }

        String username = tokenObject.getStr("username");
        Authentication authentication1 = SecurityContextHolder.getContext().getAuthentication();
        if (StrUtil.isNotEmpty(username) && authentication1 == null) {

            // 获取用户信息
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (userDetails != null && userDetails.isEnabled()) {
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                // 设置用户登录状态
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        filterChain.doFilter(request, response);
    }
}

3)添加自定义JWT过滤器,并添加到账号密码校验过滤器之前

  // 添加JWT过滤器,登录的时候校验token
        JwtAuthticationTokenFilter jwtAuthticationTokenFilter = SpringUtil.getBean(JwtAuthticationTokenFilter.class);
        http.addFilterBefore(jwtAuthticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

4)最后我们来测试一下
在这里插入图片描述

3.5 JWT续期问题

JWT(JSON Web Token)通常是在用户登录后签发的,用于验证用户身份和授权。JWT 的有效期限(或称“过期时间”)通常是一段时间(例如1小时),过期后用户需要重新登录以获取新的JWT。然而,在某些情况下,用户可能会在JWT到期之前使用应用程序,这可能会导致应用程序不可用或需要用户重新登录。为了避免这种情况,通常有两种解决方案来处理JWT续期问题:

3.5.1 刷新令牌(Refresh Token)

刷新令牌是一种机制,它允许应用程序获取一个新的JWT,而无需用户进行身份验证。当JWT过期时,应用程序使用刷新令牌向身份验证服务器请求一个新的JWT,而无需提示用户输入其凭据。这样,用户可以继续使用应用程序,而不必重新登录。
以下是一个Java伪代码,演示如何使用Refresh Token来更新JWT

public String refreshAccessToken(String refreshToken) {
 
    // 刷新token校验。检查签名,过期时间等
    boolean isValid = validateRefreshToken(refreshToken);
 
    if (isValid) {
        // 检索与刷新令牌关联的用户信息(例如用户ID)
        String userId = getUserIdFromRefreshToken(refreshToken);
 
        // 重新生成一个新的token
        String newToken= generateToken(userId);
 
        return newAccessToken;
    } else {
        throw new RuntimeException("Invalid refresh token.");
    }
}

在这个示例中,refreshAccessToken方法接收一个刷新令牌作为参数,并使用validateRefreshToken方法验证该令牌是否有效。如果令牌有效,方法将使用getUserIdFromRefreshToken方法获取与令牌关联的用户信息,然后使用generateToken方法生成一个新的JWT访问令牌,并将其返回。如果令牌无效,则抛出异常。

3.5.2 自动延长JWT有效期

在某些情况下,JWT可以自动延长其有效期。例如,当用户在JWT过期前继续使用应用程序时,应用重新设置token过期时间。
要自动延长JWT有效期,您可以在每次请求时检查JWT的过期时间,并在必要时更新JWT的过期时间。以下是一个示例Java代码,演示如何自动延长JWT有效期:

public String getAccessToken(HttpServletRequest request) {
 
    String accessToken = extractAccessTokenFromRequest(request);
 
    if (isAccessTokenExpired(accessToken)) {
        String userId = extractUserIdFromAccessToken(accessToken);
        accessToken = generateNewAccessToken(userId);
    } else if (shouldRefreshAccessToken(accessToken)) {
        String userId = extractUserIdFromAccessToken(accessToken);
        accessToken = generateNewAccessToken(userId);
    }
 
    return accessToken;
}
 
private boolean isAccessTokenExpired(String accessToken) {
    // extract expiration time from the access token
    Date expirationTime = extractExpirationTimeFromAccessToken(accessToken);
 
    // check if the expiration time is in the past
    return expirationTime.before(new Date());
}
 
private boolean shouldRefreshAccessToken(String accessToken) {
    // extract expiration time and current time
    Date expirationTime = extractExpirationTimeFromAccessToken(accessToken);
    Date currentTime = new Date();
 
    // calculate the remaining time until expiration
    long remainingTime = expirationTime.getTime() - currentTime.getTime();
 
    // refresh the token if it expires within the next 5 minutes
    return remainingTime < 5 * 60 * 1000;
}
 
private String generateNewAccessToken(String userId) {
    // generate a new access token with a new expiration time
    Date expirationTime = new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRATION_TIME);
    String accessToken = generateAccessToken(userId, expirationTime);
 
    return accessToken;
}

在这个示例中,getAccessToken方法接收HttpServletRequest对象作为参数,并使用extractAccessTokenFromRequest方法从请求中提取JWT访问令牌。然后,它使用isAccessTokenExpired方法检查JWT的过期时间是否已过期。如果过期,它使用extractUserIdFromAccessToken方法从JWT中提取用户ID,并使用generateNewAccessToken方法生成一个新的JWT访问令牌。如果JWT尚未过期,但即将到期,则使用shouldRefreshAccessToken方法检查JWT是否需要更新。如果是这样,它使用相同的流程生成一个新的JWT访问令牌。

学习总结

  1. 学习并且了解了什么是JWT
  2. 学会了JWT基本使用及常见问题的解决思路

感谢

感谢官方文章《JWT介绍》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/284755.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Mysql 高级语句

目录 高阶查询select语句&#xff1a; 显示表格中一个或数个字段的所有数据记录&#xff1a; 不显示重复的数据记录&#xff1a;distinct and且&#xff0c;or或 显示已知的值的数据记录&#xff1a;in 显示两个值范围内的数据记录&#xff1a;between 通配符&#xff1…

基于rk3568 Android H265推流SRS低延迟网页播放方案

在音视频领域&#xff0c;融合推流&#xff0c;低码流&#xff0c;低延迟&#xff0c;浏览器H5化是一个降低成本&#xff0c;提升用户体验的重要手段。同时适配现有直播的生态也是一个必要条件。 在满足上述要求的情况下&#xff0c;我做了以下实践&#xff0c;取得了良好的效果…

【ROS2】MOMO的鱼香ROS2(四)ROS2入门篇——ROS2节点通信之话题与服务

ROS2节点通信之话题与服务点 引言1 理解从通信开始1.1 TCP&#xff08;传输控制协议&#xff09;1.2 UDP&#xff08;用户数据报协议&#xff09;1.3 基于共享内存的IPC方式 2 ROS2话题2.1 ROS2话题指令2.2 话题之RCLPY实现2.2.1 编写发布者2.2 2 编写订阅者2.2.3 运行测试 3 R…

总结:回顾我的2023年

文章目录 心得体会接触博客接触竞赛接触自学接触环境一些收获 2024年的目标 2023年已经结束了&#xff0c;那么特此在这里记录这一年的心得体会&#xff0c;同时也给明年定下目标吧 心得体会 接触博客 本人于2022年9月开始的自己的大学生活&#xff0c;是一个很普通的双非院…

c++_08_操作符重载(操作符重定义) 友元

1 操作符标记 单目操作符&#xff1a; - -- * -> 等 双目操作符&#xff1a; - > < - << >> 等 三木操作符&#xff1a; ? : 2 操作符函数 2.0 前言 C编译器有能力把一个由操作…

Origin绘制频数分布直方图+曲线拟合分布

问题描述 有组数据大概分布如下&#xff0c;现在想在Origin中绘制出以下效果 流程 如果我们想要人为每个柱子的边界&#xff0c;以方便展示&#xff0c;需要新建一列&#xff0c;输入数据分布的大概区间。 需要注意的是&#xff0c;C(Y)列中删除数据时若留下的“-”符合存…

鸿蒙开发第1篇__网络请求

先访问 OpenAtom OpenHarmony &#xff0c; 浏览 Http数据请求&#xff0c;

CSS 缩减顶部动画

<template><!-- mouseenter"startAnimation" 表示在鼠标进入元素时触发 startAnimation 方法。mouseleave"stopAnimation" 表示在鼠标离开元素时触发 stopAnimation 方法。 --><!-- 容器元素 --><div class"container" mou…

Linux:apache优化(7)—— 日志分割|日志合并

作用&#xff1a;随着网站访问量的增加&#xff0c;访问日志中的信息会越来越多&#xff0c; Apache 默认访问日志access_log单个文件会越来越大&#xff0c;日志文件体积越大&#xff0c;信息都在一个文件中&#xff0c;查看及分析信息会及不方便。 分割 实现方式&#xff1a…

【java爬虫】使用element-plus进行个股详细数据分页展示

前言 前面的文章我们讲述了获取详细个股数据的方法&#xff0c;并且使用echarts对个股的价格走势图进行了展示&#xff0c;本文将编写一个页面&#xff0c;对个股详细数据进行展示。别问涉及到了element-plus中分页的写法&#xff0c;对于这部分知识将会做重点讲解。 首先看一…

【SpringBoot开发】之商城项目案例(实现登陆版)

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是君易--鑨&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的博客专栏《SpringBoot开发之商城项目系列》。&#x1f3af…

【NLP论文】02 TF-IDF 关键词权值计算

之前写了一篇关于关键词词库构建的文章&#xff0c;没想到反响还不错&#xff0c;最近有空把接下来的两篇补完&#xff0c;也继续使用物流关键词词库举例&#xff0c;本篇文章承接关键词词库构建并以其为基础&#xff0c;将计算各关键词的 TF-IDF 权值&#xff0c;TF-IDF 权值主…

霹雳吧啦Wz《pytorch图像分类》-p2AlexNet网络

《pytorch图像分类》p2AlexNet网络基础及代码 一、零碎知识点1.过拟合2.使用dropout后的正向传播3.正则化regularization4.代码中所用的知识点 二、总体架构分析1.ReLU激活函数2.手算3.模型代码 三、训练花分类课程代码1.model.py2.train.py3.predict.py 一、零碎知识点 1.过拟…

FPGA项目(14)——基于FPGA的数字秒表设计

1.功能设计 设计内容及要求: 1.秒表最大计时范围为99分59. 99秒 2.6位数码管显示&#xff0c;分辨率为0.01秒 3.具有清零、启动计时、暂停及继续计时等功能 4.控制操作按键不超过二个。 2.设计思路 所采用的时钟为50M&#xff0c;先对时钟进行分频&#xff0c;得到100HZ频率…

【Maven】下载配置maven以及IDEA配置maven详情

目录 1、下载maven 2、配置settings.xml 2.1、配置本地仓库 2.2、配置阿里云镜像仓库 2.3、配置JDK 3、配置环境变量 4、IDEA配置maven 1、下载maven maven官网&#xff1a;https://maven.apache.org/ 2、配置settings.xml 2.1、配置本地仓库 <localRepository>C:\…

oracle 9i10g编程艺术-读书笔记1

根据书中提供的下载代码链接地址&#xff0c;从github上找到源代码下载地址。 https://github.com/apress下载好代码后&#xff0c;开始一段新的旅行。 设置 SQL*Plus 的 AUTOTRACE 设置 SQL*Plus 的 AUTOTRACE AUTOTRACE 是 SQL*Plus 中一个工具&#xff0c;可以显示所执行…

GPT4-AIl本地部署-chat AI本地使用

文章目录 GPT4-AIl本地部署GPT4客户端下载地址&#xff1a;对应的下载下载后的文件点击安装&#xff0c;改一下文件存放路径&#xff0c;下面都是默认下一步进度条100%后&#xff0c;点击完成 安装完桌面生成图标&#xff0c;点击选择都是NO&#xff0c;不进行数据上传点击后&a…

Python编程新技能:如何优雅地实现水仙花数?

水仙花数&#xff08;Narcissistic number&#xff09;也被称为阿姆斯特朗数&#xff08;Armstrong number&#xff09;或自恋数等&#xff0c;它是一个非负整数&#xff0c;其特性是该数的每个位上的数字的n次幂之和等于它本身&#xff0c;其中n是该数的位数。简单来说&#x…

一起学Elasticsearch系列-写入原理

本文已收录至Github&#xff0c;推荐阅读 &#x1f449; Java随想录 微信公众号&#xff1a;Java随想录 文章目录 写入过程写操作写流程写一致性策略 写入原理RefreshMergeFlushTranslog图解写入流程 ES作为一款开源的分布式搜索和分析引擎&#xff0c;以其卓越的性能和灵活的扩…

29 UVM Command Line Processor (CLP)

随着设计和验证环境的复杂性增加&#xff0c;编译时间也增加了&#xff0c;这也影响了验证时间。因此&#xff0c;需要对其进行优化&#xff0c;以便在不强制重新编译的情况下考虑新的配置或参数。我们已经看到了function or task如何基于传递参数进行行为。类似地&#xff0c;…
最新文章