Skip to content

devwuu/spring-security-exam

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Spring security 예제

  • 강의 : 최주호님의 스프링부트 시큐리티 & JWT 강의 ( https://inf.run/R1AW )
  • 사용 버전
    • spring boot (3.1.1)
    • spring boot oauth2 client(3.1.1)
    • spring security (6.1.x)
    • spring data JPA
    • mySql

(Spring boot 3.X + Spring Security 6.X) 변경사항

1. SecurityFilterChain 설정 방법 변경

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/blog/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(formLogin -> formLogin
                .loginPage("/login")
                .permitAll()
            )
            .rememberMe(Customizer.withDefaults());

        return http.build();
    }
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
	CorsConfiguration configuration = new CorsConfiguration();
	configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
	configuration.setAllowedMethods(Arrays.asList("GET","POST"));
	UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
	source.registerCorsConfiguration("/**", configuration);
	return source;
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .cors(configurer ->
                configurer.configurationSource(corsConfigurationSource())
        )
        ...

    return http.build();
}

2. AuthenticationManager 등록 방법

@Bean
public AuthenticationManager authenticationManager(){
    DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
    authProvider.setUserDetailsService(userDetailService);
    authProvider.setPasswordEncoder(getPassWordEncoder());
    return new ProviderManager(authProvider);
}

Further Study

1. 여러개의 SecurityFilterChain 등록하는 방법

    @Bean
    public SecurityFilterChain clientFilterChain(HttpSecurity http) throws Exception {
        http
                .securityMatchers((matchers) -> matchers
                        .requestMatchers("client/**", "v1/client/**")
                )
		...

        return http.build();
    }

    @Bean
    public SecurityFilterChain adminFilterChain(HttpSecurity http) throws Exception {
        http
                .securityMatchers((matchers) -> matchers
                        .requestMatchers("admin/**", "v1/admin/**")
                )
		...

        return http.build();
    }

2. login url을 custom 하는 법

   @Bean
    public AdminAuthenticationFilter adminAuthenticationFilter(){
	...
        adminAuthenticationFilter.setFilterProcessesUrl("/admin/token");
        adminAuthenticationFilter.setPostOnly(true);
        return adminAuthenticationFilter;
    }

3. AuthorizationFilter에서 사용하지 않는 authenticationManager를 제외하고 Filter를 구현하는 방법

  • OncePerRequestFilter 를 상속받는다
  • 이 경우엔 Filter를 등록할 때 Filter의 순서를 정해줘야한다
  • 주의 : OncePerRequestFilter의 경우 Bean으로 등록하면 securityMatchers에 관계 없이 모든 filterChain에 등록되기 때문에
    (1) filterChain이 복수개일 경우 Bean으로 등록하지 않거나(new 연산자로 filterChain에 등록)
    (2) shouldNotFilter() 메서드를 overriding하여 제외시킬 url을 등록해준다
  • 예제 : https://github.com/devwuu/VRS_VETReservationSystem
  • 출처 : https://www.toptal.com/spring/spring-security-tutorial
package com.web.vt.security;

