Spring Security中用JWT退出登录时遇到的坑

Spring Security是一个非常流行的安全框架,用于在Spring应用程序中实现身份验证和授权。JWT是一种用于在不同的系统之间安全传输信息的方式。在使用Spring Security和JWT时,退出登录是常见的操作之一,但处理起来可能会遇到一些问题。下面我会详细讲解在Spring Security中使用JWT退出登录时可能遇到的坑,包括原因和解决方案。

问题

当使用Spring Security和JWT时,退出登录是比较简单的,只需从请求头中获取JWT令牌,并将其放入黑名单中即可。这样,当用户下次再尝试用这个令牌进行访问时,就会发现其已经过期了,从而被拒绝访问。

但实际上,一些Spring Security配置可能会导致问题。具体地说,这些配置将JWT令牌的校验交给了Spring Security,而不是自己进行校验。这时候,一些令牌校验可能会被忽略,从而导致用户无法退出登录。具体可能的原因和解决方案如下。

问题一:无法退出登录

在一些情况下,由于Spring Security对JWT令牌的处理,我们可能无法正常退出登录。例如,下列示例中,我们使用Spring Boot 2.1.1版本的starter-security和starter-jwt依赖,并把JWT令牌放入请求头中:

@RestController
@RequestMapping("/api/sample")
public class SampleController {

    @PostMapping("/login")
    public ResponseEntity<JwtAuthenticationResponseDto> login(@RequestBody JwtAuthenticationRequestDto requestDto) {
        // Authenticate user and return JWT token
    }

    @PostMapping("/logout")
    public ResponseEntity<Void> logout(HttpServletRequest request, HttpServletResponse response) {
        // Get JWT token from request header
        String token = getToken(request);

        // Invalidate JWT token
        tokenService.invalidate(token);

        return ResponseEntity.ok(null);
    }

    private String getToken(HttpServletRequest request) {
        final String requestHeader = request.getHeader("Authorization");
        if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
            return requestHeader.substring(7);
        }
        return null;
    }
}

当我们尝试调用POST /api/sample/logout接口时,我们会发现之后再使用同样的JWT令牌进行访问时,依然可以正常访问。这是因为,虽然我们已经将JWT令牌放入了黑名单中,但Spring Security会在黑名单中进行校验时忽略它。

问题二:过度校验

在一些情况下,Spring Security可能会对JWT令牌进行过度校验,从而导致无法退出登录。例如,下列示例中,我们使用了一个过度复杂的JWT解析器来进行令牌校验:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/api/sample/login").permitAll()
                .anyRequest().authenticated();
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        Resource resource = new ClassPathResource("public.cert");
        String publicKey;
        try {
            publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        converter.setVerifierKey(publicKey);
        return converter;
    }
}

虽然这个解析器的作用是验证token是否合法,但在退出登录时,我们需要把token放到黑名单中,而不是对token进行额外的校验。

解决方案

解决上述问题的主要方法是修改Spring Security的配置,以便在退出登录时正确地处理JWT令牌。

解决方案一:添加过滤器

在Spring Security中,可以通过添加一个过滤器来关闭原生的JWT令牌校验,从而避免退出登录时的问题。示例代码如下:

@Component
public class JwtTokenFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // Get token from request header
        String token = getToken(request);

        if (JwtTokenUtil.isRefreshToken(token)) {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return;
        }

        // Check if token exists in a blacklist
        if (tokenService.isTokenBlacklisted(token)) {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return;
        }

        filterChain.doFilter(request, response);
    }

    private String getToken(HttpServletRequest request) {
        final String requestHeader = request.getHeader("Authorization");
        if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
            return requestHeader.substring(7);
        }
        return null;
    }

    @Autowired
    private TokenService tokenService;
}

这里使用了一个自定义的JwtTokenFilter过滤器,其中doFilterInternal方法首先会获取请求头中的JWT令牌,然后校验它是否存在于黑名单中。如果不存在,则通过该请求,否则返回401未授权错误。

自Spring Boot 2.3版本开始,可以通过jwt解析器中设置自定义验签算法,例如:

converter.setVerifier(JWT.require(Algorithm.HMAC256("my-secret"))
      .withIssuer("auth0")
      .build());

这样就可以根据需求设置验签算法。

解决方案二:更改JwtAccessTokenConverter

另一种解决方法是更改JwtAccessTokenConverter,使其在退出登录时不再进行过度校验。示例代码如下:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/api/sample/login").permitAll()
                .anyRequest().authenticated();
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        Resource resource = new ClassPathResource("public.cert");
        String publicKey;
        try {
            publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        converter.setVerifier(new CustomJwtAccessTokenConverter(publicKey));
        return converter;
    }

    public static class CustomJwtAccessTokenConverter extends DefaultAccessTokenConverter {

        private final JwtVerifier verifier;

        public CustomJwtAccessTokenConverter(String publicKey) {
            this.verifier = JWT.require(Algorithm.HMAC256(publicKey))
                    .withIssuer("my-application")
                    .build();
        }

        @Override
        public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
            try {
                // Parse JWT token
                DecodedJWT jwt = verifier.verify(map.get("access_token").toString());

                // Extract authentication data from token
                AuthenticationCustomData data = new AuthenticationCustomData();
                data.setUserId(jwt.getClaim("userId").asString());
                data.setAuthorities((List<String>) map.get("authorities"));

                UsernamePasswordAuthenticationToken userAuthentication = new UsernamePasswordAuthenticationToken(
                        jwt.getSubject(),
                        null,
                        AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", data.getAuthorities())));

                return new OAuth2Authentication(
                        new OAuth2Request(null, "my-application", userAuthentication.getAuthorities(), true, null, null, null, data.getScopes(), null),
                        userAuthentication);
            } catch (JWTVerificationException e) {
                throw new InvalidTokenException(e.getMessage());
            }
        }
    }
}

