仅在需要身份验证时应用弹簧安全过滤器(由我的应用程序)

Posted

技术标签:

【中文标题】仅在需要身份验证时应用弹簧安全过滤器(由我的应用程序)【英文标题】:Applying a spring security filter only if authentication is required (by my application) 【发布时间】:2020-06-07 01:38:10 【问题描述】:

我已阅读How to apply spring security filter only on secured endpoints?,这似乎与我的问题最接近,但没有充分回答。

您将在下面看到我当前正在使用的 WebSecurityConfigurerAdapter 配置。它不会一直这样,因为我以后不会公开 h2-console。

我的问题是,JwtAuthenticationFilter 总是被执行。我宁愿希望过滤器在需要身份验证的请求上执行(在我的特定情况下:仅此处描述的内容:

.authorizeRequests()
                .anyRequest()
                    .authenticated()

)。

如何做到这一点?

P.s.:我的应用程序登录按预期工作,而 H2-console 也可以,但一直抛出 io.jsonwebtoken.SignatureException,因为 JWT H2-console 生成和使用的自然与我的应用程序使用的不同。

WebSecurityConfigurerAdapter

package com.particles.authservice;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.particles.authservice.jwt.JwtAuthenticationEntryPoint;
import com.particles.authservice.jwt.JwtAuthenticationFilter;
import com.particles.authservice.service.UserService;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter 
    @Autowired
    private UserService userService;

    @Autowired
    private JwtAuthenticationEntryPoint unauthorizedHandler;

    @Override
    public void configure(final AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception 
        authenticationManagerBuilder
                                    .userDetailsService(userService)
                                    .passwordEncoder(passwordEncoder());
    

    @Override
    protected void configure(final HttpSecurity http) throws Exception 
        //@formatter:off
        http
            .cors()
                .and()
            .csrf()
                .disable()
            .headers()
                .frameOptions()
                    .disable()
                    .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
            .authorizeRequests()
                .antMatchers("/",
                        "/favicon.ico",
                        "/**/*.png",
                        "/**/*.gif",
                        "/**/*.svg",
                        "/**/*.jpg",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js")
                    .permitAll()
                .antMatchers("/h2-console/**").permitAll()
                .antMatchers(HttpMethod.POST, "/register")
                    .permitAll()
                .antMatchers(HttpMethod.GET, "/confirm")
                    .permitAll()
                .antMatchers(HttpMethod.POST, "/login")
                    .permitAll()
                .antMatchers(HttpMethod.GET, "/user")
                    .permitAll()
                .and()
            .authorizeRequests()
                .anyRequest()
                    .authenticated()
                .and()
            .exceptionHandling()
                .authenticationEntryPoint(unauthorizedHandler)
                .and()
            .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            ;
        //@formatter:on
    

    @Bean(BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception 
        return super.authenticationManagerBean();
    

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() 
        return new JwtAuthenticationFilter();
    

    @Bean
    public BCryptPasswordEncoder passwordEncoder() 
        return new BCryptPasswordEncoder();
    

编辑:这里是 JwtAuthenticationFilter。如果您也需要 TO,请告诉我。

JwtAuthenticationFilter

package com.particles.authservice.jwt;

import java.io.IOException;
import java.util.Optional;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import com.particles.authservice.tos.UserJwt;

public class JwtAuthenticationFilter extends OncePerRequestFilter 
    private static final String AUTHORIZATION_HEADER_PREFIX               = "Authorization";
    private static final String AUTHORIZATION_HEADER_BEARER_PREFIX        = "Bearer ";
    private static final int    AUTHORIZATION_HEADER_BEARER_PREFIX_LENGTH = AUTHORIZATION_HEADER_BEARER_PREFIX.length();

    @Autowired
    private JwtService jwtService;

    @Override
    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain)
            throws ServletException, IOException 
        if (request.getHeader(AUTHORIZATION_HEADER_PREFIX) != null) 
            final Optional<String> optToken = extractTokenFromRequest(request);

            if (optToken.isPresent() && StringUtils.hasText(optToken.get()) && jwtService.isTokenValid(optToken.get())) 
                // if token exists and is valid, retrieve corresponding UserJwt-object
                final UserJwt jwt = jwtService.getJwtFromToken(optToken.get());

                final UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(jwt.getUser(), null,
                        jwt.getUser().getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            
        

        filterChain.doFilter(request, response);
    

    /**
     * This method extracts a JWT from a @link HttpServletRequest-object.
     *
     * @param request
     *            (@link HttpServletRequest) request, which supposedly contains a JWT
     * @return (Optional&lt;String&gt;) JWT as String
     */
    private Optional<String> extractTokenFromRequest(final HttpServletRequest request) 
        final String bearerToken = request.getHeader(AUTHORIZATION_HEADER_PREFIX);

        String bearerTokenContent = null;
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(AUTHORIZATION_HEADER_BEARER_PREFIX)) 
            bearerTokenContent = bearerToken.substring(AUTHORIZATION_HEADER_BEARER_PREFIX_LENGTH, bearerToken.length());
        

        return Optional.ofNullable(bearerTokenContent);
    

如果您需要查看其他课程,请告诉我,我会在此处粘贴。

【问题讨论】:

@Code_Is_Law:我添加了 JwtAuthenticationFilter 源代码。 UserJwt 类由 3 个属性组成:字符串令牌、PersistableUser 用户(包含用户数据并实现 UserDetails 的实体)和 Date expiresOn。 UserJwt 也是一个实体,因为我决定保存令牌以允许正确注销。 【参考方案1】:

添加以下方法以公开您的公共端点

 @Override
 public void configure(WebSecurity web) throws Exception 
    web.ignoring()
    .antMatchers("/public-api/**");
  

【讨论】:

我完全会这样做,但目前我的 API 在 localhost:9191/auth/* 下,h2-console (localhost:9191/auth/h2-console) 也是如此。我是否应该尝试重新调整以使我的所有公共端点都在 localhost:9191/auth/publicapi 下,而私有 API 在 localhost:9191/auth/privateapi 下公开,而 H2 控制台在 localhost:9191/auth/h2-console 下公开?【参考方案2】:

似乎不可能只使用WebSecurityConfigurerAdapter#configure 方法将过滤器应用于特定端点。

相反,我决定将私有 API 中的端点与所有其他端点分开。

需要过滤器的端点(在我的情况下为JwtAuthenticationFilter)收集在/api/ 下,而不是单独定义,因为很有可能有人忘记将它们添加到configure 方法中 所有其他端点的路径都不同于/api/

我的配置方法是这样的:

@Override
    protected void configure(final HttpSecurity http) throws Exception 
        //@formatter:off
        http
            .cors()
                .and()
            .csrf()
                .disable()
            .headers()
                .frameOptions()
                    .disable()
                    .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
            .authorizeRequests()
                .antMatchers("/",
                        "/favicon.ico",
                        "/**/*.png",
                        "/**/*.gif",
                        "/**/*.svg",
                        "/**/*.jpg",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js")
                    .permitAll()
                .antMatchers("/h2-console/**").permitAll()
                .antMatchers(HttpMethod.POST,
                        PUBLIC_API_PATH + "register",
                        PUBLIC_API_PATH + "login")
                    .permitAll()
                .antMatchers(HttpMethod.GET,
                        PUBLIC_API_PATH + "confirm")
                    .permitAll()
                .and()
            .authorizeRequests()
                .anyRequest()
                    .authenticated()
                .and()
            .exceptionHandling()
                .authenticationEntryPoint(unauthorizedHandler)
                .accessDeniedHandler(unauthorizedHandler)
                .and()
            .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            ;
        //@formatter:on

JwtAuthenticationFilter 中,我检查请求路径是否包含私有API 路径/api/。我只应用过滤器。

    @Override
    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain)
            throws ServletException, IOException 
        if (request.getRequestURI().contains(SecurityConfiguration.PRIVATE_API_PATH)) 
            // perform Jwt-authentication since request-URI suggests a call to private-API
...
            
        

        filterChain.doFilter(request, response);
    

我不喜欢这个解决方案,特别是因为我必须保持 SecurityConfiguration.PRIVATE_API_PATH 一个常量,因为 @*Mapping(value) 需要一个常量。不过,它可以完成工作。

如果您有更好的建议,我很想尝试一下。

编辑 显然可以像这样在@*Mapping(value) 中使用变量:@PostMapping(value = "$apipath/user")。所以我毕竟可以使路径可配置 - 但是JwtAuthenticationFilter 中的检查仍然必须保留;我只是不必使用常量,而是变量,其中包含来自例如application.yaml.

【讨论】:

以上是关于仅在需要身份验证时应用弹簧安全过滤器(由我的应用程序)的主要内容,如果未能解决你的问题,请参考以下文章

弹簧安全过滤器没有启动

仅为授权配置弹簧安全性?

具有弹簧安全性的 JWT 身份验证 graphql-spqr

使会话弹簧安全性无效

没有弹簧安全性的 ldap 和 JWT 身份验证

具有自定义外部表单和弹簧安全性的 CAS 身份验证