Authorization

2024. 1. 30. 10:49
728x90

1. GrantedAuthority

: 사용자가 누구인지 인증되면 사용자에 따라 접근할 수 있는 서비스를 나눌 수 있다!

== UserDetails가 나타내는 사용자가 가진 권한을 묘사하는 interface.

 

- userDetails.getAuthorities() 를 보면 반환값이 Collection<? extends GrantedAuthority>이다.

> GrantedAuthority는 인터페이스로 Authentication 객체에 주어진 권한을 보여준다. 

 

-Spring Security 내부에서는 보통 String 형식으로 권한을 표현하는 SimpleGrantedAuthority를 사용한다. 

 

1) 사용자 권한 표현

- UserEntity 객체에 Column 형식으로 부여해준다.

// UserEntity class

@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String username;
    @Column(nullable = false)
    private String password;

    private String email;
    private String phone;

	// 권한 (문자열 하나에 ','로 구분해 권한을 묘사)
    // ROLE_USER, ROLE_ADMIN, READ_AUTHORITY, WRITE_AUTHORITY
    private String authorities;
}

 

- CustomUserDetails에도 추가해준다.

+ getAuthorities() 메서드 내부 구현 : String으로 주어진 권한들을 ',' 기준으로 나누어 SimpleGrantedAuthority의 리스트 형태로 반환.

+ getRawAuthorities() (즉, getter 생성) : 이름이 authorities로 같기 때문에 이름을 다르게 getter를 만들어주어야 한다.

// CustomUserDetails class
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CustomUserDetails implements UserDetails {

    @Getter
    private Long id;
    private String username;
    private String password;
    @Getter
    private String email;
    @Getter
    private String phone;

    private String authorities;
    
    public String getRawAuthorities() {
        return this.authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
//        String authorities = "ROLE_USER, ROLE_ADMIN, READ_AUTHORITY, WRITE_AUTHORITY";
        String[] authorityArray = authorities.split(", ");
        List<SimpleGrantedAuthority> authorityList = new ArrayList<>();

        for (String s : authorityArray) {
            authorityList.add(new SimpleGrantedAuthority(s));
        }
        return authorityList;
    }

//...
}

 

- CustomUserDetailsManager에서도 생성시 권한을 함께 저장한다.

@Slf4j
@Service
public class CustomUserDetailsManager implements UserDetailsManager {
    private final UserRepository userRepository;

    public CustomUserDetailsManager(UserRepository userRepository,
                                    PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        
        // ROLE_USER
        CustomUserDetails userDetails = CustomUserDetails.builder()
                .id(1L)
                .username("user1")
                .password(passwordEncoder.encode("password1"))
                .email("email@naver.com")
                .phone("01011112222")
                .authorities("ROLE_USER")
                .build();
        createUser(userDetails);

		// ROLE_USER, ROLE_ADMIN 
        CustomUserDetails userDetails2 = CustomUserDetails.builder()
                .username("admin")
                .password(passwordEncoder.encode("password1"))
                .email("email@gmail.com")
                .phone("01012341234")
                .authorities("ROLE_USER, ROLE_ADMIN")
                .build();
        createUser(userDetails2);
    }
    
	@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // repository에서 username으로 찾아오기
        Optional<UserEntity> optionalUser = userRepository.findByUsername(username);
        // username이 없다면 예외처리
        if (optionalUser.isEmpty()) {
            throw new UsernameNotFoundException("username not found");
        }

        // 있다면 엔티티 가져오기
        UserEntity userEntity = optionalUser.get();
        // 리턴을 UserDetails로 해야하기 때문에 UserDetails 형태로 바꿔주기

        UserDetails userDetails = CustomUserDetails.builder()
                .id(userEntity.getId())
                .username(userEntity.getUsername())
                .password(userEntity.getPassword())
                .email(userEntity.getEmail())
                .phone(userEntity.getPhone())
                .authorities(userEntity.getAuthorities())
                .build();
        return userDetails;
    }
    // ...
}

 

이렇게 해서 

