首页 > 解决方案 > JWT 令牌过滤器不适用于基于移动 OTP 的登录的 spring 安全性

问题描述

我正在尝试在 Spring Boot 应用程序中实现基于 SMS otp 的登录。我没有使用用户名和密码。我能够生成 JWT,但是在标头中包含 JWT 的后续请求不允许我访问具有以下错误的资源。

 {
        "timestamp": "2020-09-08T00:32:58.576+00:00",
        "status": 403,
        "error": "Forbidden",
        "message": "Access Denied",
        "path": "/hello"
}

我的用户类如下。
用户类

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

import lombok.Data;

@Entity
@Data
public class User{
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String userName;
    private String phoneNumber;
}

我正在使用以下 jwt 依赖项

<dependency>
          <groupId>io.jsonwebtoken</groupId>
          <artifactId>jjwt</artifactId>
          <version>0.5.1</version>
</dependency>

这是我的 JWT 令牌过滤器。

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import com.java.nikitchem.dao.UserDao;
import com.java.nikitchem.exception.ResourceNotFoundException;
import com.java.nikitchem.model.User;
import com.java.nikitchem.serviceImpl.TokenProvider;

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private TokenProvider tokenProvider;
    
    @Autowired
    private UserDao userDao;

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

        final String authHeader = request.getHeader("Authorization");
        String phoneNumber = null;
        String jwt = null;
        
        if(authHeader != null && authHeader.startsWith("Bearer ")) {
            jwt = authHeader.substring(7);
            phoneNumber = tokenProvider.getUserIdFromToken(jwt);
        }
        
        if(phoneNumber!= null && SecurityContextHolder.getContext().getAuthentication() == null) {
            User user = null;
            try {
                user = userDao.getUserByPhone(phoneNumber);
                if(tokenProvider.validateToken(jwt, user)) {
                    UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(user, null);
                    authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authToken);
                }
            } catch (ResourceNotFoundException e) {
                // TODO Auto-generated catch block
                e.getMessage();
            }
            
            
            
        }
        filterChain.doFilter(request,response);
    }
}

下面是我的 TokenProvider.class

TokenProvider.class

public class TokenProvider {

    private static final Logger logger = LoggerFactory.getLogger(TokenProvider.class);

    private AuthConfig authConfig;

    public TokenProvider(AuthConfig authConfig) {
        this.authConfig = authConfig;
    }

    public String createTokenForUser(User user) {
        Map<String, Object> claims = new HashMap<>();
        return generateToken(claims, user.getPhoneNumber());

    }

    private String generateToken(Map<String, Object> claims, String phoneNumber) {
        return Jwts.builder().setClaims(claims).setSubject(phoneNumber).setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 60 * 438)))
                .signWith(SignatureAlgorithm.HS256, authConfig.getTOKEN_SECRET()).compact();
    }
    
    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }
    
    private Claims extractAllClaims(String token) {
        return Jwts.parser().setSigningKey(authConfig.getTOKEN_SECRET()).parseClaimsJws(token).getBody();
    }

    public String getUserIdFromToken(String token) {
        Claims claims = Jwts.parser().setSigningKey(authConfig.getTOKEN_SECRET()).parseClaimsJws(token).getBody();

        return claims.getSubject();
    }
    
    public Date extractExpiration(String token) {
        return extractClaim(token,Claims::getExpiration);
    }
    
    public String extractUserId(String token) {
        return extractClaim(token, Claims::getSubject);
    }
    
    public <T> T extractClaim(String token, Function<Claims, T> claimResolver) {
        final Claims claims = extractAllClaims(token);
        return claimResolver.apply(claims);
    }
    
    public boolean validateToken(String authToken, User user) {
        try {
            Jwts.parser().setSigningKey(authConfig.getTOKEN_SECRET()).parseClaimsJws(authToken);
            final String phoneNumber = getUserIdFromToken(authToken);
            return (!isTokenExpired(authToken) && phoneNumber.equals(user.getPhoneNumber()));
        } catch (SignatureException ex) {
            logger.error("Invalid JWT signature");
        } catch (MalformedJwtException ex) {
            logger.error("Invalid JWT token");
        } catch (ExpiredJwtException ex) {
            logger.error("Expired JWT token");
        } catch (UnsupportedJwtException ex) {
            logger.error("Unsupported JWT token");
        } catch (IllegalArgumentException ex) {
            logger.error("JWT claims string is empty.");
        }
        return false;
    }
}

安全配置类

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.cors().and().csrf().disable().authorizeRequests().antMatchers("/auth", "/authenticate").permitAll()
                .anyRequest().authenticated()
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // Add our custom Token based authentication filter
        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }
    
}

标签: javaspring-bootspring-securityjwtone-time-password

解决方案


JwtRequestFilter.class中,

if (phoneNumber != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(phoneNumber);
            if (tokenProvider.validateToken(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails,
                        null, userDetails.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);

            }
            
        }
        filterChain.doFilter(request, response);

我试图将 User 对象传递给 UsernamePasswordAuthenticationToken,这破坏了 Spring Security FilterChain 的执行,给出了 403 错误。

将 User 对象替换为 UserDetails.User 对象后,使用空字符串作为密码,即

new org.springframework.security.core.userdetails.User(user.getPhoneNumber(),"", new ArrayList<>());

(因为我没有使用User.class中的密码)并传递给 UsernamePasswordAuthenticationToken 以创建 AuthenticationToken。

那工作得很好。感谢 code_mechanic的帮助 :)


推荐阅读