[Spring Security] 폼 기반 인증 및 인가 구현

lkdcode·2023년 12월 4일
0

SpringBoot

목록 보기
4/4
post-thumbnail

스프링 시큐리티로 인증&인가 구현하기
유효한 계정인지?
해당 요청에 대한 권한이 있는지?

💡 클라이언트 요청 시 해당 유저에게 권한이 있는지 확인하기
1. 로그인한 유저는 해당 게시글을 조회할 수 있는 권한이 있는가?
2. 먼저, 로그인을 한 유저가 맞는가?
3. 로그인(인증) 해당 게시글의 조회 권한(인가)을 구현해보자.

관리자 계정과 일반 유저 계정으로 나눈다. [USERS, ADMIN]
일반 유저 계정은 등급이 나뉜다. [BRONZE, SILVER, GOLD]

🎯 요청 테스트를 위한 컨트롤러

테스트를 위해 총 3개의 Api 가 있으며 시나리오는 아래와 같다.

/security/all : 모든 유저가 접근할 수 있다. (다만, 로그인을 해야함.)
/security/admin : 관리 계정만 접근할 수 있다. (ADMIN)
/security/gold : 골드 등급의 계정만 접근할 수 있다. (GOLD)

@RestController
@RequestMapping("/security")
public class SecurityApiController {

    @GetMapping("/all")
    public Map<String, String> getInformation() {
        return Map.of(
                "data1", "모두가 볼 수 있는 정보1"
                , "data2", "모두가 볼 수 있는 정보2"
                , "data3", "모두가 볼 수 있는 정보3"
        );
    }

    @GetMapping("/admin")
    public Map<String, String> getInformationOnlyAdmin() {
        return Map.of(
                "data1", "ADMIN만 볼 수 있는 정보1"
                , "data2", "ADMIN만 볼 수 있는 정보2"
                , "data3", "ADMIN만 볼 수 있는 정보3"
        );
    }

    @GetMapping("/gold")
    public Map<String, String> getDTOOnlyGold() {
        return Map.of(
                "data1", "GOLD 만 볼 수 있는 정보1"
                , "data2", "GOLD 만 볼 수 있는 정보2"
                , "data3", "GOLD 만 볼 수 있는 정보3"
        );
    }
}
PostMan 테스트 결과

현재 모든 요청이 접근 가능하다.

🎯 Security dependencies

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.14'
    id 'io.spring.dependency-management' version '1.1.3'
}

...

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-security'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

의존성 추가 이후 애플리케이션 실행 시 모든 엔드포인트의 호출은 막히게 된다.

이는 개발자를 대신해 스프링 시큐리티가 즉시 사용 가능한 OOTB(out-of-the-box 즉시 사용 가능한) 기능을 제공하기 때문에 엔드포인트 접근 시 401 Unauthorized 응답을 받는다.

스프링 시큐리티는 아무런 설정 없이 사용할 때 모든 수준에서 최대의 보안이 기본값 을 채택한다.
이는 개발자의 별다른 작업 없이도 프로젝트에 스프링 시큐리티가 포함되어 있다면 애플리케이션에 보안 목표가 있음을 뜻한다.

스프링 부트+시큐리티 자동 설정은 상당한 수의 필수적인 빈을 생성한다.
(사용자 ID 와 비밀번호를 이용하는 사용자 인가와 폼 인증을 기반으로 한 기본 보안 기능을 구현하기 위해서)

브라우저로 접속하게 되면 기본 로그인 화면으로 이동하게 되는데,
스프링 부트 애플리케이션의 로깅에 Using generated security password: '비밀번호''비밀번호'를 복사한 후
패스워드를 입력해 접속해보자.
자동 생성되는 기본 계정의 Id는 user
로그인 후 요청시 모든 요청은 성공한다.
패스워드는 애플리케이션이 시작될 때마다 새로 생성된다.

시큐리티 의존성 추가만으로 최대의 보안 효과를 얻었지만,
자동 생성되는 단 하나의 유저와 비밀번호를 모든 이용자가 공유할 수는 없다.

단 하나의 계정을 모든 이용자가 공유한다면
책임과 인증의 보안 원칙이 위배되며
이용하는 유저를 고유하게 증명할 수 없다.

설정을 통해 회원가입한 유저들의 요청만 인증&인가를 처리한다.

🎯 UserDetailsService

스프링 시큐리티 인증 기능의 핵심이다.
단일 메서드가 존재하는 인터페이스이며 해당 메서드를 통해 사용자의 정보들을 얻는다.

package org.springframework.security.core.userdetails;

public interface UserDetailsService {

	/**
	 * Locates the user based on the username. In the actual implementation, the search
	 * may possibly be case sensitive, or case insensitive depending on how the
	 * implementation instance is configured. In this case, the <code>UserDetails</code>
	 * object that comes back may have a username that is of a different case than what
	 * was actually requested..
	 * @param username the username identifying the user whose data is required.
	 * @return a fully populated user record (never <code>null</code>)
	 * @throws UsernameNotFoundException if the user could not be found or the user has no
	 * GrantedAuthority
	 */
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

}

UserDetails 를 반환하기만 하면 되기 때문에 기본 구현의 세부 정보를 알 필요가 없다.

🎯 UserDetails

User Entity 를 구현체로 설정해보자.