admin의 jwt 토큰을 발급받고 header에 넣어주며 my-profile로 가면 된다.

>> 이렇게 하면 결과가 admin으로 나오고, 이 사용자의 authorities 까지 잘 나온다.

 

2) 권한의 종류

- Role : 사용자가 어떤 계급, 또는 역할의 사용자인지 나타냄 (사용자, 관리자 등)

- Authority : 역할이나 계급과 상관없이 특정 작업을 할 수 있는 권한을 나타냄 (게시글 작성 권한, 댓글 작성 권한 등)

- GrantedAuthority.getAuthority() 반환이 : ROLE_(prefix)로 시작하면 role, 아니면 authority로 취급. 

+ 미인증(로그인 안한) 사용자 : ROLE_ANONYMOUS

 

3) Authentication에 권한 부여

- Spring Security가 제공하는 인증 방식을 사용하고 있다면 > Authentication에 자동으로 사용자 권한이 설정된다. 

- Filter를 사용해 직접 Authentication을 설정한다면 > Authentication에도 사용자 권한을 설정해주어야 한다.

 

>> 아래에서 UsernamePasswordAuthenticationToken에서 계속 new ArrayList로 설정해두었던 authorities 부분을 바꿔주었다. 

// JwtTokenFilter

@Slf4j
@RequiredArgsConstructor
public class JwtTokenFilter extends OncePerRequestFilter {
    //토큰을 확인하는 메서드 (JwtTokenUtils.validate())를 위해
    private final JwtTokenUtils jwtTokenUtils;
    private final UserDetailsManager manager;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {
        // 1. authorization 헤더를 회수
//        String authHeader = request.getHeader("Authorization");
        String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);

        // 2. 헤더가 존재하는지, Bearer로 시작하는지 확인
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            // 3. 존재하면 유효한 토큰인지
            // 헤더에서 Bearer 뒤의 토큰만 가져오기
            String token = authHeader.split(" ")[1];
            // 토큰을 확인하는 메서드 (JwtTokenUtils.validate()) boolean값 반환
            if (jwtTokenUtils.validate(token)) {
                // 4. 유효하다면 해당 토큰을 바탕으로 사용자 정보를 SecurityContext에 등록
                // contextholder에서 context 만들고,
                SecurityContext context = SecurityContextHolder.createEmptyContext();
                // 토큰에서 사용자 정보 가져오기
                String username = jwtTokenUtils.parseClaims(token).getSubject();

                UserDetails userDetails = manager.loadUserByUsername(username);
                for (GrantedAuthority authority : userDetails.getAuthorities()) {
                    log.info("authority : {}", authority.getAuthority());
                }

                // 인증 정보 생성
                AbstractAuthenticationToken authenticationToken =
                        new UsernamePasswordAuthenticationToken(
                                //principal : UserDetailsManager에서 실제 사용자 정보 가져오기
                                userDetails,
                                // credentials : token
                                token,
                                // authorities
                                userDetails.getAuthorities()
                        );
                // 인증 정보 등록
                context.setAuthentication(authenticationToken);
                SecurityContextHolder.setContext(context);
                log.info("=== set security context with jwt");
            } else {
                log.warn("jwt validation failed!!");
            }
        }
        // 5. 다음 필터 호출
        filterChain.doFilter(request, response);
    }
}

 

2. 권한에 따른 접근 제어

- AuthorizationController를 만들어 준다.

// AuthorizationController

@Slf4j
@RestController
@RequestMapping("/auth")
public class AuthorizationController {

    // ROLE_USER를 가졌을 때 요청 가능
    @GetMapping("/user-role")
    public String userRole() {
        return "userRole";
    }

    // ROLE_ADMIN를 가졌을 때 요청 가능
    @GetMapping("/admin-role")
    public String adminRole() {
        return "adminRole";
    }
}

1) Role에 따른 접근 제어

- USER role 필요 vs. ADMIN role 필요

- hasAnyRole() : 주어진 인자 중 하나라도

- hasRole() : 주어진 인자를 정확하게 

