Spring Security在我尝试第二次获取JWT令牌时返回禁止访问。

huangapple go评论63阅读模式
英文:

Spring security when I try to get jwt token second time, it returns forbidden

问题

I enabled spring security. I have 2 users in my userDetails service. I have an authentication endpoint /api/v1/auth/authenticate. This endpoint is not secured.

I start the application and hit send button in postman. It returns the JWT token. When I click send right after, it returns 403.

After these steps, I entered the credentials of the second user in postman request. I hit the send request button. JWT token returned normally. When I clicked send button the second time, it returns 403.

I am able to access secured endpoints normally using the JWT.

I restarted the server, Again I am able to call and get JWT token but I can get it only once, the other attempts to generate JWT token results in a 403 response.

When I debugged the application, it gets bad credentials exception in this line

authenticationManager.authenticate(
   new UsernamePasswordAuthenticationToken(
        request.getEmail(), request.getPassword()
    )
);

it doesn't print the exception to the console. Even if the credentials are correct (I don't change request body between the first and second attempt to get JWT.) the response is 403 in the second attempt.

Why is that?

I use spring boot 3.1.0

<parent>
    <groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>3.1.0</version>
	<relativePath/> <!-- lookup parent from repository -->
</parent>

my userdetails service is:

@Component
public class MyUserDetailsService implements UserDetailsService {

    private final static List<UserDetails> APPLICATION_USERS = Arrays.asList(
            new User(
                "ylozsoy@gmail.com",
                "password",
                Collections.singleton(new SimpleGrantedAuthority("ROLE_ADMIN"))
            ),
            new User(
                    "yilmazozsoy@gmail.com",
                    "password2",
                    Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"))
            )
    );

    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        return APPLICATION_USERS
                .stream()
                .filter(u -> u.getUsername().equals(email))
                .findFirst()
                .orElseThrow(() -> new UsernameNotFoundException("No user was found for email: " + email));
    }
}

It is breaking here
image

At the first call this looks like :
image

At the second call, password is gone
image

@Override
protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain) throws ServletException, IOException {
    String authHeader = request.getHeader(AUTHORIZATION);
    String userEmail;
    String jwtToken;
    if (authHeader == null || !authHeader.startsWith("Bearer")) {
        filterChain.doFilter(request, response);
        return;
    }
    jwtToken = authHeader.substring(7);
    userEmail = jwtService.extractUsername(jwtToken);
    if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail);
        if(jwtService.isTokenValid(jwtToken, userDetails)) {
            UsernamePasswordAuthenticationToken authToken =
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
        }
    }
    filterChain.doFilter(request, response);
}
@PostMapping("/authenticate")
public ResponseEntity<String> authenticate(@RequestBody AuthenticationRequest request){
    authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword())
    );
    final UserDetails user = userDetailsService.loadUserByUsername(request.getEmail());
    if (user != null) {
        return ResponseEntity.ok(jwtService.generateToken(user));
    }
    return ResponseEntity.status(400).body("Some error has occurred");
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.csrf((csrf) -> csrf.disable());
    http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
    http.authorizeHttpRequests((requests) -> {
        requests.requestMatchers(antMatcher("/api/v1/auth/**"))
                .permitAll()
                .anyRequest()
                .authenticated();
    });
    http.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
    http.authenticationProvider(authenticationProvider());
    http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
    return http.build();
}

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.1.0</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.springbootdemoweb</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>demo</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</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>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.9.1</version>
		</dependency>
		<dependency>
			<groupId>javax.xml.bind</groupId>
			<artifactId>jaxb-api</artifactId>
			<version>2.2.7</version>
		</dependency>
		<dependency>
			<groupId>com.sun.xml.bind</groupId>
			<artifactId>jaxb-impl</artifactId>
			<version>2.2.5-b10</version>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
			

<details>
<summary>英文:</summary>

I enabled spring security. I have 2 users in my userDetails service. I have an authentication endpoint ```/api/v1/auth/authenticate```. This endpoint is not secured.

I start the application and hit send button in postman. It returns the JWT token. When I click send right after, it returns ```403```. 

After these steps, I entered the credentials of the second user in postman request. I hit the send request button. ```JWT``` token returned normally. When I clicked send button the second time, it returns ```403```.

I am able to access secured endpoints normally using the ```JWT```.

I restarted the server, Again I am able to call and get ```JWT``` token but I can get it only once, the other attempts to generate ```JWT``` token results in a ```403``` response.

When I debugged the application, it gets bad credentials exception in this line 

authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(), request.getPassword()
)
);

it doesn&#39;t print the exception to the console. Even if the credentials are correct (I don&#39;t change request body between the first and second attempt to get JWT.) the response is 403 in the second attempt.
Why is that?
I use spring boot 3.1.0

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>


