LUMI_dev

접근 불가 페이지 만들기 (권한 제어 @Secured) 본문

스파르타 코딩 클럽 | 자바 심화 과정/Spring Master (숙련 주차)

접근 불가 페이지 만들기 (권한 제어 @Secured)

luminous_dev 2025. 2. 7. 03:58

API 접근 권한 제어 이해

'일반 사용자'는 관리자 페이지에 접속이 인가되지 않아야 함

 

 

1. Spring Security에 "권한 (Authority)" 설정방법

 

 

회원 상세정보 (UserDetailsImpl) 통해 "권한 (Authority)" 설정 가능

권한을 1개 이상 설정 가능

 

"권한 이름" 규칙  → "ROLE_" 로 시작해야 함

 

예) "ADMIN" 권한 부여 → "ROLE_ADMIN" / "USER" 권한 부여 → "ROLE_USER"

 

public enum UserRoleEnum {
    USER(Authority.USER),  // 사용자 권한
    ADMIN(Authority.ADMIN);  // 관리자 권한

    private final String authority;

    UserRoleEnum(String authority) {
        this.authority = authority;
    }

    public String getAuthority() {
        return this.authority;
    }

    public static class Authority {
        public static final String USER = "ROLE_USER";
        public static final String ADMIN = "ROLE_ADMIN";
    }
}

 

UserDetailsImpl 

public class UserDetailsImpl implements UserDetails {
		// ...

		@Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        SimpleGrantedAuthority adminAuthority = new SimpleGrantedAuthority("ROLE_ADMIN");
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(adminAuthority);

        return authorities;
    }
}

위 예시에서는 ROLE_ADMIN으로 고정되어 있지만

실제 코드에서는 사용자에게 저장되어 있는 role의 authority 값을 사용하여 동적 저장 

 

UserDetailsImpl  (실제 코드) 

UserRoleEnum role = user.getRole();
String authority = role.getAuthority();

SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);

 

UserDetailsImpl 저장된 authorites 값을 사용하여 간편하게 권한 제어

User.java / UserRoleEnum.java

 

 

2. Spring Security를 이용한 API 별 권한 제어 방법

Controller 의 @Secured(" 권한 이름 ")

권한 1개 이상 설정 가능 

@Secured(UserRoleEnum.Authority.ADMIN) // 관리자용
@GetMapping("/products/secured")
public String getProductsByAdmin(@AuthenticationPrincipal UserDetailsImpl userDetails) {
    System.out.println("userDetails.getUsername() = " + userDetails.getUsername());
    for (GrantedAuthority authority : userDetails.getAuthorities()) {
        System.out.println("authority.getAuthority() = " + authority.getAuthority());
    }  
    
    return "redirect:/";
}

 

 

@Secured 애너테이션 활성화 방법

WebSecurityConfig에 설정해주기 

@Configuration
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
@EnableMethodSecurity(securedEnabled = true) // @Secured 애너테이션 활성화
public class WebSecurityConfig {

실습) 스프링 시큐리티 설정을 통해 일반 사용자가 '관리자용 상품조회 API' 에 접속 시도 시 접속 불가 페이지가 뜨게 구현

 

ProductController - Secured 테스트 부분 추가 

package com.sparta.springauth.controller;

import com.sparta.springauth.entity.User;
import com.sparta.springauth.entity.UserRoleEnum;
import com.sparta.springauth.security.UserDetailsImpl;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

//Filter에서 user Request 객체에 넣은 거 받아와볼 것임
@Controller
@RequestMapping("/api")
public class ProductController {

    @GetMapping("/products")
    public String getProducts(@AuthenticationPrincipal UserDetailsImpl userDetails) {

        //Authentication의 Principle -> Principle에 UserDetail 들어감 -> 그걸 @AuthenticationPrincipal로 가져옴
        User user = userDetails.getUser(); //UserDetailsServiceImpl에서 UserDetailsImpl에 user을 넘겨서 저장된 user 가져오기
        System.out.println("user.getUsername() = " + user.getUsername());
        System.out.println("user.getEmail() = " + user.getEmail());
        return "redirect:/";
    }

    //Secured 테스트 
    @Secured(UserRoleEnum.Authority.ADMIN) // 관리자용
    @GetMapping("/products/secured")
    public String getProductsByAdmin(@AuthenticationPrincipal UserDetailsImpl userDetails) {
        System.out.println("userDetails.getUsername() = " + userDetails.getUsername());
        for (GrantedAuthority authority : userDetails.getAuthorities()) {
            System.out.println("authority.getAuthority() = " + authority.getAuthority());
        }

        return "redirect:/";
    }
}

 

 

