2017-02-01 15 views
17

我是JWT的新手。网络上没有太多可用的信息,因为我是作为最后的手段来到这里的。我已经开发了一个春季启动应用程序,使用spring安全性使用spring会话现在,而不是春季会议,我们正在转向智威汤逊。我发现有几个链接,现在我可以验证用户并生成令牌。现在困难的部分是,我想创建一个过滤器,它将验证服务器的每个请求,如何设计一个好的JWT认证过滤器

  1. 过滤器如何验证令牌? (只需验证签名就足够了?)
  2. 如果有人偷走了令牌并进行其他呼叫,我将如何验证该令牌。
  3. 如何在过滤器中绕过登录请求?由于它没有授权标头。
+0

你是询问代码或约的JWT过滤器应该如何工作的泛泛的请求上下文? – pedrofb

+0

您可以分享用于生成JWT的代码吗?我配置了spring以使用OAuth2生成JWT,但我在Auth Server和web-app之间看不到任何令牌... – Teo

回答

10

这里是一个过滤器,可以做你需要的东西:

public class JWTFilter extends GenericFilterBean { 

    private static final Logger LOGGER = LoggerFactory.getLogger(JWTFilter.class); 

    private final TokenProvider tokenProvider; 

    public JWTFilter(TokenProvider tokenProvider) { 

     this.tokenProvider = tokenProvider; 
    } 

    @Override 
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, 
     ServletException { 

     try { 
      HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; 
      String jwt = this.resolveToken(httpServletRequest); 
      if (StringUtils.hasText(jwt)) { 
       if (this.tokenProvider.validateToken(jwt)) { 
        Authentication authentication = this.tokenProvider.getAuthentication(jwt); 
        SecurityContextHolder.getContext().setAuthentication(authentication); 
       } 
      } 
      filterChain.doFilter(servletRequest, servletResponse); 

      this.resetAuthenticationAfterRequest(); 
     } catch (ExpiredJwtException eje) { 
      LOGGER.info("Security exception for user {} - {}", eje.getClaims().getSubject(), eje.getMessage()); 
      ((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED); 
      LOGGER.debug("Exception " + eje.getMessage(), eje); 
     } 
    } 

    private void resetAuthenticationAfterRequest() { 
     SecurityContextHolder.getContext().setAuthentication(null); 
    } 

    private String resolveToken(HttpServletRequest request) { 

     String bearerToken = request.getHeader(SecurityConfiguration.AUTHORIZATION_HEADER); 
     if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { 
      String jwt = bearerToken.substring(7, bearerToken.length()); 
      return jwt; 
     } 
     return null; 
    } 
} 

而在过滤器链中包含的过滤器:

public class SecurityConfiguration extends WebSecurityConfigurerAdapter { 

    public final static String AUTHORIZATION_HEADER = "Authorization"; 

    @Autowired 
    private TokenProvider tokenProvider; 

    @Autowired 
    private AuthenticationProvider authenticationProvider; 

    @Override 
    protected void configure(AuthenticationManagerBuilder auth) throws Exception { 
     auth.authenticationProvider(this.authenticationProvider); 
    } 

    @Override 
    protected void configure(HttpSecurity http) throws Exception { 

     JWTFilter customFilter = new JWTFilter(this.tokenProvider); 
     http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); 

     // @formatter:off 
     http.authorizeRequests().antMatchers("/css/**").permitAll() 
     .antMatchers("/images/**").permitAll() 
     .antMatchers("/js/**").permitAll() 
     .antMatchers("/authenticate").permitAll() 
     .anyRequest().fullyAuthenticated() 
     .and().formLogin().loginPage("/login").failureUrl("/login?error").permitAll() 
     .and().logout().permitAll(); 
     // @formatter:on 
     http.csrf().disable(); 

    } 
} 

的TokenProvider类:

public class TokenProvider { 

    private static final Logger LOGGER = LoggerFactory.getLogger(TokenProvider.class); 

    private static final String AUTHORITIES_KEY = "auth"; 

    @Value("${spring.security.authentication.jwt.validity}") 
    private long tokenValidityInMilliSeconds; 

    @Value("${spring.security.authentication.jwt.secret}") 
    private String secretKey; 

    public String createToken(Authentication authentication) { 

     String authorities = authentication.getAuthorities().stream().map(authority -> authority.getAuthority()).collect(Collectors.joining(",")); 

     ZonedDateTime now = ZonedDateTime.now(); 
     ZonedDateTime expirationDateTime = now.plus(this.tokenValidityInMilliSeconds, ChronoUnit.MILLIS); 

     Date issueDate = Date.from(now.toInstant()); 
     Date expirationDate = Date.from(expirationDateTime.toInstant()); 

     return Jwts.builder().setSubject(authentication.getName()).claim(AUTHORITIES_KEY, authorities) 
        .signWith(SignatureAlgorithm.HS512, this.secretKey).setIssuedAt(issueDate).setExpiration(expirationDate).compact(); 
    } 