my userdetails service is:
```java
@Component
public class MyUserDetailsService implements UserDetailsService {
private final static List&lt;UserDetails&gt; APPLICATION_USERS = Arrays.asList(
new User(
&quot;ylozsoy@gmail.com&quot;,
&quot;password&quot;,
Collections.singleton(new SimpleGrantedAuthority(&quot;ROLE_ADMIN&quot;))
),
new User(
&quot;yilmazozsoy@gmail.com&quot;,
&quot;password2&quot;,
Collections.singleton(new SimpleGrantedAuthority(&quot;ROLE_USER&quot;))
)
);
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return APPLICATION_USERS
.stream()
.filter(u -&gt; u.getUsername().equals(email))
.findFirst()
.orElseThrow(() -&gt; new UsernameNotFoundException(&quot;No user was found for email: &quot; + email));
}
}

It is breaking here
image

At the first call this looks like :
image

At the second call, password is gone
image

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader(AUTHORIZATION);
String userEmail;
String jwtToken;
if (authHeader == null || !authHeader.startsWith(&quot;Bearer&quot;)) {
filterChain.doFilter(request, response);
return;
}
jwtToken = authHeader.substring(7);
userEmail = jwtService.extractUsername(jwtToken);
if (userEmail != null &amp;&amp; SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail);
if(jwtService.isTokenValid(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
@PostMapping(&quot;/authenticate&quot;)
public ResponseEntity&lt;String&gt; authenticate(@RequestBody AuthenticationRequest request){
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword())
);
final UserDetails user = userDetailsService.loadUserByUsername(request.getEmail());
if (user != null) {
return ResponseEntity.ok(jwtService.generateToken(user));
}
return ResponseEntity.status(400).body(&quot;Some error has occured&quot;);
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf((csrf) -&gt; csrf.disable());
http.sessionManagement(session -&gt; session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.authorizeHttpRequests((requests) -&gt; {
requests.requestMatchers(antMatcher(&quot;/api/v1/auth/**&quot;))
.permitAll()
.anyRequest()
.authenticated();
});
http.sessionManagement((session) -&gt; session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.authenticationProvider(authenticationProvider());
http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}

pom.xml;

&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt;
&lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;
&lt;parent&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;
&lt;version&gt;3.1.0&lt;/version&gt;
&lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt;
&lt;/parent&gt;
&lt;groupId&gt;com.springbootdemoweb&lt;/groupId&gt;
&lt;artifactId&gt;demo&lt;/artifactId&gt;
&lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;
&lt;name&gt;demo&lt;/name&gt;
&lt;description&gt;Demo project for Spring Boot&lt;/description&gt;
&lt;properties&gt;
&lt;java.version&gt;17&lt;/java.version&gt;
&lt;/properties&gt;
&lt;dependencies&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
&lt;artifactId&gt;lombok&lt;/artifactId&gt;
&lt;optional&gt;true&lt;/optional&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;
&lt;scope&gt;test&lt;/scope&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
&lt;groupId&gt;io.jsonwebtoken&lt;/groupId&gt;
&lt;artifactId&gt;jjwt&lt;/artifactId&gt;
&lt;version&gt;0.9.1&lt;/version&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
&lt;groupId&gt;javax.xml.bind&lt;/groupId&gt;
&lt;artifactId&gt;jaxb-api&lt;/artifactId&gt;
&lt;version&gt;2.2.7&lt;/version&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
&lt;groupId&gt;com.sun.xml.bind&lt;/groupId&gt;
&lt;artifactId&gt;jaxb-impl&lt;/artifactId&gt;
&lt;version&gt;2.2.5-b10&lt;/version&gt;
&lt;/dependency&gt;
&lt;/dependencies&gt;
&lt;build&gt;
&lt;plugins&gt;
&lt;plugin&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
&lt;configuration&gt;
&lt;excludes&gt;
&lt;exclude&gt;
&lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
&lt;artifactId&gt;lombok&lt;/artifactId&gt;
&lt;/exclude&gt;
&lt;/excludes&gt;
&lt;/configuration&gt;
&lt;/plugin&gt;
&lt;/plugins&gt;
&lt;/build&gt;
&lt;/project&gt;

This is the complete project.

答案1

得分: 1

Spring Security 避免在内存中保存凭据,如密码,时间尽量短。这是通过 org.springframework.security.core.userdetails.User 实现的 eraseCredentials() 方法来实现的。您的 User 列表保存在静态字段中,因此两个 User 实例被重复使用。一旦凭据被擦除,这会使 User 无效。

与创建自己的内存中的 UserDetailsService 实现不同,我建议使用 Spring Security 的 InMemoryUserDetailsManager

英文:

Spring Security avoids holding credentials, such as a password, in memory for any longer than necessary. This is implemented using the eraseCredentials() method that org.springframework.security.core.userdetails.User implements. Your list of Users is held in a static field so the two User instances are re-used. Once the credentials have been erased, this makes the User useless.

Rather than creating your own in-memory UserDetailsService implementation, I would use Spring Security's InMemoryUserDetailsManager instead.

huangapple
  • 本文由 发表于 2023年5月30日 08:33:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/76360972.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定