WebSecurityConfig 상단에 다음과 같이 넣어주기 

import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
@EnableMethodSecurity(securedEnabled = true)
public class WebSecurityConfig {

 

 

 

 

Forbidden페이지

resource > static > forbidden.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>접근 불가</title>
        <style>
            html,body{
                margin:0;
                padding:0;
                display:flex;
                justify-content:center;
                align-items:center;
                background-color:salmon;
                font-family:"Quicksand", sans-serif;

            }

            #container_anim{
                position:relative;
                width:100%;
                height:70%;
            }

            #key{
                position:absolute;
                top:77%;
                left:-33%;
            }

            #text{
                font-size:4rem;
                position:absolute;
                top:55%;
                width:100%;
                text-align:center;
            }

            #credit{
                position:absolute;
                bottom:0;
                width:100%;
                text-align:center;
                bottom:
            }

            a{
                color: rgb(115,102,102);
            }
        </style>
        <script>
          var lock = document.querySelector('#lock');
          var key = document.querySelector('#key');


          function keyAnimate(){
            dynamics.animate(key, {
              translateX: 33
            }, {
              type:dynamics.easeInOut,
              duration:500,
              complete:lockAnimate
            })
          }


          function lockAnimate(){
            dynamics.animate(lock, {
              rotateZ:-5,
              scale:0.9
            }, {
              type:dynamics.bounce,
              duration:3000,
              complete:keyAnimate
            })
          }


          setInterval(keyAnimate, 3000);
        </script>
    </head>
    <body>
        <div id="container_anim">
            <div id="lock" class="key-container">
                <?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="317.286 -217 248 354" width="248" height="354"><g><path d="M 354.586 -43 L 549.986 -43 C 558.43 -43 565.286 -36.144 565.286 -27.7 L 565.286 121.7 C 565.286 130.144 558.43 137 549.986 137 L 354.586 137 C 346.141 137 339.286 130.144 339.286 121.7 L 339.286 -27.7 C 339.286 -36.144 346.141 -43 354.586 -43 Z" style="stroke:none;fill:#2D5391;stroke-miterlimit:10;"/><g transform="matrix(-1,0,0,-1,543.786,70)"><text transform="matrix(1,0,0,1,0,234)" style="font-family:'Quicksand';font-weight:700;font-size:234px;font-style:normal;fill:#4a4444;stroke:none;">U</text></g><g transform="matrix(-1,0,0,-1,530.786,65)"><text transform="matrix(1,0,0,1,0,234)" style="font-family:'Quicksand';font-weight:700;font-size:234px;font-style:normal;fill:#8e8383;stroke:none;">U</text></g><path d="M 343.586 -52 L 538.986 -52 C 547.43 -52 554.286 -45.144 554.286 -36.7 L 554.286 112.7 C 554.286 121.144 547.43 128 538.986 128 L 343.586 128 C 335.141 128 328.286 121.144 328.286 112.7 L 328.286 -36.7 C 328.286 -45.144 335.141 -52 343.586 -52 Z" style="stroke:none;fill:#4A86E8;stroke-miterlimit:10;"/><g><circle vector-effect="non-scaling-stroke" cx="441.28571428571433" cy="63.46153846153848" r="10.461538461538453" fill="rgb(0,0,0)"/><rect x="436.055" y="66.538" width="10.462" height="34.462" transform="matrix(1,0,0,1,0,0)" fill="rgb(0,0,0)"/></g></g></svg>
            </div>

            <div id="key">
                <?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="232.612 288.821 169.348 109.179" width="169.348" height="109.179"><g><path d=" M 382.96 349.821 L 368.96 349.821 L 368.96 314.821 L 382.96 307.821 L 382.96 349.821 Z " fill="rgb(55,49,49)"/><path d=" M 292.134 354.827 L 379.96 315.39 L 379.96 305.547 L 292.134 343.094 L 292.134 354.827 Z " fill="rgb(55,49,49)"/><path d=" M 280.96 340.109 L 401.96 288.821 L 401.96 340.109 L 382.96 349.972 L 382.96 308.547 L 265.96 360.821 L 259.96 349.972 L 280.96 340.109 Z " fill="rgb(115,102,102)"/><path d=" M 401.96 288.821 L 382.96 288.821 L 280.96 332.821 L 292.134 340.109 L 401.96 288.821 Z " fill="rgb(115,102,102)"/><g><path d=" M 232.755 354.125 C 230.958 328.501 246.297 306.519 266.988 305.068 C 287.679 303.617 305.937 323.243 307.734 348.867 C 309.531 374.492 294.191 396.473 273.5 397.924 C 252.809 399.375 234.552 379.75 232.755 354.125 Z " fill="rgb(55,49,49)"/><path d=" M 239.241 352.316 C 237.564 328.406 252.144 307.876 271.779 306.499 C 291.414 305.122 308.716 323.416 310.393 347.326 C 312.07 371.236 297.49 391.766 277.855 393.143 C 258.22 394.52 240.917 376.226 239.241 352.316 Z " fill="rgb(115,102,102)"/><path d=" M 260.038 353.084 C 259.196 348.171 261.788 343.621 265.822 342.929 C 269.856 342.238 273.816 345.665 274.658 350.578 C 275.5 355.49 272.909 360.041 268.874 360.732 C 264.84 361.424 260.88 357.997 260.038 353.084 Z " fill="salmon"/></g></g></svg>
            </div>
        </div>

        <p id="text">403 FORBIDDEN</p>
        <p id="credit">사용자 접근 불가 페이지입니다.</a></p>
    </body>
