Spring) Spring Security Session Authentication - REST API Login
This post was migrated from Tistory. You can find the original here.
Version
Spring boot 3.3.3
Spring Security 6.3.3
JWT and Session
This can vary depending on the implementation, but in general the two mean the following.
JWT is stateless. The server does not hold any authentication state.
Session is stateful. The server holds the authentication state.
For a service with a lot of concurrent connections, having the server hold authentication state is a burden,
but if the set of people connecting is fixed (admins, on-site workers, etc.), going with the Session approach can be perfectly reasonable.
Implementing Security Session REST API Login
Let’s implement Session Login with Spring Security.
The filter chain flow is nicely summarized in the following blog post.
Configure SecurityConfig as follows,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// SecurityConfig.java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
....
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable);
http.cors(withDefaults());
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(permitAllRouteArray).permitAll()
.requestMatchers("/api/user/signup").permitAll()
.requestMatchers("/api/user/login").permitAll()
.requestMatchers("/api/user/check").permitAll()
.anyRequest().authenticated()
);
http.sessionManagement(session -> session
.sessionFixation().migrateSession()
);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(authenticationProvider);
}
@Bean
public HttpSessionSecurityContextRepository securityContextRepository() {
HttpSessionSecurityContextRepository repository = new HttpSessionSecurityContextRepository();
repository.setSpringSecurityContextKey(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
return repository;
}
@Bean
public UserDetailsService userDetailsService() {
return customUserDetailsService;
}
@Bean
public PasswordEncoder passwordEncoder() {
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder());
return new DelegatingPasswordEncoder("bcrypt", encoders);
}
...
}
In UserController, log in as follows.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// UserController.java
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/user")
@Slf4j
public class UserController {
@NonNull
private UserService userService;
@NonNull
private AuthenticationManager authenticationManager;
@NonNull
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
@NonNull
private HttpSessionSecurityContextRepository securityContextRepository;
@PostMapping(value = "/login", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public ResponseEntity<String> login(
@ModelAttribute UserCreateDto requestDto,
HttpServletRequest req,
HttpServletResponse res
){
System.out.println(requestDto);
// authentication
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(requestDto.getEmail(), requestDto.getPassword());
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// Save the authenticated SecurityContext into the SessionRepository as well as the request and response
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(authentication);
securityContextRepository.saveContext(context, req, res);
return ResponseEntity.ok().body("success");
}
Authentication
1
2
3
4
// authentication
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(requestDto.getEmail(), requestDto.getPassword());
Authentication authentication = authenticationManager.authenticate(authenticationToken);
This corresponds to this part.
This is the flow of the UsernamePasswordAuthenticationFilter in the filter chain.
The AuthenticationManager in the Config delegated authentication to DaoAuthenticationProvider,
and calling manager.authenticate(authToken) in the Controller kicks off the authentication process for the specified Provider.
The Provider’s authentication process goes through UserDetailsService to obtain a UserDetails object,
then checks whether it matches the password after it’s been run through the passwordEncoder specified earlier.
The UserDetails/Service implementation looks like this.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// UserDetailsServiceImpl.java
@Service
@RequiredArgsConstructor
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {
@NonNull
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
log.info("=================== UserDetailsServiceImpl");
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + email));
return new UserDetailsImpl(user);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//UserDetailsImpl.java
@RequiredArgsConstructor
@Slf4j
public class UserDetailsImpl implements UserDetails {
@NonNull
private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
log.info("=================== UserDetailsImpl");
UserRoleEnum role = user.getRole();
String authority = role.getAuthority();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(simpleGrantedAuthority);
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getEmail();
}
@Override
public boolean isAccountNonExpired() {
return UserDetails.super.isAccountNonExpired();
}
@Override
public boolean isAccountNonLocked() {
return UserDetails.super.isAccountNonLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return UserDetails.super.isCredentialsNonExpired();
}
@Override
public boolean isEnabled() {
return UserDetails.super.isEnabled();
}
}
Storing the Session
If you don’t implement a separate storage mechanism for Security Session authentication, the session is stored in the server’s memory.
1
2
3
4
// Save the authenticated SecurityContext into the SessionRepository as well as the request and response
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(authentication);
securityContextRepository.saveContext(context, req, res);
Let’s take a closer look at this part.
After setting up the SecurityContext object, it’s saved into the securityContextRepository injected from the Config.
The Context is also saved onto the request (HttpServletRequest) and the response (HttpServletResponse). This way, the response cookie ends up containing the authenticated JSESSIONID value.
By implementing HttpSessionListener, you can check the sessions that are currently connected.
1
2
3
4
5
6
7
8
9
10
11
12
13
@GetMapping(value = "check")
private ResponseEntity<?> check(){
Map<String, HttpSession> sessions = SessionListener.getSessions();
Map<String, Object> sessionData = new HashMap<>();
for (Map.Entry<String, HttpSession> entry : sessions.entrySet()) {
String sessionId = entry.getKey();
HttpSession session = entry.getValue();
Object principal = session.getAttribute("SPRING_SECURITY_CONTEXT");
sessionData.put(sessionId, principal);
}
return ResponseEntity.ok(sessionData);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class SessionListener implements HttpSessionListener {
@Getter
private static final Map<String, HttpSession> sessions = new ConcurrentHashMap<>();
@Override
public void sessionCreated(HttpSessionEvent event) {
HttpSession session = event.getSession();
sessions.put(session.getId(), session);
}
@Override
public void sessionDestroyed(HttpSessionEvent event) {
HttpSession session = event.getSession();
sessions.remove(session.getId());
}
}

