1. 시큐리티란?
Springboot 시큐리티(Security)는 스프링 프레임워크에서 제공하는 시큐리티 모듈을 기반으로 한 웹 애플리케이션의 인증과 권한 부여 등의 보안 기능을 쉽게 구현할 수 있도록 도와주는 라이브러리이다.
스프링 부트 시큐리티를 사용하면 보안 구현에 대한 많은 부분을 자동으로 처리할 수 있기 때문에, 개발자는 보다 안전하고 신뢰성 높은 웹 애플리케이션을 쉽게 구현할 수 있다.
2. 시큐리티를 이용한 보안 설정
1) 스프링부트 시큐리티는 application.properties 또는 application.yml 파일을 이용하여 보안 설정을 관리한다.
2) 보안 설정 파일에서는 인증, 권한 부여, 로그인 폼, 로그아웃, CSRF 등 다양한 보안 관련 설정을 할 수 있다.
3) 보안 설정 파일은 스프링부트의 자동 설정 기능을 이용하여 쉽게 구성할 수 있다.
3. 시큐리티 필터 체인
필터 체인(Filter Chain)은 클라이언트의 요청이 들어오면 해당 요청에 대한 보안 처리를 수행하는 다양한 필터들이 연속적으로 적용된다.
스프링부트 시큐리티에서 자주 사용되는 필터 체인 종류는 아래와 같다.
1) SecurityContextPersistenceFilter: SecurityContext를 HTTP 세션에 저장하거나 로드한다.
2) WebAsyncManagerIntegrationFilter: Spring의 WebAsyncManager를 SecurityContext와 통합한다.
3) HeaderWriterFilter: HTTP 응답 헤더를 처리한다.
4) CorsFilter: Cross-Origin Resource Sharing(CORS)을 처리한다.
5) CsrfFilter: CSRF 공격 방지를 처리한다.
6) LogoutFilter: 로그아웃 처리를 수행한다.
7) UsernamePasswordAuthenticationFilter: 사용자 인증을 처리한다.
8) DefaultLoginPageGeneratingFilter: 기본 로그인 페이지를 생성한다.
9) DefaultLogoutPageGeneratingFilter: 기본 로그아웃 페이지를 생성한다.
10) BasicAuthenticationFilter: HTTP 기본 인증을 처리한다.
11) RequestCacheAwareFilter: 요청 캐시를 관리한다.
12) SecurityContextHolderAwareRequestFilter: 보안 컨텍스트 홀더를 요청에 노출한다.
13) AnonymousAuthenticationFilter: 익명 사용자 인증을 처리한다.
14) SessionManagementFilter: 세션 관리를 처리한다.
15) ExceptionTranslationFilter: 예외 처리를 담당한다.
16) FilterSecurityInterceptor: URL 권한 검사를 처리한다.
4. 인증과 권한 부여
- 스프링부트 시큐리티에서는 인증(Authentication)과 권한 부여(Authorization)를 분리하여 처리한다.
- 인증은 사용자가 제공한 정보를 검증하여 유효한 사용자인지를 확인하는 과정이다.
- 권한 부여는 인증된 사용자가 요청한 자원에 대한 접근 권한이 있는지 확인하는 과정이다.
- 스프링부트 시큐리티에서는 다양한 인증 방식을 제공합니다. 예를 들어, 폼 로그인, HTTP 기본 인증, OAuth 2.0, OpenID Connect 등이 있다.
- 인증과 권한 부여는 사용자가 로그인하면 실행됩니다. 스프링부트 시큐리티는 로그인한 사용자를 인식하기 위해 SecurityContextHolder라는 보안 컨텍스트 홀더를 사용한다.
- SecurityContextHolder에는 인증된 사용자와 그에 해당하는 권한 정보가 저장되며, 이 정보는 필터 체인에서 필요할 때마다 사용된다.
5. 보안 이벤트
- 스프링부트 시큐리티에서는 보안 이벤트를 발생시켜서 사용자에게 보안 상태를 알릴 수 있다.
- 보안 이벤트에는 로그인 실패, 로그인 성공, 로그아웃 등이 포함되며, 보안 이벤트는 보안 이벤트 리스너를 등록하여 처리할 수 있다.
6. Spring Security OAuth2
- 스프링부트 시큐리티에서는 Spring Security OAuth2를 지원한다.
- Spring Security OAuth2는 OAuth2.0 프로토콜을 구현하는 라이브러리로, 인증 서버와 리소스 서버를 구현할 수 있다.
- Spring Security OAuth2를 사용하면 클라이언트 애플리케이션에서 보안 토큰을 발급받아 리소스 서버에 접근할 수 있다.
- 기본적으로, 구글, 트위터, 페이스북 설정을 지원해준다. (한국에서 자주 사용하는 카카오, 네이버는 지원대상 X)
7. 보안 테스트
- 스프링부트 시큐리티에서는 보안 테스트를 쉽게 수행할 수 있도록 Test-Support 모듈을 제공한다.
- Test-Support 모듈을 사용하면 보안 설정이 적용된 상태에서 테스트를 수행할 수 있다.
8. 사용 방법
💡 build.gradle에 아래 내용 추가 후 웹 실행
implementation 'org.springframework.boot:spring-boot-starter-security’
[ 기본 폼 ]
9. 구조로 보는 스프링 시큐리티
1) 사용자가 로그인 정보와 함께 인증 요청을 한다.(Http Request)
2) AuthenticationFilter가 요청을 가로채고, 가로챈 정보를 통해 UsernamePasswordAuthenticationToken의 인증용 객체를 생성한다.
3) AuthenticationManager의 구현체인 ProviderManager에게 생성한 UsernamePasswordToken 객체를 전달한다.
4) AuthenticationManager는 등록된 AuthenticationProvider를 조회하여 인증을 요구한다.
5) 실제 DB에서 사용자 인증정보를 가져오는 UserDetailsService에 사용자 정보를 넘겨준다.
6) 넘겨받은 사용자 정보를 통해 DB에서 찾은 사용자 정보인 UserDetails 객체를 만든다.
7) AuthenticationProvider는 UserDetails를 넘겨받고 사용자 정보를 비교한다.
8) 인증이 완료되면 권한 등의 사용자 정보를 담은 Authentication 객체를 반환한다.
9) 다시 최초의 AuthenticationFilter에 Authentication 객체가 반환된다.
10) Authenticaton 객체를 SecurityContext에 저장한다.
10. 코드로 알아보는 스프링부트 시큐리티
1) 회원가입 시, 비밀번호 암호화 처리
@Transactional
public boolean write(MemberDto memberDto) {
// 스프링 시큐리티에서 제공하는 암호화 방식 적용
// 사용 이유: 1)DB내 패스워드 감추기 위함, 2)정보가 이동하면서 패스워드 노출 방지하기 위함
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
log.info("비크립트 암호화 사용: " + passwordEncoder.encode(memberDto.getPassword()));
memberDto.setMpassword(passwordEncoder.encode(memberDto.getMpassword()));
// passwordEncoder.encode(암호화할 데이터); => 같은 데이터가 들어와도 서로 다른 암호 생성됨
MemberEntity entity = memberEntityRepository.save(memberDto.toEntity());
if( entity.getMno() > 0 ) { return true; }
return false;
}
2) 로그인 인증 필터 가로채기 작업
@Configuration // 스프링 빈에 등록
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private MemberService memberService;
// configure(HttpSecurity http): http[URL] 관련 보안 담당 메소드
@Override // 오버라이딩으로 재정의
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
http
.csrf() // 사이트 간 요청위조 [post,put http 사용불가]
.ignoringAntMatchers("/member/info") // ignor로 csrf 무시
.ignoringAntMatchers("/member/login")
.and() // 기능 추가&구분할 때 사용하는 메소드
.formLogin()
.loginPage("/member/login") // 로그인으로 사용될 페이지의 매핑 URL
.loginProcessingUrl("/member/login") // 로그인을 처리할 매핑 URL
.defaultSuccessUrl("/") // 로그인 성공했을 때 이동할 매핑 URL
.failureUrl("/member/login") // 로그인 실패했을 때 이동할 매핑 URL
.usernameParameter("memail") // 로그인 시 사용될 계정 아이디의 필드명
.passwordParameter("mpassword") // 로그인 시 사용될 계정 패스워드의 필드명
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/member/logout")) // 로그아웃 처리를 요청할 매핑 URL
.logoutSuccessUrl("/") // 로그아웃 성공했을 때 이동할 매핑 URL
.invalidateHttpSession(true); // 세션 초기화
}
}
3) DB에 저장된 정보와 비교 작업 진행(UserDetailsService 커스터마이징 적용)
[UserDetailsService에 새로 정의한 Service 인스턴스 적용]
// ------------------------------------------------- 위에 코드에 내용 추가
@Configuration // 스프링 빈에 등록
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private MemberService memberService;
@Override // 인증[로그인] 관련 보안 담당 메소드
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// super.configure(auth);
auth.userDetailsService(memberService).passwordEncoder(new BCryptPasswordEncoder());
// 해석1: auth.userDetailsService(memberService) => userDetailsService 구현된 서비스 대입
// 해석2: BCryptPasswordEncoder 사용 시, passwordEncoder 메소드 호출
// 해석3: passwordEncoder 메소드에 Service에서 사용한 객체 대입
}
}
[Serivce 클래스 정의 작업: UserDetailsService implements]
@Service
@Slf4j
public class MemberService implements UserDetailsService {
@Autowired
private MemberEntityRepository memberEntityRepository;
@Autowired
private HttpServletRequest request;
// 1. 회원가입[C]
@Transactional
public boolean write(MemberDto memberDto) {
// 스프링 시큐리티에서 제공하는 암호화 방식 적용
// 사용 이유: 1)DB내 패스워드 감추기 위함, 2)정보가 이동하면서 패스워드 노출 방지하기 위함
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
log.info("비크립트 암호화 사용: " + passwordEncoder.encode(memberDto.getPassword()));
memberDto.setMpassword(passwordEncoder.encode(memberDto.getMpassword()));
// passwordEncoder.encode(암호화할 데이터); => 같은 데이터가 들어와도 서로 다른 암호 생성됨
MemberEntity entity = memberEntityRepository.save(memberDto.toEntity());
if( entity.getMno() > 0 ) { return true; }
return false;
}
// 스프링 시큐리티 적용 로그인: implements UserDetailsService 사용
@Override
public UserDetails loadUserByUsername(String memail) throws UsernameNotFoundException {
// 1. UserDetailsService 인터페이스 구현
// 2. loadUserByUsername(): 아이디 검증
MemberEntity entity = memberEntityRepository.findBymemail(memail);
if( entity == null ) { return null; }
// 3. 패스워드 검증은 시큐리티에서 자동으로 진행
// 4. 검증 후, 세션에 저장할 DTO 반환처리
MemberDto dto = entity.toDto();
log.info("UserDetails로 들어온 dto: " + dto);
return dto;
}
}
[DTO 정의 작업: UserDetails 인터페이스 implements]
package web.domain.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import web.domain.entity.member.MemberEntity;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Set;
// 시큐리티 + 일반DTO
@Data
@AllArgsConstructor@NoArgsConstructor@Builder
public class MemberDto implements UserDetails {
private int mno;
private String memail;
private String mpassword;
private String mname;
private String mphone;
private String mrole;
Set<GrantedAuthority> grantedList;
// 추가 필드
private LocalDateTime cdate;
private LocalDateTime udate;
// toEntity (입력용: cdate,udate 정보는 자동으로 들어가기 때문에 필요 없음.)
public MemberEntity toEntity(){
return MemberEntity.builder()
.mno(this.mno)
.memail(this.memail)
.mpassword(this.mpassword)
.mname(this.mname)
.mphone(this.mphone)
.mrole(this.mrole)
.build();
}
// UserDetails(인터페이스) 구현 메소드
@Override // 인증된 권한 반환
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.grantedList;
}
// Returns the authorities granted to the user. Cannot return null.
// Returns: the authorities, sorted by natural key (never null)
@Override // 패스워드 반환
public String getPassword() {
return this.mpassword;
}
// Returns the password used to authenticate the user.
// Returns: the password
@Override // 계정 반환
public String getUsername() {
return this.memail;
}
// Returns the username used to authenticate the user. Cannot return null.
// Returns: the username (never null)
@Override // 계정 만료
public boolean isAccountNonExpired() {
return true;
}
// Indicates whether the user's account has expired. An expired account cannot be authenticated.
// Returns: true if the user's account is valid (ie non-expired), false if no longer valid (ie expired)
@Override // 계정 락킹 처리
public boolean isAccountNonLocked() {
return true;
}
// Indicates whether the user is locked or unlocked. A locked user cannot be authenticated.
// Returns: true if the user is not locked, false otherwise
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// Indicates whether the user's credentials (password) has expired. Expired credentials prevent authentication.
// Return: true if the user's credentials are valid (ie non-expired), false if no longer valid (ie expired)
@Override
public boolean isEnabled() {
return true;
}
// Indicates whether the user is enabled or disabled. A disabled user cannot be authenticated.
// Returns: true if the user is enabled, false otherwise
}
[ 권한 설정: SecurityConfiguration 클래스에 코드 추가 ]
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
http
// 권한에 따른 HTTP GET 요청 제한
.authorizeRequests()// HTTP 인증 요청
.antMatchers("/member/info/mypage")// 권한 유저 마이페이지 URL 매핑
.hasRole("user") // 토큰에 저장되어 있는 데이터: ROLE=user (user 마이페이지 접근 허용 설정)
.antMatchers("/admin/**") // localhost:8080/admin 이하 모든 페이지
.hasRole("admin") // admin만 접근 허용 설정
.antMatchers("/board/write")
.hasRole("user")
.antMatchers("/**") // localhost:8080 이하 모든 페이지 (최하단에 위치해야 위에 설정값 유효)
.permitAll() // 모두 허용 설정
}
'SpringBoot' 카테고리의 다른 글
SpringBoot - JPA 이해 (0) | 2023.04.28 |
---|---|
SpringBoot - @Slf4j 이해 (0) | 2023.04.28 |
SpringBoot - @Autowired 이해 (0) | 2023.04.28 |
SpringBoot - @RequestMapping 활용 (0) | 2023.04.28 |
SpringBoot - @Controller, @RestController 비교 (0) | 2023.04.28 |