即使凭据为真,Spring Boot Security 也会在 API 调用上引发 401 身份验证错误

Posted

技术标签:

【中文标题】即使凭据为真,Spring Boot Security 也会在 API 调用上引发 401 身份验证错误【英文标题】:Spring Boot Security throws 401 Authentication error on API calls even if credentials are true 【发布时间】:2018-11-07 01:14:17 【问题描述】:

我正在使用 JWT 身份验证使用 Spring Boot 和 React 开发单页应用程序。我在开发过程中使用了 spring-boot-starter-tomcat 中的 Embedded Tomcat,并成功配置和测试了我的 API 的每个处理程序/方法。

编辑:我改变了对 app.server 的选择并改用 Wildfly。详情请参阅我的答案

然后我将应用程序作为 WAR 打包并部署到应用程序服务器/servlet 容器等。应用程序开始拒绝来自我的客户端和 Firefox 上的其余客户端“RESTClient”的 authenticated() 请求。我已经通过我的 JwtFilter 监控了请求,这不是错误的凭据。

我尝试使用 Glassfish 4.1.2(构建 1)中的其余端点,但没有成功。同样由于一些神秘的原因,glassfish 的 server.log 被 [Thread - 8] unknown.jul.logger 淹没——时间戳......相同......输出,因此目前没有关于请求拒绝的服务器输出, 我还在 Apache Tomcat 8.5.23 上部署了该应用程序,请求安全资源时的错误消息如下:

JwtAuthenticationEntryPoint      : Responding with unauthorized error. Message - Full authentication is required to access this resource

我的应用程序应该保护除 /api/auth/** 下的所有请求之外的所有请求,并且在 IntelliJ 调试模式下在嵌入式 tomcat 上运行时它完美地做到了这一点。 我之前从客户端代码(使用 axios)和 RESTClient 对服务器的调用如下:

(GET) http://localhost:8080/api/user/me

在 app.server/web 容器上:

(GET) http://localhost:8080/myproject/api/user/me

问题是,安全的 rest 端点在 IntelliJ 调试模式下在嵌入式 tomcat 上接受经过身份验证的请求,在 WAR 部署之后它神秘地拒绝了所有安全请求。

我按照from spring docs 使用 maven 进行 WAR 打包的步骤,GitHub page 对 glassfish、tomcat、wildfly 等的 Spring Boot 部署测试进行了操作,最终得到了一个基类如下的项目:

主类,(使用@SpringBootApplication)

@SpringBootApplication
@EntityScan(basePackageClasses = 
        AccountingApplication.class,
        Jsr310JpaConverters.class
)
public class AccountingApplication extends SpringBootServletInitializer

    @PostConstruct
    void init() 
        TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
    

    public static void main(String[] args) 
        SpringApplication.run(applicationClass, args);
    

    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) 
        return application.sources(applicationClass);
    

    private static Class<AccountingApplication> applicationClass = AccountingApplication.class;

以下是我的安全配置

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
    securedEnabled = true,
    jsr250Enabled = true,
    prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter 

@Autowired
CustomUserDetailsService userDetailsService;

@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;

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


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



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


@Bean
public PasswordEncoder passwordEncoder() 
    /* removed for clarity */


