用于 REST API 的带有 JWT 的 Spring Security
Posted
技术标签:
【中文标题】用于 REST API 的带有 JWT 的 Spring Security【英文标题】:Spring Security with JWT for REST API 【发布时间】:2021-11-07 02:39:05 【问题描述】:我有这门课:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ApiWebSecurityConfig extends WebSecurityConfigurerAdapter
private static final String SALT = "fd&lkj§isfs23#$1*(_)nof";
private final JwtAuthenticationEntryPoint unauthorizedHandler;
private final JwtTokenUtil jwtTokenUtil;
private final UserSecurityService userSecurityService;
@Value("$jwt.header")
private String tokenHeader;
public ApiWebSecurityConfig(JwtAuthenticationEntryPoint unauthorizedHandler, JwtTokenUtil jwtTokenUtil,
UserSecurityService userSecurityService)
this.unauthorizedHandler = unauthorizedHandler;
this.jwtTokenUtil = jwtTokenUtil;
this.userSecurityService = userSecurityService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
auth
.userDetailsService(userSecurityService)
.passwordEncoder(passwordEncoder());
@Bean
public BCryptPasswordEncoder passwordEncoder()
return new BCryptPasswordEncoder(12, new SecureRandom(SALT.getBytes()));
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
return super.authenticationManagerBean();
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
httpSecurity
// we don't need CSRF because our token is invulnerable
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// don't create session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// Un-secure H2 Database
.antMatchers("/h2-console/**/**").permitAll()
.antMatchers("/api/v1/users").permitAll()
.antMatchers("/error").permitAll()
.anyRequest().authenticated();
// Custom JWT based security filter
JwtAuthorizationTokenFilter authenticationTokenFilter = new JwtAuthorizationTokenFilter(userDetailsService(), jwtTokenUtil, tokenHeader);
httpSecurity
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// disable page caching
httpSecurity
.headers()
.frameOptions()
.sameOrigin() // required to set for H2 else H2 Console will be blank.
.cacheControl();
@Override
public void configure(WebSecurity web)
// AuthenticationTokenFilter will ignore the below paths
web
.ignoring()
.antMatchers(
HttpMethod.POST,
"/api/v1/auth"
)
.antMatchers(
HttpMethod.POST,
"/api/v1/users"
)
.antMatchers(
HttpMethod.GET,
"/api/v1/countries"
);
和
@Provider
@Slf4j
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter
private UserDetailsService userDetailsService;
private JwtTokenUtil jwtTokenUtil;
private String tokenHeader;
public JwtAuthorizationTokenFilter(UserDetailsService userDetailsService,
JwtTokenUtil jwtTokenUtil,
String tokenHeader)
this.userDetailsService = userDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
this.tokenHeader = tokenHeader;
@Override
protected boolean shouldNotFilter(HttpServletRequest request)
return new AntPathMatcher().match("/api/v1/users",
request.getServletPath());
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException,
IOException
log.info("processing authentication for ''", request.getRequestURL());
log.info("tokenHeader ''", tokenHeader);
final String requestHeader = request.getHeader(this.tokenHeader);
log.info("requestHeader ''", requestHeader);
String username = null;
String authToken = null;
if (requestHeader != null && requestHeader.startsWith("Bearer "))
authToken = requestHeader.substring(7);
log.info("authToken ''", authToken);
try
username = jwtTokenUtil.getUsernameFromToken(authToken);
catch (IllegalArgumentException e)
logger.info("an error occured during getting username from token", e);
catch (ExpiredJwtException e)
logger.info("the token is expired and not valid anymore", e);
else
logger.info("couldn't find bearer string, will ignore the header");
log.info("checking authentication for user ''", username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null)
logger.info("security context was null, so authorizating user");
// It is not compelling necessary to load the use details from the database. You could also store the information
// in the token and read it from it. It's up to you ;)
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// For simple validation it is completely sufficient to just check the token integrity. You don't have to call
// the database compellingly. Again it's up to you ;)
if (jwtTokenUtil.validateToken(authToken, userDetails))
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
log.info("authorizated user '', setting security context", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
和
@RestController
@Slf4j
public class AuthenticationRestController
private static final Logger LOG = LoggerFactory.getLogger (AuthenticationRestController.class);
@Value("$jwt.header")
private String tokenHeader;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserSecurityService userSecurityService;
@PostMapping(path = "/api/v1/auth", consumes = "application/json", produces = "application/json")
public ResponseEntity<JwtAuthenticationResponse>
createAuthenticationToken( @RequestBody JwtAuthenticationRequest authenticationRequest,
HttpServletRequest request) throws AuthenticationException
LOG.info("authenticating " , authenticationRequest.getUsername());
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
// Reload password post-security so we can generate the token
final User userDetails = (User) userSecurityService.loadUserByUsername(authenticationRequest.getUsername());
if (!userDetails.isEnabled())
throw new UserDisabledException();
if (LOG.isDebugEnabled())
LOG.debug("UserDetails userDetails [ " + authenticationRequest.getUsername() + " ]");
final String token = jwtTokenUtil.generateToken(userDetails);
// Return the token
return ResponseEntity.ok(new JwtAuthenticationResponse(token));
@GetMapping(path = "$jwt.route.authentication.refresh", consumes = "application/json", produces = "application/json")
public ResponseEntity<?> refreshAndGetAuthenticationToken(HttpServletRequest request)
String authToken = request.getHeader(tokenHeader);
final String token = authToken.substring(7);
String username = jwtTokenUtil.getUsernameFromToken(token);
JwtUser user = (JwtUser) userSecurityService.loadUserByUsername(username);
if (jwtTokenUtil.canTokenBeRefreshed(token, user.getLastPasswordResetDate()))
String refreshedToken = jwtTokenUtil.refreshToken(token);
return ResponseEntity.ok(new JwtAuthenticationResponse(refreshedToken));
else
return ResponseEntity.badRequest().body(null);
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<String> handleAuthenticationException(AuthenticationException e)
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
/**
* Authenticates the user. If something is wrong, an @link AuthenticationException will be thrown
*/
private void authenticate(String username, String password)
Objects.requireNonNull(username);
Objects.requireNonNull(password);
try
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
catch (DisabledException e)
e.printStackTrace();
throw new AuthenticationException("User is disabled!", e);
catch (BadCredentialsException e)
throw new AuthenticationException("Bad credentials!", e);
catch (Exception e)
e.printStackTrace();
和
@RestController
@RequestMapping("/api/v1/styles")
@Slf4j
public class StyleResourceController
@PutMapping(path = "/styleCode")
@ResponseStatus(HttpStatus.OK)
public void setAlerts(@RequestHeader(value = "Authorization") String authHeader, @PathVariable String styleCode)
throws DataAccessException
System.out.println("add style ");
final User user = authUserOnPath(authHeader);
System.out.println("user " + user);
protected User authUserOnPath(String authHeader)
String authToken = authHeader.substring(7);
String username = jwtTokenUtil.getUsernameFromToken(authToken);
User user = userService.findByUserName(username);
if (user == null)
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "UserNotFound");
return user;
和
@Component
public class JwtTokenUtil implements Serializable
//static final String CLAIM_KEY_USERNAME = "sub";
//static final String CLAIM_KEY_CREATED = "iat";
private static final long serialVersionUID = -3301605591108950415L;
// @SuppressFBWarnings(value = "SE_BAD_FIELD", justification = "It's okay here")
private Clock clock = DefaultClock.INSTANCE;
@Value("$jwt.secret")
private String secret;
@Value("$jwt.expiration")
private Long expiration;
public String getUsernameFromToken(String token)
return getClaimFromToken(token, Claims::getSubject);
public Date getIssuedAtDateFromToken(String token)
return getClaimFromToken(token, Claims::getIssuedAt);
public Date getExpirationDateFromToken(String token)
return getClaimFromToken(token, Claims::getExpiration);
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver)
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
private Claims getAllClaimsFromToken(String token)
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
private Boolean isTokenExpired(String token)
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(clock.now());
private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset)
return (lastPasswordReset != null && created.before(lastPasswordReset));
private Boolean ignoreTokenExpiration(String token)
// here you specify tokens, for that the expiration is ignored
return false;
public String generateToken(UserDetails userDetails)
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
private String doGenerateToken(Map<String, Object> claims, String subject)
final Date createdDate = clock.now();
final Date expirationDate = calculateExpirationDate(createdDate);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset)
final Date created = getIssuedAtDateFromToken(token);
return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
&& (!isTokenExpired(token) || ignoreTokenExpiration(token));
public String refreshToken(String token)
final Date createdDate = clock.now();
final Date expirationDate = calculateExpirationDate(createdDate);
final Claims claims = getAllClaimsFromToken(token);
claims.setIssuedAt(createdDate);
claims.setExpiration(expirationDate);
return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
public Boolean validateToken(String token, UserDetails userDetails)
JwtUser user = (JwtUser) userDetails;
final String username = getUsernameFromToken(token);
final Date created = getIssuedAtDateFromToken(token);
return (
username.equals(user.getUsername())
&& !isTokenExpired(token)
&& !isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate())
);
private Date calculateExpirationDate(Date createdDate)
return new Date(createdDate.getTime() + expiration * 1000);
但是当我从令牌中获取用户时为空:
17:19:22.017 [http-nio-1133-exec-8] INFO c.d.c.JwtAuthorizationTokenFilter - tokenHeader 'Authorization'
17:19:22.017 [http-nio-1133-exec-8] INFO c.d.c.JwtAuthorizationTokenFilter - requestHeader 'Bearer eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2OTE3NjcxMzYsImlhdCI6MTYzMTI4NzEzNn0.C9s3dbjWNVyGdV5k0LXsNhMGMvPzboTx1J6sGEbfXVOP1CzCLeZFgVPQ4o8jgugvgURF3BcnsWAk7ygd7RCvdg'
17:19:22.017 [http-nio-1133-exec-8] INFO c.d.c.JwtAuthorizationTokenFilter - authToken 'eyJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2OTE3NjcxMzYsImlhdCI6MTYzMTI4NzEzNn0.C9s3dbjWNVyGdV5k0LXsNhMGMvPzboTx1J6sGEbfXVOP1CzCLeZFgVPQ4o8jgugvgURF3BcnsWAk7ygd7RCvdg'
17:19:22.018 [http-nio-1133-exec-8] INFO c.d.c.JwtAuthorizationTokenFilter - checking authentication for user 'null'
【问题讨论】:
您的令牌不包含用户名,因此请将您的精力集中在创建部分。 在您的令牌中找不到任何与用户相关的数据。粘贴您的 JWT 令牌 here。当你创建它们时,你需要在你的令牌中添加与用户相关的数据。 如 sam 和 ray 所示,您的令牌不包含任何用户信息(例如,如果您将令牌复制并粘贴到 jet.io 中,它将在有效负载中不提供sub
值)。 create createAuthenticationToken
方法看起来和generateToken
一样好。请尝试在此行之前调试您的 ÙserDetails` 实例:final String token = jwtTokenUtil.generateToken(userDetails);
。另外,请确保您的UserDetails
正确实现了getUsername
方法,有时我们从数据库中派生它并且我们没有正确实现它。尝试调试方法...
doGenerateToken
以及查看subject
变量的值。我希望我能帮上忙。
对不起,我想说jwt.io,而不是jet.io。我的电脑翻译器。
【参考方案1】:
仔细检查您的 jwt 令牌。我认为它错过了子属性(此处的主题或用户名)。
我还强烈建议您为 JwtTokenUtil 等少数类编写少量单元测试,以确保您的代码按预期工作。您可以使用 spring-test 轻松完成。
它可以帮助您更轻松、更快地发现错误。
这是我用来测试命令“jwt generate”和“jwt parse”的几个测试
@Test
public void testInvalidKey()
String result = execute("jwt generate ");
Assertions.assertEquals(JsonWebToken.ERR_LOAD_PRIVATE_KEY, result);
@Test
public void testInvalidExpiredDate()
String result = execute(
"jwt generate -exp \"12-12-2020\"");
Assertions.assertEquals(JsonWebToken.ERR_INVALID_EXPIRED_DATE, result);
@Test
public void testGenerateOk()
String result = execute(
"jwt generate");
Assertions.assertNotNull(result);
Assertions.assertTrue(result.length() > 100);
@Test
public void testParseToken()
String result = execute(
"jwt generate -sub designer");
Assertions.assertNotNull(result);
String parse = execute("jwt parse -token " + result);
Assertions.assertTrue(parse.contains("sub:designer"));
这是我用来检查控制器和过滤器的其他一些测试。
@Test
public void testInValidToken() throws Exception
Map<String, Object> map = new LinkedHashMap<>(reqObj);
mockMvc.perform(postWithTokenAndData(getUrlProvider(), map)
.header(HEADER_AUTHORIZATION, String.format("Bearer %s", INVALID_JWT))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().is4xxClientError());
@Test
public void testExpired() throws Exception
Date exp = Date.from(Instant.now().minusSeconds(3600));
Map<String, Object> map = new LinkedHashMap<>(reqObj);
mockMvc.perform(postWithTokenAndData(getUrlProvider(), map)
.header(HEADER_AUTHORIZATION, String.format("Bearer %s", JwtHelperTest.getJwtToken(exp,
"designer")))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().is4xxClientError());
【讨论】:
以上是关于用于 REST API 的带有 JWT 的 Spring Security的主要内容,如果未能解决你的问题,请参考以下文章
JSON:带有 django-rest-framework-json-api 和 JWT 的 API
带有 jwt 身份验证的 django rest api 要求 csrf 令牌
带有自定义 API Rest Endpoint 的 WordPress 和 JWT
使用 JWT 访问 Rest API 适用于 Postman 但不适用于 Axios
带有 okta OAUTH 令牌身份验证的 Django Rest API
如何在 Django 和 Python 中使用 JWT(JSON Web 令牌)来创建用于注册和登录的 REST API