java - 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);
}
}
解决方案
在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的帮助 :)
推荐阅读
- pygame - Pygame - 创建射弹的问题,“* 之后的 add() 参数必须是可迭代的,而不是 int”
- laravel - 带有变量名称的标记刀片组件
- html - 在组件的 html 中使用 id 属性可以吗?
- xamarin - 无法从程序集 [应用中心] 加载“Xamarin.Build.Download.XamarinBuildCastAssemblyResources”任务
- react-native - 退出世博会再回来
- css - 如何删除我添加到按钮的图像后面的背景
- javascript - 从没有 list-unsubscribe 标头的电子邮件正文中检测退订
- php - Laravel 6用不同的where子句查询同一张表
- apache-spark - 如何在运行 Spark 时获取暂存目录?
- sql - 比较 SQL 中的两个关系