๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

Back-end/JAVA & Spring

[ Spring Security ] ๋™์  URL ์ ‘๊ทผ ๊ถŒํ•œ ์ œ์–ด ๊ตฌํ˜„

 

 

 

 

 

 

๐ŸŒฑ Spring Security: ๋™์  URL ์ ‘๊ทผ ๊ถŒํ•œ ์ œ์–ด ๊ตฌํ˜„

 

๊ฐœ์š”

Spring Security๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์ •์  ๋ฐฉ์‹์˜ URL ์ ‘๊ทผ ์ œ์–ด๋ฅผ ์ œ๊ณตํ•œ๋‹ค. ํ•˜์ง€๋งŒ ์‹ค๋ฌด์—์„œ๋Š” ๊ถŒํ•œ ์ •์ฑ…์ด ์ž์ฃผ ๋ณ€๊ฒฝ๋˜๊ฑฐ๋‚˜, URL ์ ‘๊ทผ ๊ถŒํ•œ์„ ๊ด€๋ฆฌ ํ™”๋ฉด์—์„œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ œ์–ดํ•ด์•ผ ํ•˜๋Š” ์š”๊ตฌ๊ฐ€ ๋งŽ๋‹ค.

 

"GUEST ์‚ฌ์šฉ์ž์— ๋Œ€ํ•œ ์ ‘๊ทผ ๊ฐ€๋Šฅ URL"์„ DB์—์„œ ๋™์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ , ์ด๋ฅผ Spring Security์— ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜ ๋  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ•œ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•  ์˜ˆ์ •์ด๋‹ค.

 

 

 

ํ•ต์‹ฌ ๋ชฉํ‘œ

  • GUEST(๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž)์˜ ์ ‘๊ทผ URL์„ DB์—์„œ ๊ด€๋ฆฌ
  • Spring Security์˜ ์ธ์ฆ ํ•„ํ„ฐ ์ด์ „์— ๋™์ ์œผ๋กœ URL ์ ‘๊ทผ ์—ฌ๋ถ€ ํŒ๋‹จ
  • ๊ถŒํ•œ ์ •๋ณด๋Š” ์บ์‹ฑํ•˜์—ฌ ์„ฑ๋Šฅ ์ตœ์ ํ™”
  • ์žฌ๊ธฐ๋™ ์—†์ด ์ •์ฑ… ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ

 

 

๊ธฐ์ˆ  ๊ตฌ์„ฑ

  • Spring boot 3.x
  • Spring Security 5.5 ์ด์ƒ
  • Java 17
  • @Scheduled (์บ์‹œ ๊ฐฑ์‹ )

 

 

์‹œ์Šคํ…œ ๊ตฌ์กฐ ์š”์•ฝ

DB ํ…Œ์ด๋ธ” ๊ตฌ์„ฑ

  • roles: ๊ถŒํ•œ ์ด๋ฆ„, ์‚ญ์ œ์—ฌ๋ถ€ ๋“ฑ
  • menus: URL, ๋ฉ”๋‰ด ์ด๋ฆ„, ํ™œ์„ฑ์—ฌ๋ถ€ ๋“ฑ
  • menu_role: ๋ฉ”๋‰ด์™€ ๊ถŒํ•œ์˜ ๋งคํ•‘

์ „์ฒด ํ๋ฆ„

1. ์‚ฌ์šฉ์ž ์š”์ฒญ

2. Spring Security์—์„œ AuthorizationManager๋กœ ์œ„์ž„

3. AuthorizationManager์—์„œ ์บ์‹œ๋œ guest URL ๋ชฉ๋ก๊ณผ ๋น„๊ต

4. ํ—ˆ์šฉ ์—ฌ๋ถ€ ๊ฒฐ์ • 

 

 

 

 

๊ตฌํ˜„ ์ƒ์„ธ

 

1. Spring boot ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ

https://euntry.tistory.com/53

 

[ VScode ] vscode์—์„œ java spring boot ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ

