Post

Spring) Spring Security Session Authentication - REST API Login

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.

https://gngsn.tistory.com/160


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());
    }
}

Information about the currently active sessions

This post is licensed under CC BY-NC 4.0 by the author.