스프링 시큐리티로 인증&인가 구현하기유효한 계정인지?해당 요청에 대한 권한이 있는지?
💡 클라이언트 요청 시 해당 유저에게 권한이 있는지 확인하기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; } }
기본 엔티티에
role
과 grade
는 권한이다.role
:USERS
orADMIN
grade
:BRONZE
orSILVER
orGOLD
오라이딩된 메서드들은 다음과 같다.
getAuthorities()
: 계정이 가지고 있는 권한 목록 리턴
getUsername()
: 계정의 이름을 리턴. (식별자)
isAccountNonExpired()
: 계정이 만료됐는지? (true는 만료되지 않음)
isAccountNonLocked()
: 계정이 잠겼는지? (true는 잠기지 않음)
isCredentialsNonExpired()
: 비밀번호가 만료됐는지? (true는 만료되지 않음)
isEnabled()
: 계정이 활성화됐는지? (true는 활성화 상태)
🎯 이용자들
시나리오를 짚고 넘어간다.
- 회원가입된 이용자만 접근이 가능하다.
- 권한을 살피기전, 유효한 이용자인지 아닌지 알아야한다.
- 회원가입 리스트는
new InMemoryUserDetailsManager('이용자1', '이용자2'...);
에 저장한다.
- 권한에 따라 응답한다.
총 두명의 이용자가 있다.
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
를 추가한다.유저
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