</html>

 

 

이제 튕겨져 나갔을 때 fobidden.html이 뿌려지는 것 처리해야 함

 

WebSecurityConfig 

//접근 불가 페이지 처리
http.exceptionHandling((exceptionHandling) ->
        exceptionHandling
                .accessDeniedPage("/forbidden.html") //accessDenied 당했을 때 어떤 페이지로 보낼지 설정하는 곳
);

 

 

package com.sparta.springauth.config;

import com.sparta.springauth.jwt.JwtAuthorizationFilter;
import com.sparta.springauth.jwt.JwtAuthenticationFilter;
import com.sparta.springauth.jwt.JwtUtil;
import com.sparta.springauth.security.UserDetailsServiceImpl;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
@EnableMethodSecurity(securedEnabled = true)
public class WebSecurityConfig {

    private final JwtUtil jwtUtil; //filter에 넣어야해서 주입
    private final UserDetailsServiceImpl userDetailsService; //filter에 넣어야해서 주입
    private final AuthenticationConfiguration authenticationConfiguration;

    public WebSecurityConfig(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService, AuthenticationConfiguration authenticationConfiguration) {
        this.jwtUtil = jwtUtil;
        this.userDetailsService = userDetailsService;
        this.authenticationConfiguration = authenticationConfiguration;
    }

    //AuthenticationManager 만들기 - Bean으로 등록
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    //인증 Filter 등록 + Bean으로 등록하는 메서드
    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        //인증 Filter 생성
        JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);

        //JwtAuthenticationFilter에서 getAuthenticationManager()로 가져오므로 set도 해줘야 함
        filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
        //AuthenticationManager만드는 메서드에 먼저 넣고 그 값을 set해주기
        return filter;
    }

    //AuthorizationFilter
    @Bean
    public JwtAuthorizationFilter jwtAuthorizationFilter() {
        return new JwtAuthorizationFilter(jwtUtil, userDetailsService);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf((csrf) -> csrf.disable());

        // 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
        http.sessionManagement((sessionManagement) ->
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        );

        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                        .requestMatchers("/api/user/**").permitAll() // '/api/user/'로 시작하는 요청 모두 접근 허가
                        .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );

        http.formLogin((formLogin) ->
                formLogin
                        .loginPage("/api/user/login-page").permitAll()
        );


        //여기까지해도 Filter만 만들어준 거임 =  SecurityFilterChain에 넣어주지 않았기 때문에 끝난게 아님
        //-> SecurityFilter안에 끼워넣어줘야 함 (어떤 순서에 언제 끼워넣을건지 말해주지 않음)
        //아래 두개의 코드가 관련된 내용임

        // 필터 관리
        http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); //UsernamePasswordAuthenticationFilter : username과 password 확인해서 인증처리
        //.addFilterBefore 해당 filter 전에 내가 넣을 거다 (UsernamePasswordAuthenticationFilter 전에 jwtAuthenticationFilter를 먼저 수행하겠습니다)
        //로그인을 하기 전에 인가하고 인가가 제대로 되지 않았으면 로그인하는 게 올바른 순서라서 jwtAuthenticationFilter가 먼저


        //접근 불가 페이지 처리
        http.exceptionHandling((exceptionHandling) ->
                exceptionHandling
                        .accessDeniedPage("/forbidden.html") //accessDenied 당했을 때 어떤 페이지로 보낼지 설정하는 곳
        );


        return http.build();
    }
}