在这个解决方案中,我们自定义了CustomJwtAccessTokenConverter类,并重写了其中的extractAuthentication方法,使其只解密token,而不进行过度校验。在上述示例中,我们使用了一个自定义的AuthenticationCustomData类,它包含了用户ID、授权信息等信息。我们还在JwtAccessTokenConverter中添加了公钥,并使用它来验证JWT令牌的签名。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Spring Security中用JWT退出登录时遇到的坑 - Python技术站

(0)
上一篇 2023年5月20日
下一篇 2023年5月20日

相关文章

  • JavaWeb Servlet中Filter过滤器的详解

    JavaWeb Servlet中Filter过滤器的详解 一、什么是Filter过滤器 Filter,即过滤器,是Servlet规范中一个重要的组件,用于对Servlet容器的请求和响应进行过滤和修改。它能够拦截所有的用户请求,对请求参数、头部信息等进行检查,还可以修改请求的目标和响应内容。 二、Filter过滤器的应用场景 在JavaWeb开发中,通常会出…

    Java 2023年6月15日
    00
  • jmeter添加自定函数的实例(jmeter5.3+IntelliJ IDEA)

    下面我将为你讲解“jmeter添加自定函数的实例(jmeter5.3+IntelliJ IDEA)”的攻略: 1. 准备工作 1.1 下载安装Java Development Kit(JDK) 在JMeter中开发自定义Java代码,需要安装好JDK,并设置好JAVA_HOME环境变量。下载地址:https://www.oracle.com/java/tec…

    Java 2023年5月19日
    00
  • Java 读取外部资源的方法详解及实例代码

    Java 读取外部资源的方法详解及实例代码 在Java中,可以通过多种方式读取外部资源,比如文件、网络数据等。本篇攻略将介绍Java中常用的读取外部资源的方法及实例代码。 读取本地文件 1. 使用 FileInputStream FileInputStream 是一个用来打开文件以进行读取操作的类。下面是使用 FileInputStream 读取本地文件的方…

    Java 2023年5月19日
    00
  • 简洁实用的Java Base64编码加密异常处理类代码

    我们来讲解一下“简洁实用的Java Base64编码加密异常处理类代码”的完整攻略。 什么是Base64编码加密? Base64编码是一种将二进制数据转换成文本数据的方法,它可以用来将数据在网络上进行传输。Base64编码是一种简单、可逆的编码方式,目前广泛应用于各种网络协议和文件格式。在Java中可以使用Base64编码对二进制数据进行加密。 Java中的…

    Java 2023年5月20日
    00
  • Java中字节流和字符流的理解(超精简!)

    了解Java中字节流和字符流的区别和使用场景,是Java IO编程的基础。下面我们来详细讲解一下这个问题。 1. 什么是Java中的字节流和字符流? Java IO流分为字节流和字符流两种类型,它们的差别在于输入输出流所处理的数据单元不同:字节流以字节(8 bit)为单位,而字符流以字符为单位(Java中一个字符占2个字节)。 2. Java中字节流 字节流…

    Java 2023年5月27日
    00
  • Java+MySQL实现学生信息管理系统源码

    Java+MySQL实现学生信息管理系统 本文将介绍如何使用Java和MySQL数据库实现一个简单的学生信息管理系统,并提供完整的源码和演示。 准备工作 为了使用Java和MySQL实现学生信息管理系统,需要先进行以下准备工作: 安装Java开发环境(JDK) 安装MySQL数据库 安装Java连接MySQL的驱动程序(JDBC驱动) 在这里以Windows…

    Java 2023年5月19日
    00
  • 基于Java SpringBoot的前后端分离信息管理系统的设计和实现

    基于Java SpringBoot的前后端分离信息管理系统的设计和实现 背景介绍 基于Java SpringBoot的前后端分离信息管理系统是一种常见的Web应用开发模式,它能够通过前后端分离的方式,实现代码的解耦,提高应用的开发效率和可维护性。本文将详细介绍如何设计和实现一个基于Java SpringBoot的前后端分离信息管理系统。 系统设计 本系统采用…

    Java 2023年5月19日
    00
  • Java后端长时间无操作自动退出的实现方式

    实现Java后端长时间无操作自动退出,主要需要使用Java的定时器和线程等相关技术。 以下是实现Java后端长时间无操作自动退出的完整攻略: 第一步:设置最大空闲时间和定时器 首先,我们需要设置一个最大空闲时间,当用户最后一次请求后,超过了该时间,就会被认为是无操作状态。例如,我们设置最大空闲时间为10分钟。 接下来,我们需要使用Java的定时器,定时器会在…

    Java 2023年5月20日
    00
合作推广
合作推广
分享本页
返回顶部