JWT令牌过滤器在Spring安全中无法用于基于移动OTP的登录。

huangapple go评论75阅读模式
英文:

JWT token filter not working for spring security for mobile OTP based login

问题

我正在尝试在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类如下<br>
**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类

**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);
    }
}
英文:

I am trying to implement SMS otp based login in spring boot application. I am not using username and password.
I am able to generate the JWT but subsequent request containing JWT in header is not letting me access resources with below error.

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

My User class is below.<br>
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;
}

I am using below jwt dependency

&lt;dependency&gt;
&lt;groupId&gt;io.jsonwebtoken&lt;/groupId&gt;
&lt;artifactId&gt;jjwt&lt;/artifactId&gt;
&lt;version&gt;0.5.1&lt;/version&gt;
&lt;/dependency&gt;

Here is my JWT Token Filter.

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(&quot;Authorization&quot;);
String phoneNumber = null;
String jwt = null;
if(authHeader != null &amp;&amp; authHeader.startsWith(&quot;Bearer &quot;)) {
jwt = authHeader.substring(7);
phoneNumber = tokenProvider.getUserIdFromToken(jwt);
}
if(phoneNumber!= null &amp;&amp; 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);
}
}

Below is my 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&lt;String, Object&gt; claims = new HashMap&lt;&gt;();
return generateToken(claims, user.getPhoneNumber());
}
private String generateToken(Map&lt;String, Object&gt; 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 &lt;T&gt; T extractClaim(String token, Function&lt;Claims, T&gt; 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) &amp;&amp; phoneNumber.equals(user.getPhoneNumber()));
} catch (SignatureException ex) {
logger.error(&quot;Invalid JWT signature&quot;);
} catch (MalformedJwtException ex) {
logger.error(&quot;Invalid JWT token&quot;);
} catch (ExpiredJwtException ex) {
logger.error(&quot;Expired JWT token&quot;);
} catch (UnsupportedJwtException ex) {
logger.error(&quot;Unsupported JWT token&quot;);
} catch (IllegalArgumentException ex) {
logger.error(&quot;JWT claims string is empty.&quot;);
}
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(&quot;/auth&quot;, &quot;/authenticate&quot;).permitAll()
.anyRequest().authenticated()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Add our custom Token based authentication filter
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}

答案1

得分: 1

**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][1] 提供的帮助 :)

  [1]: https://stackoverflow.com/users/1712172/code-mechanic
英文:

In JwtRequestFilter.class,

if (phoneNumber != null &amp;&amp; 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);

I was trying to pass User object to UsernamePasswordAuthenticationToken, which was breaking Spring Security FilterChain execution, giving 403 error.

After replacing User object to UserDetails.User object with empty string as password i.e.

new org.springframework.security.core.userdetails.User(user.getPhoneNumber(),&quot;&quot;, new ArrayList&lt;&gt;());

(as i am not working with passwords in User.class)
and passing to UsernamePasswordAuthenticationToken for creating AuthenticationToken.

That worked perfectly fine.
Thanks to code_mechanic for your help JWT令牌过滤器在Spring安全中无法用于基于移动OTP的登录。

huangapple
  • 本文由 发表于 2020年9月8日 17:38:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/63791170.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定