๐Ÿ”Ž VScode์—์„œ JAVA spring boot project ์ƒ์„ฑ JAVA ํ™•์žฅํŒฉ(Extensions) ์„ค์น˜- Extension Pack for Java- Spring Boot Extension Pack https://euntry.tistory.com/47 [ VScode ] JAVA ๊ฐœ๋ฐœ์„ ์œ„ํ•œ Extensions ์ถ”์ฒœ (OS Window)๐Ÿ”Ž VScode JAVA ํ”Œ๋Ÿฌ

euntry.tistory.com

 

 

 

2. build.gradle (Spring boot 3.x ๊ธฐ์ค€)

 

dependencies {
    // Spring Boot Web
    implementation 'org.springframework.boot:spring-boot-starter-web'

    // Spring Security
    implementation 'org.springframework.boot:spring-boot-starter-security'
    // Spring Security์—์„œ AuthorizationManager ์‚ฌ์šฉ ๊ฐ€๋Šฅ
    // (Security 5.5 ์ด์ƒ๋ถ€ํ„ฐ ์‚ฌ์šฉ ๊ฐ€๋Šฅ)

    // ์Šค์ผ€์ค„๋ง (@Scheduled)
    implementation 'org.springframework.boot:spring-boot-starter'

    // Lombok
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
}

 

 

 

 

3. ์Šค์ผ€์ค„๋ง ํ™œ์„ฑํ™”

@EnableScheduling ์–ด๋…ธํ…Œ์ด์…˜ ์ถ”๊ฐ€

 

@SpringBootApplication
@EnableScheduling
public class YourApplication {
    public static void main(String[] args) {
        SpringApplication.run(YourApplication.class, args);
    }
}

 

"@EnableScheduling"์€ Spring์—๊ฒŒ ์Šค์ผ€์ค„๋ง ๊ธฐ๋Šฅ์„ ํ™œ์„ฑํ™”ํ•˜๋ผ๊ณ  ์ง€์‹œํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜์ด๋‹ค.

Spring boot ํ”„๋กœ์ ํŠธ์—์„œ ์ด ์˜ค๋…ธํ…Œ์ด์…˜์€ ๋ช…์‹œ์ ์œผ๋กœ ์ถ”๊ฐ€ํ•ด์•ผ ํ•˜๋ฉฐ, ์ž๋™ ํ™œ์„ฑํ™”๋˜์ง€ ์•Š๋Š”๋‹ค.

 

 

 

4. GUEST ์ ‘๊ทผ ๊ฐ€๋Šฅ URL ์บ์‹œ ์„ค์ •

"@Cacheable"์„ ์‚ฌ์šฉํ•ด GUEST๊ฐ€ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ URL์„ ์บ์‹ฑํ•œ๋‹ค. DB๋ฅผ ๋งค๋ฒˆ ์กฐํšŒํ•˜์ง€ ์•Š์•„ ์„ฑ๋Šฅ์ ์œผ๋กœ ์œ ๋ฆฌ

 

@Cacheable(value = CACHE_KEY.SECURITY, key = "'guestUrls'", unless = "#result == null or #result.isEmpty()")
public List<String> getGuestAccessibleUrls() {
    log.info(">>> DB์—์„œ GUEST_ROLE ์ ‘๊ทผ ๊ฐ€๋Šฅ URL ์กฐํšŒ");

    return menuRoleRepository.findByRole_RoleSeqAndMenu_ActiveYnAndDelYn(ROLE_KEY.GUEST, ActiveYn.Y, DelYn.N)
        .stream()
        .map(menuRole -> converToWildcardpattern(menuRole.getMenu().getMenuUrl()))
        .filter(Objects::nonNull)
        .distinct()
        .collect(Collectors.toList());
}

@CacheEvict(value = CACHE_KEY.SECURITY, key = "'guestUrls'")
public void refreshGuestAccessibleUrls() {
    // ๋ณ„๋„ ๋กœ์ง ๋ถˆํ•„์š”, ์บ์‹œ๋งŒ ์ œ๊ฑฐ๋จ → ๋‹ค์Œ ํ˜ธ์ถœ ์‹œ ์ž๋™ ์žฌ๋กœ๋”ฉ
}

