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

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

Tags
Spring
Core
Published
December 4, 2023
Author
lkdcode
스프링 시큐리티로 인증&인가 구현하기유효한 계정인지?해당 요청에 대한 권한이 있는지?
 
💡 클라이언트 요청 시 해당 유저에게 권한이 있는지 확인하기
1. 로그인한 유저는 해당 게시글을 조회할 수 있는 권한이 있는가?
2. 먼저, 로그인을 한 유저가 맞는가?
3. 로그인(인증) 해당 게시글의 조회 권한(인가)을 구현해보자.
관리자 계정과 일반 유저 계정으로 나눈다. [USERSADMIN]
일반 유저 계정은 등급이 나뉜다. [BRONZESILVERGOLD]
 
🎯 요청 테스트를 위한 컨트롤러
테스트를 위해 총 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 테스트 결과

notion image
notion image
notion image
현재 모든 요청이 접근 가능하다.
 
🎯 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 다
로그인 후 요청시 모든 요청은 성공한다.
패스워드는 애플리케이션이 시작될 때마다 새로 생성된다.
notion image
시큐리티 의존성 추가만으로 최대의 보안 효과를 얻었지만,
자동 생성되는 단 하나의 유저와 비밀번호를 모든 이용자가 공유할 수는 없다.
단 하나의 계정을 모든 이용자가 공유한다면책임과 인증의 보안 원칙이 위배되며이용하는 유저를 고유하게 증명할 수 없다.
설정을 통해 회원가입한 유저들의 요청만 인증&인가를 처리한다.
 
🎯 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; } }
 
기본 엔티티에 role 과 grade 는 권한이다.
  • role : USERS or ADMIN
  • grade : BRONZE or SILVER or GOLD
notion image
 
오라이딩된 메서드들은 다음과 같다.
  • getAuthorities() : 계정이 가지고 있는 권한 목록 리턴
  • getUsername() : 계정의 이름을 리턴. (식별자)
  • isAccountNonExpired() : 계정이 만료됐는지? (true는 만료되지 않음)
  • isAccountNonLocked() : 계정이 잠겼는지? (true는 잠기지 않음)
  • isCredentialsNonExpired() : 비밀번호가 만료됐는지? (true는 만료되지 않음)
  • isEnabled() : 계정이 활성화됐는지? (true는 활성화 상태)
    🎯 이용자들
    시나리오를 짚고 넘어간다.
    1. 회원가입된 이용자만 접근이 가능하다.
    1. 권한을 살피기전, 유효한 이용자인지 아닌지 알아야한다.
    1. 회원가입 리스트는 new InMemoryUserDetailsManager('이용자1', '이용자2'...); 에 저장한다.
    1. 권한에 따라 응답한다.
    총 두명의 이용자가 있다.
    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 : ADMIN 과 GOLD 권한을 가지고 있다.
    • another : USERS 과 SILVER 권한을 가지고 있다.
      🎯 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 를 추가한다.
      notion image
       
      유저 lkdcode 는 "ADMIN" , "GOLD" 권한을 가지고 있다.
      • /security/all 요청 성공
      notion image
       
      • /security/admin 요청 성공
      notion image
       
      • /security/gold 요청 성공
      notion image
       
      유저 another 는 "ADMIN" , "SILVER" 권한을 가지고 있다.
      • /security/all 요청 성공
      notion image
       
      • /security/admin 요청 성공
      notion image
       
      • /security/gold 요청 실패 403 Forbidden
      notion image
       
      설정한 인증&인가대로 요청이 잘 처리 됐다.
      해당 요청에 대한 로깅을 찍기 위해
      application.yml 을 설정할 수 있다.
      logging: level: org.springframework.security: DEBUG