    public Authentication getAuthentication(String token) { 

     Claims claims = Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(token).getBody(); 

     Collection<? extends GrantedAuthority> authorities = Arrays.asList(claims.get(AUTHORITIES_KEY).toString().split(",")).stream() 
        .map(authority -> new SimpleGrantedAuthority(authority)).collect(Collectors.toList()); 

     User principal = new User(claims.getSubject(), "", authorities); 

     return new UsernamePasswordAuthenticationToken(principal, "", authorities); 
    } 

    public boolean validateToken(String authToken) { 

     try { 
      Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(authToken); 
      return true; 
     } catch (SignatureException e) { 
      LOGGER.info("Invalid JWT signature: " + e.getMessage()); 
      LOGGER.debug("Exception " + e.getMessage(), e); 
      return false; 
     } 
    } 
} 

现在回答你的问题:

  1. 完成这个过滤器
  2. 保护您的HTTP请求,请使用HTTPS
  3. 就允许所有的/login URI(/authenticate在我的代码)
+0

感谢您的回答,但请您解释这两行,Authentication authentication = this.tokenProvider .getAuthentication(JWT); SecurityContextHolder.getContext()。setAuthentication(authentication); – arunan

+0

您是否将密码存储在JWT令牌中? – arunan

+0

没有密码未存储在JWT中。 'this.tokenProvider.getAuthentication(jwt)'使用密钥解密jwt并返回一个没有密码的新的Spring Security'UsernamePasswordAuthenticationToken' - 它从声明中提取用户名和权限。然后我把这个AuthenticationToken放在'SecurityContextHolder'里面,这样Spring Security认为用户已经登录了。 –

1

看看this项目是非常良好的实施和有所需的文件。

。它是上面的项目,这是你需要验证令牌的唯一东西,这就足够了。其中token是请求头中的Bearer的值。

try { 
    final Claims claims = Jwts.parser().setSigningKey("secretkey") 
     .parseClaimsJws(token).getBody(); 
    request.setAttribute("claims", claims); 
} 
catch (final SignatureException e) { 
    throw new ServletException("Invalid token."); 
} 

。窃取令牌不是那么容易,但根据我的经验,您可以通过为每次成功登录手动创建一个Spring会话来保护自己。还将会话唯一ID和承载者值(令牌)映射到Map(创建Bean例如使用API​​范围)。

@Component 
public class SessionMapBean { 
    private Map<String, String> jwtSessionMap; 
    private Map<String, Boolean> sessionsForInvalidation; 
    public SessionMapBean() { 
     this.jwtSessionMap = new HashMap<String, String>(); 
     this.sessionsForInvalidation = new HashMap<String, Boolean>(); 
    } 
    public Map<String, String> getJwtSessionMap() { 
     return jwtSessionMap; 
    } 
    public void setJwtSessionMap(Map<String, String> jwtSessionMap) { 
     this.jwtSessionMap = jwtSessionMap; 
    } 
    public Map<String, Boolean> getSessionsForInvalidation() { 
     return sessionsForInvalidation; 
    } 
    public void setSessionsForInvalidation(Map<String, Boolean> sessionsForInvalidation) { 
     this.sessionsForInvalidation = sessionsForInvalidation; 
    } 
} 

SessionMapBean将适用于所有会议。现在,在每个请求中,您不仅要验证令牌,还要检查他是否会对会话进行数学运算(检查请求会话ID是否与存储在SessionMapBean中的会话ID匹配)。当然,会话ID也可能被窃取,因此您需要确保通信安全。窃取会话ID的最常见方式是会话嗅探(或中间的男人)和跨站点脚本攻击。我不会详细了解他们,你可以阅读如何保护自己免受这种攻击。

3.你可以看到它到我链接的项目中。最简单的过滤器将验证所有/api/*,您将登录到/user/login例如。

2

我将集中在智威汤逊的一般提示,没有关于代码implemementation(见其他答案)

过滤器如何将验证令牌? (只是验证签名是否足够?)

RFC7519规定如何验证智威汤逊(见7.2. Validating a JWT),基本上是一个语法验证和签名验证

如果JWT正用于身份验证流程中,我们可以查看OpenID连接规范3.1.3.4 ID Token Validation提出的验证。总结:

  • iss包含发行者标识符(和aud包含client_id如果使用OAuth)使用秘密密钥

    令牌的签名iat和验证之间exp

  • 当前时间

  • sub标识有效的用户

如果别人偷了令牌,使其余的电话,我将如何验证。

JWT的拥有权是验证的证明。偷走令牌的攻击者可以冒充用户。因此,保持令牌使用TLS

  • 使用安全存储您的令牌安全

    • 加密通信通道。如果使用web前端考虑增加额外的安全措施来保护的localStorage /饼干对XSS和CSRF的攻击

    • 设置短的到期时间身份验证令牌,需要的凭据,如果令牌已过期

    我将如何绕过过滤器中的登录请求?由于它没有授权标头。

    由于您要验证用户凭证,因此登录表单不需要JWT令牌。保持表格不在过滤器的范围内。发出JWT成功认证后,认证过滤器适用于服务

    其余然后过滤应该截获所有请求除了登录表单,并检查:

    1. 如果用户认证?如果不抛弃401-Unauthorized

    2. 如果用户授权请求资源?如果不投掷403-Forbidden

    3. 允许访问。把用户数据(例如,使用一个ThreadLocal)