import com.auth0.jwt.JWT;
import com.web.vt.utils.StringUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class AdminAuthorizationFilter extends OncePerRequestFilter {

    private final AdminDetailService adminDetailService;

    public AdminAuthorizationFilter(AdminDetailService adminDetailService) {
        this.adminDetailService = adminDetailService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String authorization = request.getHeader("Authorization");

        if(StringUtil.isEmpty(authorization) || !StringUtil.startsWith(authorization, JwtProperties.PRE_FIX)){
            filterChain.doFilter(request, response);
            return;
        }

        String id = JWT.require(JwtProperties.SIGN)
                .build()
                .verify(StringUtil.remove(authorization, JwtProperties.PRE_FIX))
                .getClaim("id")
                .asString();

        AdminPrincipal principal = (AdminPrincipal) adminDetailService.loadUserByUsername(id);
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(token);
        filterChain.doFilter(request, response);
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        String url = request.getRequestURI();
        return Stream.of("/client/**", "/v1/client/**").anyMatch(x -> new AntPathMatcher().match(x, url));
    }
}
    @Bean
    public AdminAuthorizationFilter adminAuthorizationFilter(){
        return new AdminAuthorizationFilter(adminDetailService());
    }

    @Bean
    public SecurityFilterChain adminFilterChain(HttpSecurity http) throws Exception {
        http
		...
                .addFilter(adminAuthenticationFilter())
                .addFilterBefore(adminAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

또는

package com.web.vt.security;

import com.auth0.jwt.JWT;
import com.web.vt.utils.StringUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class AdminAuthorizationFilter extends OncePerRequestFilter {

    private final AdminDetailService adminDetailService;

    public AdminAuthorizationFilter(AdminDetailService adminDetailService) {
        this.adminDetailService = adminDetailService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String authorization = request.getHeader("Authorization");

        if(StringUtil.isEmpty(authorization) || !StringUtil.startsWith(authorization, JwtProperties.PRE_FIX)){
            filterChain.doFilter(request, response);
            return;
        }

        String id = JWT.require(JwtProperties.SIGN)
                .build()
                .verify(StringUtil.remove(authorization, JwtProperties.PRE_FIX))
                .getClaim("id")
                .asString();

        AdminPrincipal principal = (AdminPrincipal) adminDetailService.loadUserByUsername(id);
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(token);
        filterChain.doFilter(request, response);
    }
}
    @Bean
    public SecurityFilterChain adminFilterChain(HttpSecurity http,
                                                @Qualifier("adminAuthenticationFilter") UserAuthenticationFilter authenticationFilter,
                                                        AdminDetailService detailService,
                                                        JwtUtil jwtUtil) throws Exception {
            http
            ....
            .addFilter(authenticationFilter)
            .addFilterBefore(new UserAuthorizationFilter(detailService, jwtUtil), AuthorizationFilter.class)
            .addFilterAt(new FilterExceptionHandler(), ExceptionTranslationFilter.class);
    
            return http.build();
        }

4. JWT Property(JWT 설정) 환경별로 분리하기

...
spring:
  config:
    activate:
      on-profile: dev
app:
  security:
    jwt:
      secret: dev
      limit: 10
      issuer: localhost:8090
...
...
spring:
  config:
    activate:
      on-profile: local
...
app:
  security:
    jwt:
      secret: local
      limit: 1440
      issuer: localhost:8080

...
@ConfigurationProperties(prefix = "app.security.jwt")
@Getter @Setter
public class JwtProperties {

    private String secret;
    private int limit;
    private String issuer;
    private String prefix = "Bearer ";

    public Instant getExpiredTime(){
        return LocalDateTime.now().plusMinutes(limit).toInstant(ZoneOffset.UTC);
    }

    public Algorithm getSign(){
        return Algorithm.HMAC256(secret);
    }

}
@Configuration
@EnableConfigurationProperties(JwtProperties.class)
public class AppConfiguration {

...

}

5. 테스트

@SpringBootTest
@AutoConfigureMockMvc
@Disabled
@Transactional
@ActiveProfiles("local")
public class ControllerTestSupporter {

    protected MockMvc mvc;
    ...

    @BeforeEach
    void setUp(WebApplicationContext context, RestDocumentationContextProvider provider) {

        mvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity())
                ...
                .build();
    }

}
@WithUserDetails(userDetailsServiceBeanName = "employeeDetailService", value = "test")
class ReservationClientControllerTest extends ControllerTestSupporter {
	...
}

local에서 CORS 설정 테스트하기

테스트용 스크립트

curl -I -X OPTIONS \
  -H "Origin: http://localhost:8090" \
  -H 'Access-Control-Request-Method: GET' \
  -H 'Content-Type: application/json' \
  http://localhost:8080/api/v1/home

결과 예시

curl -I -X OPTIONS \
  -H "Origin: http://localhost:8999" \
  -H 'Access-Control-Request-Method: GET' \
  -H 'Content-Type: application/json' \
  http://localhost:8080/api/v1/home
HTTP/1.1 403
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Transfer-Encoding: chunked
Date: Tue, 25 Jul 2023 07:30:00 GMT
curl -I -X OPTIONS \
  -H "Origin: http://localhost:8090" \
  -H 'Access-Control-Request-Method: GET' \
  -H 'Content-Type: application/json' \
  http://localhost:8080/api/v1/home
HTTP/1.1 200
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Access-Control-Allow-Origin: http://localhost:8090
Access-Control-Allow-Methods: GET
Access-Control-Allow-Credentials: true
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 0
Date: Tue, 25 Jul 2023 07:30:04 GMT