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日

相关文章

  • IDEA2022创建Maven Web项目教程(图文)

    让我为您详细讲解“IDEA2022创建Maven Web项目教程(图文)”的完整攻略: 1. 准备工作 在开始创建 Maven Web 项目前,您需要先准备好以下环境与工具: IntelliJ IDEA 2022 JDK 8或以上版本 Maven 3.6.0 或以上版本 2. 创建 Maven Web 项目 启动 IntelliJ IDEA,并选择菜单栏中的…

    Java 2023年5月19日
    00
  • Java实现纪元秒和本地日期时间互换的方法【经典实例】

    Java实现纪元秒和本地日期时间互换的方法【经典实例】 什么是纪元秒? 纪元秒是指从“1970年1月1日 00:00:00 UTC”开始计算至某一时刻之间的秒数。 纪元秒与本地日期时间的相互转换 Java提供了从纪元秒到本地日期时间和从本地日期时间到纪元秒的转换方法。这些方法都属于Java API中的java.time包。 从纪元秒到本地日期时间 Java中…

    Java 2023年5月20日
    00
  • Java常用工具类汇总 附示例代码

    Java常用工具类汇总 附示例代码 在Java编程中,我们常常要使用一些工具类来方便我们进行开发。本文将会汇总一些Java常用的工具类,旨在提供一个全面的工具类汇总供大家参考。我们将会介绍以下常用工具类: StringUtils:用于操作字符串的工具类。 DateUtils:用于时间和日期格式化、计算等操作的工具类。 MathUtils:用于数学计算的工具类…

    Java 2023年5月23日
    00
  • MyEclipse+Tomcat+MAVEN+SVN项目完整环境搭建(图文教程)

    完整环境搭建需要涉及到以下步骤: 安装MyEclipse 下载MyEclipse安装包 运行安装程序 按照安装程序指引完成安装过程 安装Tomcat 下载Tomcat安装包 运行安装程序 按照安装程序指引完成安装过程 安装MAVEN 下载MAVEN安装包 解压文件到一个目录 配置MAVEN的环境变量 新建一个环境变量:MAVEN_HOME,值为MAVEN所在…

    Java 2023年5月19日
    00
  • java最新版本连接mysql失败的解决过程

    下面我将详细讲解 Java 最新版本连接 MySQL 失败的解决过程的完整攻略。 问题描述 在使用 Java 最新版本连接 MySQL 数据库时,可能会遇到连接失败的问题。这个问题可能涉及到 MySQL 数据库、Java 连接、Java 依赖库等多个方面。具体的表现可能包括但不限于以下情况: 报错信息中包含“java.sql.SQLNonTransientC…

    Java 2023年5月20日
    00
  • 关于JavaScript作用域你想知道的一切

    关于JavaScript作用域你想知道的一切 什么是作用域? 在介绍作用域之前,我们先来看一下变量的定义。在JavaScript中,我们可以通过var、let或const三个关键字来声明变量。 var a = 1; // 使用var声明的变量 let b = 2; // 使用let声明的变量 const c = 3; // 使用const声明的变量 那么,作…

    Java 2023年6月16日
    00
  • @Accessors 注解参数

    @Accessors 注解参数经常会在实体类上看到,记录一下,方便以后复习 @Accessors注解的作用:当属性字段在生成 getter 和 setter 方法时,做一些相关的设置。 @Accessors 共有三个属性,分别是 fluent,chain,prefix fluent 属性 不写默认为false,当该值为 true 时,对应字段的 getter…

    Java 2023年5月9日
    00
  • HTML静态页面引入公共html文件(ssi服务器端指令详解)

    HTML静态页面引入公共HTML文件,是一种在静态页面中引用公共代码的方法,能够节省代码量,实现代码复用,提高代码效率和维护性。这种方法可以使用SSI服务器端指令实现,下面将介绍完整的流程。 SSI(Server Side Include)服务器端指令详解 SSIs指的是服务器端指令(Server Side Includes),用于在HTML页面中插入服务器…

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