Spring Boot 安全 + JWT

Nuñ*_*ada 5 spring spring-mvc jwt spring-boot

我有一个使用 JSON Web Tokens 的 SpringBoot 2.4.2 应用程序(JWT,有时发音为 /d\xca\x92\xc9\x92t/,与英语单词“jot”[1]相同)是互联网提出的创建标准具有可选签名和/或可选加密的数据,其有效负载包含断言一定数量声明的 JSON。令牌使用私钥或公钥/私钥进行签名。例如,服务器可以生成一个具有“以管理员身份登录”声明的令牌,并将其提供给客户端。然后,客户端可以使用该令牌来证明它是以管理员身份登录的。

\n

这是我的网络安全配置:

\n
@Configuration\n@EnableWebSecurity\n@EnableGlobalMethodSecurity(prePostEnabled = true)\npublic class WebSecurityConfig extends WebSecurityConfigurerAdapter {\n\n    private static final String SALT = "fd23451*(_)nof";\n\n    private final JwtAuthenticationEntryPoint unauthorizedHandler;\n    private final JwtTokenUtil jwtTokenUtil;\n    private final UserSecurityService userSecurityService;\n\n    @Value("${jwt.header}")\n    private String tokenHeader;\n\n\n    public ApiWebSecurityConfig(JwtAuthenticationEntryPoint unauthorizedHandler, JwtTokenUtil jwtTokenUtil,\n            UserSecurityService userSecurityService) {\n        this.unauthorizedHandler = unauthorizedHandler;\n        this.jwtTokenUtil = jwtTokenUtil;\n        this.userSecurityService = userSecurityService;\n    }\n\n    @Autowired\n    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {\n        auth\n                .userDetailsService(userSecurityService)\n                .passwordEncoder(passwordEncoder());\n    }\n\n    @Bean\n    public BCryptPasswordEncoder passwordEncoder() {\n        return new BCryptPasswordEncoder(12, new SecureRandom(SALT.getBytes()));\n    }\n\n    @Bean\n    @Override\n    public AuthenticationManager authenticationManagerBean() throws Exception {\n        return super.authenticationManagerBean();\n    }\n\n    @Override\n    protected void configure(HttpSecurity httpSecurity) throws Exception {\n\n        httpSecurity\n                // we don't need CSRF because our token is invulnerable\n                .csrf().disable()\n\n                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()\n\n                // don't create session\n                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()\n                .authorizeRequests()\n                // Un-secure H2 Database\n                .antMatchers("/h2-console/**/**").permitAll()\n                .antMatchers("/api/v1/users").permitAll()\n                .anyRequest().authenticated();\n\n        // Custom JWT based security filter\n        JwtAuthorizationTokenFilter authenticationTokenFilter = new JwtAuthorizationTokenFilter(userDetailsService(), jwtTokenUtil, tokenHeader);\n        httpSecurity\n                .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);\n\n        // disable page caching\n        httpSecurity\n                .headers()\n                .frameOptions()\n                .sameOrigin()  // required to set for H2 else H2 Console will be blank.\n                .cacheControl();\n    }\n\n    @Override\n    public void configure(WebSecurity web) {\n\n        // AuthenticationTokenFilter will ignore the below paths\n        web\n                .ignoring()\n                .antMatchers(\n                        HttpMethod.POST,\n                        "/api/v1/users"\n                );\n\n    }\n\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这是我的过滤器:

\n
@Provider\n@Slf4j\npublic class JwtAuthorizationTokenFilter extends OncePerRequestFilter {\n\n    private UserDetailsService userDetailsService;\n    private JwtTokenUtil jwtTokenUtil;\n    private String tokenHeader;\n\n    public JwtAuthorizationTokenFilter(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, String tokenHeader) {\n        this.userDetailsService = userDetailsService;\n        this.jwtTokenUtil = jwtTokenUtil;\n        this.tokenHeader = tokenHeader;\n    }\n\n\n    @Override\n    protected boolean shouldNotFilter(HttpServletRequest request) {\n        return new AntPathMatcher().match("/api/v1/users", request.getServletPath());\n    }\n\n\n    @Override\n    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException,\n            IOException {\n\n        log.info("processing authentication for '{}'", request.getRequestURL());\n\n        final String requestHeader = request.getHeader(this.tokenHeader);\n\n        String username = null;\n        String authToken = null;\n\n        if (requestHeader != null && requestHeader.startsWith("Bearer ")) {\n            authToken = requestHeader.substring(7);\n            try {\n                username = jwtTokenUtil.getUsernameFromToken(authToken);\n            } catch (IllegalArgumentException e) {\n                logger.info("an error occured during getting username from token", e);\n            } catch (ExpiredJwtException e) {\n                logger.info("the token is expired and not valid anymore", e);\n            }\n        } else {\n            logger.info("couldn't find bearer string, will ignore the header");\n        }\n\n        log.info("checking authentication for user '{}'", username);\n\n        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {\n            logger.info("security context was null, so authorizating user");\n\n            // It is not compelling necessary to load the use details from the database. You could also store the information\n            // in the token and read it from it. It's up to you ;)\n            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);\n\n            // For simple validation it is completely sufficient to just check the token integrity. You don't have to call\n            // the database compellingly. Again it's up to you ;)\n            if (jwtTokenUtil.validateToken(authToken, userDetails)) {\n                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());\n                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));\n                log.info("authorizated user '{}', setting security context", username);\n                SecurityContextHolder.getContext().setAuthentication(authentication);\n            }\n        }\n        chain.doFilter(request, response);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

\n
@Component\n@Slf4j\npublic class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {\n\n    private static final long serialVersionUID = -8970718410437077606L;\n\n    @Override\n    public void commence(HttpServletRequest request,\n            HttpServletResponse response,\n            AuthenticationException authException) throws IOException {\n\n        log.info("user tries to access a secured REST resource without supplying any credentials");\n\n        // This is invoked when user tries to access a secured REST resource without supplying any credentials\n        // We should just send a 401 Unauthorized response because there is no 'login page' to redirect to\n        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这是我启动应用程序时的控制台:

\n
18:02:51.974 [restartedMain] DEBUG com.agrumh.Application - Running with Spring Boot v2.4.2, Spring v5.3.3\n18:02:51.974 [restartedMain] INFO  com.agrumh.Application - No active profile set, falling back to default profiles: default\n18:02:57.383 [restartedMain] INFO  o.s.s.web.DefaultSecurityFilterChain - Will secure Ant [pattern='/api/v1/users', POST] with []\n18:02:57.414 [restartedMain] DEBUG o.s.s.w.a.e.ExpressionBasedFilterInvocationSecurityMetadataSource - Adding web access control expression [permitAll] for Ant [pattern='/h2-console/**/**']\n18:02:57.415 [restartedMain] DEBUG o.s.s.w.a.e.ExpressionBasedFilterInvocationSecurityMetadataSource - Adding web access control expression [permitAll] for Ant [pattern='/api/v1/users']\n18:02:57.416 [restartedMain] DEBUG o.s.s.w.a.e.ExpressionBasedFilterInvocationSecurityMetadataSource - Adding web access control expression [authenticated] for any request\n18:02:57.422 [restartedMain] INFO  o.s.s.web.DefaultSecurityFilterChain - Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@24c68fed, org.springframework.security.web.context.SecurityContextPersistenceFilter@1537eb0a, org.springframework.security.web.header.HeaderWriterFilter@95de45c, org.springframework.security.web.authentication.logout.LogoutFilter@733cf550, com.dispacks.config.JwtAuthorizationTokenFilter@538a96c8, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@8d585b2, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@784cf061, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@64915f19, org.springframework.security.web.session.SessionManagementFilter@21f180d0, org.springframework.security.web.access.ExceptionTranslationFilter@2b153a28, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@4942d157]\n18:02:58.619 [restartedMain] INFO  com.dispacks.DispacksApplication - Started DispacksApplication in 6.974 seconds (JVM running for 7.697)\n18:04:03.685 [http-nio-1133-exec-1] DEBUG o.s.security.web.FilterChainProxy - Securing POST /error\n18:04:03.687 [http-nio-1133-exec-1] DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - Set SecurityContextHolder to empty SecurityContext\n18:04:03.689 [http-nio-1133-exec-1] DEBUG o.s.s.w.a.AnonymousAuthenticationFilter - Set SecurityContextHolder to anonymous SecurityContext\n18:04:03.694 [http-nio-1133-exec-1] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Failed to authorize filter invocation [POST /error] with attributes [authenticated]\n18:04:03.698 [http-nio-1133-exec-1] INFO  c.d.s.JwtAuthenticationEntryPoint - user tries to access a secured REST resource without supplying any credentials\n18:04:03.699 [http-nio-1133-exec-1] DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - Cleared SecurityContextHolder to complete request\n
Run Code Online (Sandbox Code Playgroud)\n

但是当我使用 Postman 访问时出现此错误:

\n
22:58:33.562 [http-nio-1133-exec-2] WARN  o.s.w.s.m.s.DefaultHandlerExceptionResolver - Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'text/plain' not supported]\n22:58:33.579 [http-nio-1133-exec-2] INFO  c.d.s.JwtAuthenticationEntryPoint - user tries to access a secured REST resource without supplying any credentials\n
Run Code Online (Sandbox Code Playgroud)\n

小智 0

授权和认证不同 POST 是/api/v1/users被允许的,因为 POST 资源不需要授权就可以访问。

在你的代码中,

    @Override
    public void commence(HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException authException // AuthenticationException means authentication failed, not "without supplying any credentials".
    ) throws IOException {

// Break point here, or print authException.

        log.info("user tries to access a secured REST resource without supplying any credentials"); // Wrong message. You can say "Authentication failed.", or log.info(authException.getMessage()).

        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
Run Code Online (Sandbox Code Playgroud)

身份验证错误实际上是在访问/error资源时发生的。

18:04:03.694 [http-nio-1133-exec-1] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Failed to authorize filter invocation [POST /error] with attributes [authenticated]
Run Code Online (Sandbox Code Playgroud)

我假设发生了一些错误,您的应用程序将您重定向到/error,但/error是受保护的。所以authenticationException发生在/error.

  1. 添加/error之前.permitAll()
  2. 对authenticationException 进行断点,以便我可以更新此答案。