Spring) Spring Security Authentication/Authorization
This post was migrated from Tistory. You can find the original here.
Filter
This is Spring’s life cycle.
Security
Spring Security is also made up of a lot of filters.
The purpose of Security is to separate authentication/authorization from the controller and handle it easily at the filter layer.
Authentication - verifying that the user is who they claim to be
Authorization - checking whether the user is allowed to access a particular resource
Authentication - verifying membership, logging in, issuing a JWT (with user info in the payload)
Authorization - JWT verification and authorization-related work (Role_Blabla)
Security’s authorization can also be done with annotations like @Secured.
Once you add implementation 'org.springframework.boot:spring-boot-starter-security' to gradle, autoconfiguration automatically kicks in and all routes start returning 401.
1
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
You can exclude it before setting things up.
Authentication
(1)
(2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(class WebSecurityConfig)
private final AuthenticationConfiguration authenticationConfiguration;
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception{
return configuration.getAuthenticationManager();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception{
JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
return filter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
...
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
We create a custom filter, assign it an AuthenticationManager, and build a jwtAuthenticationFilter() that returns the filter.
In securityFilterChain, we create the filter with the Manager assigned, and specify where in the chain it should run.
(3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter)
@Autowired
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
setFilterProcessesUrl("/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
...
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
requestDto.getUsername(), //Principal
requestDto.getPassword(), //Credentials
null // Authorities
)
);
}
We override the attemptAuthentication function and attempt login with requestDto. What the login attempt function returns is an Authentication object, structured as shown in (3).
1
2
3
4
5
6
7
8
(public class UserDetailsServiceImpl implements UserDetailsService)
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));
return new UserDetailsImpl(user);
}
The Authentication returned from attemptAuthentication passes only the String username as the Principal into the UserDetailsService implementation. ( hmm… )
As shown in (2), UserDetailsService must return a UserDetails object wrapping the user object.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(public class UserDetailsImpl implements UserDetails {)
private final User user;
public UserDetailsImpl(final User user) {
this.user = user;
}
public User getUser(){
return user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
UserRoleEnum role = user.getRole();
String authority = role.getAuthority();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(simpleGrantedAuthority);
return authorities;
}
...
...
We implement the functions in accordance with the interface we need to implement.
In (3), Authorities is a Collection that abstracts permissions as GrantedAuthority.
The UserDetails returned this way is compared against requestDto, as shown in (2), to check whether the username and password match. (the comparison logic isn’t something we write ourselves) If they match, flow continues to step 4 in (1); if not, it goes to step 3.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {)
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) {
log.info("로그인 성공 및 JWT 생성"); // Login succeeded, generating JWT
String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();
String token = jwtUtil.createToken(username, role);
jwtUtil.addJwtToCookie(token, response);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
log.info(failed.getMessage());
response.setStatus(401);
}
AuthenticationSuccessHandler extends UsernamePasswordAuthenticationFilter and corresponds to our custom filter’s successfulAuthentication function, while AuthenticationFailureHandler corresponds to unsuccessfulAuthentication.
We override both functions to handle each case appropriately.
Authorization
(4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
....
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf((csrf) -> csrf.disable());
http.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.authorizeHttpRequests((authorizeHttpRequest) ->
authorizeHttpRequest
.requestMatchers("/api/user/signup").permitAll()
.anyRequest().authenticated()
);
http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
The authorization filter verifies the JWT and authorizes the ROLE. It extends OncePerRequestFilter and overrides doFilterInternal.
So AuthorizationFilter() is a filter that runs once even without being inserted into the Security chain, and inserting it into the chain lets you control the timing at which it runs.
As shown in (4), the route Security uses for authentication is never actually connected through to the controller’s route. permitAll() doesn’t apply to the authentication route either.
1
2
.requestMatchers("/login").permitAll()
// the authenticated url is /login
Even if you write it this way, only Security’s /login handles the request, not the controller’s /login.
+) When creating a class that extends UsernamePasswordAuthenticationFilter, we set Security’s authenticated route with setFilterProcessesUrl(“/login”).
In the end, permitAll() only applies to the authorization filter. Just because permitAll() is applied doesn’t mean the authorization filter doesn’t run. It seems that regardless of the authorization filter’s result, the request passes through the Security chain.
(Does it work this way because permitAll skips Security while OncePerRequestFilter still runs once, or does the security chain proceed as normal but just ignore the result?)
When a request comes in on the login route, both authorization and authentication run, but functionally only authentication should run for things to behave as expected.




