【问题标题】:Spring Security with JWT for REST API用于 REST API 的带有 JWT 的 Spring Security
【发布时间】: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。我的电脑翻译器。

标签: java spring-boot jwt rest


【解决方案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());
  }

【讨论】:

    猜你喜欢
    • 2017-01-04
    • 2020-08-09
    • 2018-11-06
    • 2018-01-29
    • 2017-12-12
    • 2018-11-11
    • 2016-08-30
    • 2021-01-27
    • 2016-07-08
    相关资源
    最近更新 更多