@Getter
@NoArgsConstructor(access = AccessLevel.PACKAGE)
@Entity
public class Users implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @JsonProperty(access = READ_WRITE)
    @Column(nullable = false, unique = true)
    private String email;
    @JsonProperty(access = WRITE_ONLY)
    @Column(nullable = false)
    private String password;

    @JsonProperty(access = WRITE_ONLY)
    @Column(nullable = false)
    private String role;

    @JsonProperty(access = WRITE_ONLY)
    @Column(nullable = false)
    private String grade;

    @Builder
    public Users(Long id, String email, String password, String role, String grade) {
        this.id = id;
        this.email = email;
        this.password = password;
        this.role = role;
        this.grade = grade;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority(this.role), new SimpleGrantedAuthority(this.grade));
    }

    @JsonProperty(access = WRITE_ONLY)
    @Override
    public String getUsername() {
        return this.email;
    }

    @JsonProperty(access = WRITE_ONLY)
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @JsonProperty(access = WRITE_ONLY)
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @JsonProperty(access = WRITE_ONLY)
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @JsonProperty(access = WRITE_ONLY)
    @Override
    public boolean isEnabled() {
        return true;
    }
}

기본 엔티티에 rolegrade 는 권한이다.

  • role : USERS or ADMIN
  • grade : BRONZE or SILVER or GOLD

오라이딩된 메서드들은 다음과 같다.

  • getAuthorities() : 계정이 가지고 있는 권한 목록 리턴
  • getUsername() : 계정의 이름을 리턴. (식별자)
  • isAccountNonExpired() : 계정이 만료됐는지? (true는 만료되지 않음)
  • isAccountNonLocked() : 계정이 잠겼는지? (true는 잠기지 않음)
  • isCredentialsNonExpired() : 비밀번호가 만료됐는지? (true는 만료되지 않음)
  • isEnabled() : 계정이 활성화됐는지? (true는 활성화 상태)

🎯 이용자들

시나리오를 짚고 넘어간다.

  1. 회원가입된 이용자만 접근이 가능하다.
  2. 권한을 살피기전, 유효한 이용자인지 아닌지 알아야한다.
  3. 회원가입 리스트는 new InMemoryUserDetailsManager('이용자1', '이용자2'...); 에 저장한다.
  4. 권한에 따라 응답한다.

총 두명의 이용자가 있다.

Users lkdcode = Users.builder()
	.email("lkdcode@email.com")
    .password(passwordEncoder.encode("password123"))
    .grade("GOLD")
    .role("ADMIN")
    .build();

Users another = Users.builder()
	.email("another@email.com")
    .password(passwordEncoder.encode("password123"))
    .grade("SILVER")
    .role("USERS")
    .build();
  • lkdcode : ADMINGOLD 권한을 가지고 있다.
  • another : USERSSILVER 권한을 가지고 있다.

🎯 SpringSecurity 설정용 클래스

테스트를 위해 InMemoryUserDetailsManager(lkdcode, another); 에 저장한다.

	//...
	return new InMemoryUserDetailsManager(lkdcode, another);
	//...

🎯 SpringSecurity 설정용 클래스

@Configuration
public class SecurityConfig {
	
    private final PasswordEncoder passwordEncoder =
			PasswordEncoderFactories.createDelegatingPasswordEncoder();

    @Bean
    public UserDetailsService authentication() {
        Users lkdcode = Users.builder()
                .email("lkdcode@email.com")
                .password(passwordEncoder.encode("password123"))
                .grade("GOLD")
                .role("ADMIN")
                .build();

        Users another = Users.builder()
                .email("another@email.com")
                .password(passwordEncoder.encode("password123"))
                .grade("SILVER")
                .role("ADMIN")
                .build();

        return new InMemoryUserDetailsManager(lkdcode, another);
    }

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        return http.authorizeHttpRequests()
                .antMatchers("/security/admin").hasAuthority("ADMIN")
                .antMatchers("/security/gold").hasAuthority("GOLD")
//              .requestMatchers("/security/admin").hasAuthority("ADMIN") Boot 3.0.2ver
//              .requestMatchers("/security/gold").hasAuthority("GOLD") Boot 3.0.2ver
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .httpBasic()
                .and()
                .build();
    }
}
.antMatchers("/security/admin").hasAuthority("ADMIN") // : 해당 요청은 `ADMIN` 권한만 접근 가능.
.antMatchers("/security/gold").hasAuthority("GOLD") // : 해당 요청은 `GOLD` 권한만 접근 가능.
  • .hasRole('권한'); : 해당 메서드는 접두사로 "ROLE_" 이 자동으로 추가된다.

🎯 결과

포스트맨에 Basic Auth 를 추가한다.

유저 lkdcode"ADMIN" , "GOLD" 권한을 가지고 있다.

  • /security/all 요청 성공

  • /security/admin 요청 성공

  • /security/gold 요청 성공

유저 another"ADMIN" , "SILVER" 권한을 가지고 있다.

  • /security/all 요청 성공

  • /security/admin 요청 성공

  • /security/gold 요청 실패 403 Forbidden

설정한 인증&인가대로 요청이 잘 처리 됐다.
해당 요청에 대한 로깅을 찍기 위해

application.yml 을 설정할 수 있다.

logging:
  level:
    org.springframework.security: DEBUG
profile
되면 한다

0개의 댓글

Powered by GraphCDN, the GraphQL CDN