@Override
protected void configure(HttpSecurity http) throws Exception 

    http
            .cors()
                .and()
            .csrf()
                .disable()
            .exceptionHandling()
                .authenticationEntryPoint(unauthorizedHandler)
                .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
            .authorizeRequests()
                .antMatchers("/resources/**")
                    .permitAll()
                .antMatchers("/",
                        "/favicon.ico",
                        "/**/*.png",
                        "/**/*.gif",
                        "/**/*.svg",
                        "/**/*.woff",
                        "/**/*.woff2",
                        "/**/*.ttf",
                        "/**/*.eot",
                        "/**/*.jpg",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js")
                    .permitAll()
                .antMatchers("/api/auth/**")
                    .permitAll()
                .anyRequest()
                    .authenticated();

    http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.my.project</groupId>
<artifactId>myproject-web</artifactId>
<version>1.0</version>
<packaging>war</packaging>

<name>myproject-web</name>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.1.RELEASE</version>
    <relativePath/>
</parent>

<properties>
    <start-class>com.my.MainApplication</start-class>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-rest</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-hateoas</artifactId>
    </dependency>
    
    <!-- Exclusion of artifacts as instructed in Spring Boot deployment docs -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
            <exclusion>
                <groupId>org.hibernate.validator</groupId>
                <artifactId>hibernate-validator</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <!-- Provided scope because of the dependency collision during WAR deployment -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.hibernate.validator</groupId>
        <artifactId>hibernate-validator</artifactId>
        <scope>provided</scope>
    </dependency>

    <!-- To access DB -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    
    <!-- utilities -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.0</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>       
</dependencies>
<build>
    <finalName>myproject</finalName>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <addResources>true</addResources>
            </configuration>
        </plugin>
        <plugin>
            <artifactId>maven-failsafe-plugin</artifactId>
            <executions>
                <execution>
                    <goals>
                        <goal>integration-test</goal>
                        <goal>verify</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

application.properties

spring.data.rest.base-path=/api

spring.datasource.url=jdbc:mysql://localhost:3306/myDB?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=1234

spring.jpa.hibernate.ddl-auto=none
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.show-sql = true
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect

logging.level.org.hibernate.SQL= DEBUG
# App properties
app.jwtSecret= jwtSecretKey
app.jwtExpirationInMs = 604800000

一个行为不端的控制器

@RestController
@RequestMapping("/api")
public class UserController 

    @GetMapping("/user")
    public UserProfile getCurrentUser(@CurrentUser UserPrincipal currentUser) 
        UserProfile userSummary = new UserProfile(currentUser.getId(), currentUser.getUsername(), currentUser.getName());
        return userSummary;
    

最后,如果有人想知道是否有任何自定义过滤会干扰预期的流程,我会在容器启动期间展示过滤器配置。

o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'errorPageFilter' to: [/*]
o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
.s.DelegatingFilterProxyRegistrationBean : Mapping filter: 'springSecurityFilterChain' to: [/*]
o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'jwtAuthenticationFilter' to: [/*]
o.s.b.w.servlet.ServletRegistrationBean  : Servlet dispatcherServlet mapped to [/]

Spring Boot 版本。 2.0.1-发布 构建管理。工具:Maven

抱歉,帖子太长了,我能提供的任何东西都可以在 cmets 中询问。

【问题讨论】:

有一个关于这个主题的更新,当我将战争部署到独立的 Tomcat 在调试模式时,它有点作用,但是当我测试访问/安全问题时我上面说的就没了。然后在生产模式下,我成功地从服务器请求了安全资源。 关于 glassfish,我在几个不眠之夜后选择不部署到 glassfish,并发现它需要的不仅仅是调整一些 .xml 文件,而是需要对 servlet 进行更多微调我不太喜欢弄乱的水平。但我确信这是 glassfish 作为应用程序服务器并且所有模块、provided .jars 等与我的项目依赖项和所有内容冲突的问题。我祝所有即将开始工作的人好运。 我在 glassfish 上部署 WAR 时确实遇到了同样的问题。它与嵌入式 tomcat 服务器一起工作。你会如何解决这个问题? 【参考方案1】:

所以,我在问题的cmets上解释了tomcat和glassfish问题,我想分享一下Wildfly部署的细节。

我使用 Wildfly 版本 10.1.Final 进行部署,也使用 11.0.Final 进行了测试,认为部署到 12.0.Final 不会造成任何问题。

为了能够部署,您需要上面的 SpringBootServletInitializer 扩展主类,并且您的 POM 中需要具有以下结构。

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
            <exclusion>
                <groupId>org.hibernate.validator</groupId>
                <artifactId>hibernate-validator</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <!-- Provided scope because of the dependency collision during WAR deployment -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <scope>provided</scope>
    </dependency>

默认情况下,wildfly 10.1.Final 使用 Hibernate Validator 5.x.Final 和 Bean Validation 1.1 和 spring-boot-starter-web 依赖在撰写本文时提供 Hibernate Validator 6.0.5.Final 和 Bean验证 2.0。

因此,为了部署并且不会在服务器调用过程中由于某些验证错误而崩溃,您需要设置 hibernate-validator &lt;scope&gt;provided&lt;/scope&gt; 并确保您拥有 org.hibernate.validator ^6.0.5 和 javax。在您的应用程序中验证 ^2.0 .jar。服务器模块。

如果有人希望就未解决的问题分享他们的智慧,我们非常欢迎。

【讨论】:

以上是关于即使凭据为真,Spring Boot Security 也会在 API 调用上引发 401 身份验证错误的主要内容,如果未能解决你的问题,请参考以下文章

如何将自定义凭据传递给 Spring Boot ldap

Spring Boot Gradle 插件凭据

如何获取 Spring Boot 和 OAuth2 示例以使用默认密码授予凭据以外的其他凭据

即使凭据正确,Spring-security 也不会登录用户

请求正文中的 Spring Boot Keycloak 客户端凭据

Spring Security - 即使凭据正确,身份验证也不起作用