JWT令牌过滤器不适用于基于移动OTP的登录的Spring Security

问题描述

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

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

我的User类在下面。
User.class

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;
    }
}

SecurityConfig.class

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);
    }
    
}

解决方法

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的帮助:)