- 앞의 ROLE_은 생략하고 작성한다.

 

- 이것을 제어하기 위해 WebSecurityConfig에서 설정해준다.

// WebSecurityConfig class

@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {
    private final JwtTokenUtils jwtTokenUtils;
    private final UserDetailsManager manager;

    @Bean
    public SecurityFilterChain securityFilterChain(
            HttpSecurity http
    ) throws Exception {
        http
                // csrf 보안 해제
                .csrf(AbstractHttpConfigurer::disable)
                // url에 따른 요청 인가
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(
                                "/no-auth",
                                "/users/home",
                                "/tests",
                                "/token/validate",
                                "/token/issue"
                        )
                        .permitAll()
                        .requestMatchers("/users/my-profile")
                        .authenticated()
                        
                        // 여기부터
                        .requestMatchers("/auth/user-role")
                        .hasRole("USER")
                        .requestMatchers("/auth/admin-role")
                        .hasRole("ADMIN")
                        // 여기까지 추가
                        
                        .requestMatchers("/users/login",
                                "/users/register")
                        .anonymous()
                        .anyRequest().permitAll()
                )
                // JWT를 사용하기 때문에 보안 관련 세션 해제
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                // JWT 필터를 권한 필터 앞에 삽입
                .addFilterBefore(new JwtTokenFilter(jwtTokenUtils, manager),
                        AuthorizationFilter.class)
        ;
        return http.build();
    }
}

 

 

그러면 결과는 user로 토큰을 발급받으면 admin-role에는 접속 불가, 

admin으로 토큰을 발급받으면 admin-role, user-role에 둘다 접속 가능하게 나온다.

admin 토큰 발급시 

 

user 토큰 발급시

 

 

2) Authority에 따른 접근 제어

- WRITE_AUTHORITY authority 필요 vs. READ_AUTHORITY authority 필요

- hasAnyAuthority() : 주어진 인자 중 하나라도

- hasAuthority() : 주어진 인자를 정확하게

- 권한 이름을 전부 적어야 한다.

 

- 사실 위와 같은 방식으로 진행하면 된다.

// AuthorizationController 
    @GetMapping("/read-authority")
    public String readAuth() {
        return "able to read";
    }

    @GetMapping("/write-authority")
    public String writeAuth() {
        return "able to write";
    }
    
// CustomUserDetailsManager 생성자
    public CustomUserDetailsManager(UserRepository userRepository,
                                    PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        CustomUserDetails userDetails = CustomUserDetails.builder()
                .id(1L)
                .username("user1")
                .password(passwordEncoder.encode("password1"))
                .email("email@naver.com")
                .phone("01011112222")
                .authorities("ROLE_USER, READ_AUTHORITY")
                .build();
        createUser(userDetails);

        CustomUserDetails userDetails2 = CustomUserDetails.builder()
                .username("admin")
                .password(passwordEncoder.encode("password1"))
                .email("email@gmail.com")
                .phone("01012341234")
                .authorities("ROLE_USER, ROLE_ADMIN, WRITE_AUTHORITY")
                .build();
        createUser(userDetails2);
    }

// WebSecurityConfig securityFilterChain에 추가
                        .requestMatchers("/auth/read-authority")
                        .hasAuthority("READ_AUTHORITY")
                        .requestMatchers("/auth/write-authority")
                        .hasAuthority("WRITE_AUTHORITY")

 

>> 결과는 user1일 때는 read-authority 접속 가능/ write-authority 접속 불가능

admin일 때는 read-authority 접속 불가능/ write-authority 접속 가능

user 토큰 발급시 

 

admin 토큰 발급시

 

 

728x90

'Programming > Spring, SpringBoot' 카테고리의 다른 글

OAuth2 - Kakao Login 구현  (1) 2024.01.30
OAuth2 - 소셜 로그인  (0) 2024.01.30
JWT  (1) 2024.01.29
HandlerInterceptor & Filter / SecurityFilterChain  (0) 2024.01.27
Spring의 Dispatcher Servlet  (3) 2024.01.26

BELATED ARTICLES

more