private String converToWildcardpattern(String url) {
    // RESTful ๊ฒฝ๋กœ ๋ณ€์ˆ˜({...})๋ฅผ *๋กœ ๋ณ€ํ™˜ํ•ด URL ํŒจํ„ด ์ •๊ทœํ™”
    return StringUtils.hasText(url) ? url.replaceAll("\\{[^}]+\\}", "*") : url;
}

 

 

 

 

5. ์บ์‹œ ๊ฐฑ์‹  ์Šค์ผ€์ค„๋Ÿฌ

 

@Component
@RequiredArgsConstructor
@Slf4j
public class PermitAllCacheRefresher {

    private final MenuRoleService menuRoleService;

    @Scheduled(fixedRate = 600000) // 10๋ถ„ ๊ฐ„๊ฒฉ ์‹คํ–‰
    public void refreshPermitAllUrls() {
        //log.info("GUEST ROLE ์ ‘๊ทผ ๊ฐ€๋Šฅ URL ์บ์‹œ ๊ฐฑ์‹ ");
        menuRoleService.refreshGuestAccessibleUrls();
    }
}

 

 

 

 

6. GuestUrlAuthorizationManager ๊ตฌํ˜„

 

@Component
@RequiredArgsConstructor
public class GuestUrlAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    private final MenuRoleService menuRoleService;

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
        HttpServletRequest request = context.getRequest();
        String requestURI = request.getRequestURI();

        List<String> guestUrls = menuRoleService.getGuestAccessibleUrls();

        boolean isGuestUrl = guestUrls.stream()
                                        .filter(StringUtils::hasText)
                                        .anyMatch(pattern -> requestURI.matches(convertToRegex(pattern)));

        Authentication auth = authentication.get();

        // ์ธ์ฆ๋˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๋งŒ guest URL ์ ‘๊ทผ ํ—ˆ์šฉ
        boolean isGuest = !auth.isAuthenticated() || auth instanceof AnonymousAuthenticationToken;

        if (isGuest && isGuestUrl) {
            return new AuthorizationDecision(true);
        }

        // ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋Š” ํ†ต๊ณผ (์ธ์ฆ ์—ฌ๋ถ€๋งŒ ํ™•์ธ)
        if (auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken)) {
            return new AuthorizationDecision(true); // ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž → ํ†ต๊ณผ
        }

        return new AuthorizationDecision(false); // ๋‚˜๋จธ์ง€๋Š” ์ฐจ๋‹จ
    }

    private String convertToRegex(String pattern) {
        // Spring URL ํŒจํ„ด์„ ์ •๊ทœ์‹์œผ๋กœ ๋ณ€ํ™˜
        String regex = pattern
                .replace(".", "\\.")
                .replace("**", ".*")
                .replace("*", "[^/]*");
        return "^" + regex + "$";
    }
}

 

 

 

 

7. Spring Security ์„ค์ •

 

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, GuestUrlAuthorizationManager guestAuthManager) throws Exception {
    http
        .csrf(AbstractHttpConfigurer::disable)
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/css/**", "/images/**", "/js/**", "/error/**").permitAll()
            .anyRequest().access(guestAuthManager)
        )
        .formLogin(...)
        .logout(...)
        .sessionManagement(...);

    return http.build();
}

 

 

 

 


 

์ •๋ฆฌ ๋ฐ ํšจ๊ณผ

  • DB์—์„œ ์ง์ ‘ URL ์ ‘๊ทผ ์ •์ฑ…์„ ๊ด€๋ฆฌ → ์šด์˜์ž๊ฐ€ ์ง์ ‘ ์ œ์–ด ๊ฐ€๋Šฅ
  • ์„œ๋ฒ„ ์žฌ๊ธฐ๋™ ์—†์ด ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜ ๊ฐ€๋Šฅ (@CacheEvict + @Scheduled) → ์šด์šฉ ํšจ์œจ ↑
  • ๋ณด์•ˆ ์ •์ฑ…์ด ์ฝ”๋“œ์™€ ๋ถ„๋ฆฌ๋˜์–ด ์œ ์ง€๋ณด์ˆ˜ ๊ฐ„ํŽธ
  • AuthorizationManager๋ฅผ ํ†ตํ•ด ์—ญํ•  ๊ธฐ๋ฐ˜ ์ œ์–ด ํ™•์žฅ ๊ฐ€๋Šฅ