Spring 如何设计一个好的JWT认证过滤器

Spring 如何设计一个好的JWT认证过滤器,spring,spring-boot,jwt,Spring,Spring Boot,Jwt,我是JWT的新手。由于我是万不得已才来到这里的,所以网上没有多少信息。我已经使用spring会话开发了一个使用spring安全性的spring引导应用程序。现在,我们将转向JWT,而不是春季课程。我发现很少有链接,现在我可以验证用户身份并生成令牌。现在困难的部分是,我想创建一个过滤器,它将对服务器的每个请求进行身份验证 过滤器将如何验证令牌?(仅验证签名就足够了?) 如果有人偷了令牌并拨打rest电话,我将如何验证 如何在筛选器中绕过登录请求?因为它没有授权头 以下是一个可以满足您需要的过滤器:

我是JWT的新手。由于我是万不得已才来到这里的,所以网上没有多少信息。我已经使用spring会话开发了一个使用spring安全性的spring引导应用程序。现在,我们将转向JWT,而不是春季课程。我发现很少有链接,现在我可以验证用户身份并生成令牌。现在困难的部分是,我想创建一个过滤器,它将对服务器的每个请求进行身份验证

  • 过滤器将如何验证令牌?(仅验证签名就足够了?)
  • 如果有人偷了令牌并拨打rest电话,我将如何验证
  • 如何在筛选器中绕过登录请求?因为它没有授权头

  • 以下是一个可以满足您需要的过滤器:

    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();
    
        }
    }
    
    令牌提供程序类:

    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;
            }
        }
    }
    
    公共类令牌提供者{
    私有静态最终记录器Logger=LoggerFactory.getLogger(TokenProvider.class);
    私有静态最终字符串权限\u KEY=“auth”;
    @值(${spring.security.authentication.jwt.validity}”)
    私有长令牌ValidityInms;
    @值(${spring.security.authentication.jwt.secret}”)
    私钥;
    公共字符串createToken(身份验证){
    字符串authorities=authentication.getAuthorities().stream().map(authority->authority.getAuthority()).collect(collector.joining(“,”);
    ZonedDateTime now=ZonedDateTime.now();
    ZonedDateTime expirationDateTime=now.plus(this.tokenValidityInMillions,ChronoUnit.MILLIS);
    Date issueDate=Date.from(now.toInstant());
    Date expirationDate=Date.from(expirationDateTime.toInstant());
    返回Jwts.builder().setSubject(authentication.getName()).claim(AUTHORITIES\u KEY,AUTHORITIES)
    .signWith(SignatureAlgorithm.HS512,this.secretKey).setIssuedAt(issueDate).setExpiration(expirationDate).compact();
    }
    公共身份验证getAuthentication(字符串令牌){
    Claims=Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(token.getBody();
    收集看看这个项目,它执行得非常好,并且有必要的文档

    1。在上面的项目中,这是您唯一需要验证令牌的东西,并且已经足够了。其中
    token
    是请求头中
    承载者的值

    try {
        final Claims claims = Jwts.parser().setSigningKey("secretkey")
            .parseClaimsJws(token).getBody();
        request.setAttribute("claims", claims);
    }
    catch (final SignatureException e) {
        throw new ServletException("Invalid token.");
    }
    
    2。窃取令牌并不容易,但根据我的经验,您可以通过为每次成功登录手动创建Spring会话来保护自己。此外,还可以将会话唯一ID和承载值(令牌)映射到映射中(例如使用API范围创建Bean)

    @组件
    公共类SessionMapBean{
    私有地图jwtSessionMap;
    私有映射会话失效;
    publicsessionmapbean(){
    this.jwtSessionMap=newhashmap();
    this.sessionsforinvalization=新HashMap();
    }
    公共映射getJwtSessionMap(){
    返回jwtSessionMap;
    }
    public void setJwtSessionMap(映射jwtSessionMap){
    this.jwtSessionMap=jwtSessionMap;
    }
    公共映射GetSessionForInvalization(){
    返回失效会话;
    }
    公共无效设置会话失效验证(映射会话失效){
    this.sessionsforinvalization=sessionsforinvalization;
    }
    }
    
    SessionMapBean
    将可用于所有会话。现在,在每个请求中,您不仅要验证令牌,还要检查他是否匹配会话(检查请求会话id是否与存储在
    SessionMapBean
    中的会话id匹配)。当然,会话ID也可能被窃取,因此您需要保护通信安全。窃取会话ID的最常见方法是会话嗅探(或中间的人)和跨站点脚本攻击。我将不详细介绍它们。您可以阅读如何保护自己免受此类攻击


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

    我将关注JWT的一般提示,而不考虑代码的实现(参见其他答案)

    过滤器将如何验证令牌?(仅验证签名就足够了?)

    RFC7519指定如何验证JWT(请参阅),基本上是语法验证和签名验证

    如果JWT用于身份验证流,我们可以查看OpenID connect规范提出的验证。总结:

    • iss
      包含发卡机构标识符(如果使用oauth,
      aud
      包含
      client\u id

    • iat
      exp

    • 使用密钥验证令牌的签名

    • sub
      标识有效用户

    如果有人偷了令牌并拨打rest电话,我将如何验证

    拥有JWT是身份验证的证明。窃取令牌的攻击者可以模拟用户。因此,请确保令牌的安全

    • 使用TLS加密通信通道

    • > P>使用令牌<<强>安全存储< /强>。如果使用Web前端考虑添加额外的安全措施来保护本地存储/ Cookie免受XSS或CSRF攻击,

    • 在身份验证令牌上设置较短的过期时间,如果令牌过期,则需要凭据

    我将如何在筛选器中绕过登录请求?因为它没有授权标头

    登录表单不需要JWT令牌,因为您将验证用户凭据
    @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;
        }
    }