0. 시작하게 된 계기 및 다짐 😮
이번 코드스테이츠의 백엔드 엔지니어링 개발자 부트캠프
에 참여하게 되면서 현직개발자 분들의 빠른 성장을 위한 조언 중 자신만의 블로그를 이용하여 배운 것 들을 정리하는게 많은 도움이 된다 하여 시작하게 되었다.
1. 학습 목표 😮
목표 | 결과 |
---|---|
Spring Security 환경 구성에 대해 배우고 구현 | O |
Spring Security 인증(Authentication) 구성요소에 대해 이해 | O |
Spring Security 인가(Authorization) 구성요소에 대해 이해 | O |
Spring Rest Docs를 사용해서 API 문서를 생성 및 배포 | O |
2. 정리 😮
@어노테이션
@Configuration
@EnableWebSecurity
@Data
@EnableGlobalMethodSecurity(securedEnabled = true)
@EnableGlobalMethodSecurity(prePostEnabled=true)
@Secured("ROLE_ADMIN")
@PreAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
@PostAuthorize
스프링 시큐리티(Spring Security)
build.gradle
스프링 시큐리티
필수 용어
1). 주체(Principal)
2). 인증(Authentication)
3). 인가(Authorization = 권한부여)
4). 접근통제(Access control)
Spring Security 사용 이유
1). 모든 요청에 대해 인증 요구
2). 사용자 이름 및 암호를 가진 사용자가 양식 기반으로 인증을 허용
3). 사용자의 로그아웃을 허용
4). CSRF 공격을 방지
5). Session Fixsation(세선 고정 공격)을 보호
[공격자가 자신의 세션 id를 다른 사용자에게 줌으로써 공격하는 방법]
- https://exhibitlove.tistory.com/318
6). 보안 헤더 통합
- HSTS 강화
: 강제적으로 HTTPS Protocol로만 접속하게 하는 기능
: https://m.blog.naver.com/PostView.nhn?blogId=aepkoreanet&logNo=221575708943&proxyReferer=https:%2F%2Fwww.google.com%2F
- X-Content-TypeOptions
- 캐시 컨트롤(정적 리소스 캐싱)
- X-XSS-Protection XSS 보안
[스크립트 공격 보안]
- 클릭재킹을 방지하는 X-Frame 옵션 통합
[웹 사용자가 자신이 클릭하고 있다고 인지하는것과 다른 어떤 클릭하게 속이는 악의적인 기법][잠재적으로 공격자는 비밀정보를 유출하거나 그들의 컴퓨터에 대한 제어를 획득]
: 검색을 클릭하였는데 '구매'버튼을 누르게 하는것
: https://ko.wikipedia.org/wiki/%ED%81%B4%EB%A6%AD%EC%9E%AC%ED%82%B9
7). Servlet API 제공
Spring Security 코드 분석
[FilterChain @Bean등록 예제Code]
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.headers().frameOptions().disable();
http.addFilterBefore(new FirstFilter(), LogoutFilter.class); //(0)
http.authorizeRequests() //(1)
.antMatchers("/user/**").authenticated()
.antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll();
//나머지 요청들은 권한 없이 접속 허가
.and()
.formLogin()
.loginPage("/login")
//권한이 없는 페이지들에 접속하였을시, login페이지로 이동;
//http 객체의 설정을 이어서 할 수 있게 하는 메서드
return http.build();
}
(0) 사용자가 정의한 Filter등을 SecurityFilterChain 내 원하는 필터 전에 적용시킬 수 있음
- http.addFilterAfter(new FirstFilter(), LogoutFilter.class) : 해당 필터 이후에 적용
(1) authorizeRequests()는 시큐리티 처리에 HttpServletRequest를 이용한다는것을 의미
}
1) http.csrf().disable()
- form태그로만 요청이 가능해지고 postman등의 요청이 불가능해진다.
- Get요청을 제외한 상태를 변화시킬 수 있는 POST, PUT, DELETE 요청으로 부터 보호된다.
- disable() 사용이 가능하게 만듬 현재
2). http.headers().frameOptions().disable()
- X-Frame-Opions공격을 방지하기 위해 사용하지만, 여기서는 disable()로 사용가능하게 만듬
- h2 연결할 때 필요한 옵션
- 해당 페이지를 <frame>, <iframe>에서 렌더링할 수 있는지 여부를 나타내는데 사용
- 즉, 해당 사이트내 컨텐츠들이 다른 사이트에 포함되지 않도록 하는것.
- 여기서는 , h2를 사용하기 위해 disable사용
3). .antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
- 해당 url의 경우 권한이 있을 경우에만 접속이 가능하게 만들어준다.
- /, /login, /join 3개의 url은 로그인 없이, 권한 없이 접속이 가능하게 된다.
[WebMvcConfig.java] : mustache -> html 사용 설정
import org.springframework.boot.web.servlet.view.MustacheViewResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
MustacheViewResolver resolver = new MustacheViewResolver();
resolver.setCharset("UTF-8");
resolver.setContentType("text/html; charset=UTF-8");
resolver.setPrefix("classpath:/templates/");
resolver.setSuffix(".html");
registry.viewResolver(resolver);
}
}
(1) Controller 반환값이 기본적으로 String 이나 @ResponseEntity와 같은 객체를 반환하는데,
(2) String으로 반환할 시 해당 String에 맞는 View로 변환하여 보여주기 위함
[UserDetails 오버라이드 클래스 예제코드] : 현재 유저의 권한등의 정보를 세부적으로 가져오는 클래스
import com.codestates.section4week1.model.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
public class PrincipalDetails implements UserDetails {
private Member member;
public PrincipalDetails(Member member) {
this.member = member;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
// User의 권한을 리턴
public String getAuthority() {
return member.getRole();
}
});
return collection;
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return member.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
// 암호 사용 기간이 지났는지에 관해 확인
public boolean isCredentialsNonExpired() {
return true;
}
@Override
//특정 사이트 규칙에 따라 return false로 설정
public boolean isEnabled() {
return true;
}
}
- Authentication 타입 객체이며 안에 Member 정보가 있어야 합니다.
- 로그인 진행이 완료되면 security session을 만들어줍
- Security Session ⇒ Authentication ⇒ UserDetails
[UserDetailsService 오버라이딩 클래스 예제]
import com.codestates.section4week1.model.Member;
import com.codestates.section4week1.repository.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class PrincipalDetailsService implements UserDetailsService {
@Autowired
private MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Member memberEntity = memberRepository.findByUsername(username);
System.out.println("username : " + username);
if(memberEntity != null) {
return new PrincipalDetails(memberEntity);
}
return null;
}
}
- Security 설정에서 loginProcessingUrl(”/login”);으로 요청이 오면 자동으로 UserDetailsService 타입으로 IoC되어 있는 loadUserByUsername 함수가 실행
[loginProcessingUrl("/login_proc")는, 로그인 Form Action Url, default: /login을 의미한다.]
- 메서드 파라미터에 username이라고 되어있으면 form을 통해 username을 가져올 때 name이 반드시 매치되어야 합니다.
1). 이름을 똑같이 변경해주거나
2). SecurityConfig에 .loginPage() 아래에 .usernameParameter(”다른 이름")으로 추가해줘야 합니다.
- loadUserByUsername 함수가 Authentication으로 값이 return 된다.
Filter와 FilterChain
Overview
1). 웹 컨테이너(서블릿 컨테이너)
2). 필터 동작 흐름
1. Filter [사진]
1). 클라 요청 -> Servlet Filter를 거침
2). DispatcherServlet과 같은 Serlvlet에서 요청이 처리됨
2. FilterChain
filter들이 사슬처럼 연결되어 각 처리되는 동작
클라이언트가 앱에 요청을 보내고 컨테이너가 URI 경로를 기반으로 필터로 서블릿을 어떤걸 사용할 지 결정
필터내의 체인에서 걸러질 수 있음
다운스트림 필터와 서블릿을 사용해서 요청과 응답을 수정 가능
컨테이너는 Servlet과 여러Filter로 구성된 필터 체인을 만들어 URI패스를 기반으로 HttpServletReqeust처리
DispatcherServlet에 의해 다뤄지기 전,후로 동작
1개의 Servlet이 HttpServletRequest와 HttpServletResponse 처리를 담당
Filter는 FilterChain안에 있을 때 효력발휘
3. Filter 인터페이스
1). public void init(FilterConfig filterConfig) throws ServletException
2). public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws java.io.IOException, ServletException{ .... chain.doFilter(request, response)...}
3). public void destroy()
4. ★DelegatingFilterProxy★
★ DelegatingFilterProxy 클래스는 Filter를 Spring Bean으로 사용할 수 있도록 합니다.
Spring Bean으로 등록되어있는 정보를 가져오기 위해서 이를 사용
스프링 시큐리티가 모든 애플리케이션 요청을 감싸게하여 모든 요청에 보안이 적용되게 하는 서블릿필터
서블릿 필터 라이프 사이클과 연계해 스프링 빈 의존성을 서블릿 필터에 바인딩
Spring IOC가 Filter bean을 가지고있고 이는 FilterChainProxy다.
스프링 부트는 DelegatingFilterProxy라는 Filter 구현체로 서블릿 컨테이너의 생명주기와 스프링 ApplicationContext를 연결
서블릿 컨테이너는 자체 표준을 사용해서 Filter를 등록할 수 있지만 스프링이 정의하는 Bean은 인식하지 못합
DelegatingFilterProxy는 표준 서블릿 컨테이너 메커니즘으로 등록할 수 있으면서도 모든 처리를 Filter를 구현한 스프링 빈으로 위임
DelegatingFilterProxy는 ApplicationContext에서 Bean Filter()을 찾아 실행합니다.
Bean Filter0는 FilterChainProxy가 됩니다.
5. FilterChainProxy
6. DelegatingPasswordEncoder
PasswordEncoder default : ElegatingPasswrodEncoder
[Ex. PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()]
1). 장점
2). 커스텀 DelegatingPasswwordEncoder
[예제 Code]
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());
PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);
3). Password Storage Format
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
- PasswordEncoder id는 'bcrypt'
- encodedPassword는 나머지 뒷부분
- BcryptPasswordEncoder로 위임
1. Spring Security 인증 처리 흐름 [사진]
1. Overview
사용자가 Username , password를 통해 인증요청
AuthenticationFilter의 구현체인 UsernamePasswordAuthenticationFilter는 전달받은 Username과 Password를 가지고 UsernamePasswordAuthenticationToken을 생성
생성된 Authentication(Token)을 AuthenticationManager에 전달합니다.
AuthenticationManager는 전달받은 Authentication을 다수의 AuthenticationProvider에 전달하여 유효성 검증 및 처리를 위임합니다.
입력받은 사용자의 인증 정보의 유효성 검증을 위해 UserDetailsService로 전달합니다.
UserDetailsService는 loadUserByUsername() 메소드를 통해 사용자 정보를 조회하여 실제 존재하는 사용자인지, Username과 Password가 유효한지 검증합니다.
만약 6에서 사용자의 인증 정보 검증이 성공적으로 이루어졌다면 해당 사용자 정보를 활용해 UserDetails를 생성합니다.
생성된 UserDetails를 AuthenticationProvider에 전달합니다.
authenticate() 메소드가 호출되며 UserDetails와 Authorities로 생성한 Authentication을 Manager에 전달합니다.
생성된 Authentication을 AuthenticationFilter에 전달합니다.
AuthenticationFilter는 인증 처리가 모두 완료되어 해당 사용자의 인증 정보를 담고 있는 Authentication을 SecurityContext에 저장합니다.
Authentication
1. Overview
Authentication(Token)은 인터페이스로 존재하며, 인증을 성공적 수행하면 사용자의 인증 관련 정보를 가지고있음
일반적으로 많이 사용되는 구현체는 UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken은 고유 식별자와 암호로 간단하게 Authentication 생성가능
2. Authentication(인증)
3. Password Storage
Servlet Authentication Architecture
1. Architecture
Overview
SecurityContextPersistenceFilter를 통해 SecurityContextRepository 라는 SecurityContext 저장소 객체에 해당 SecurityContext 영속화
로그인 인증 요청시 새 Context객체를 생성하여 SecurityContextHolder에 등록후, 아래 과정들을 거쳐 인증에 성공하면
HttpSession에 해당 SecurityContext 저장 후(SecurityContextPerstenceFIlter), 요청이 끝나면 SecurityContextHolder를 비우고 SecurityContextRepository 에 해당 SecurityContext 를 다시 저장
인증 후, 요청이 들어오면, 이 HttpSession에서 SecurityContext를 꺼내어 SecurityContextHolder에 저장
SecurityContext안에 Authentication 객체가 존재하면 계속 인증 유지
추후, 요청시 등록이 되있냐 안되있냐에 따라 접근 허가 or 불가
최종적으로 응답이 끝나면, 마찬가지로 SecurityContextHolder를 비움
1. SecurityContextHolder
[Ex. Code]
SecurityContext context = SecurityContextHolder.createEmptyContext(); // (1)
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER"); // (2)
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context); // (3)
(1). Holder를 이용해 빈 Context 생성
(2). Authentication(유저, 패스워드, 권한)을 생성
[UsernamePasswordAuthenticationToken(userDetails, password, authorities)를 주로 사용]
(3) Holder에 이 Context를 저장
SecurityContext
Authentication
GrantedAuthority
AuthenticationManager
ProviderManager
AuthenticationProvider
Request Credentials with AuthenticationEntryPoint
AbstractAuthenticationProcessingFilter
SecurityContextPersistenceFilter
1. SecurityContextPersistenceFilter
2. 처리순서
클라이언트 요청 발생
SecurityContextRepository 인터페이스의 loadContext() 메소드로 저장된 SecurityContext 객체를 가져옴
해당 클라이언트의 요청이 모두 수행되고, 기존 SecurityContextHolder의 clearContext()로 비움
SecurityContextRepository 에 해당 SecurityContext 를 다시 저장
3. SecurityContext 객체의 생성, 저장, 조회
1). 익명 사용자
- 새로운 SecurityContext 객체를 생성하여 SecurityContextHolder에 저장한다.
- AnonymouseAuthenticationFilter에서 AnonymousAuthenticationToken 객체를 SecurityContext에 저장한다.
2). 인증 시
- 새로운 SecurityContext객체를 생성하여 SecurityContextHolder에 저장한다
- UsernamePasswordAuthenticationFilter에서 인증 성공 후 SecurityContext 에 UsernamePasswordAuthentication 객체를 SecurityContext에 저장한다.
- 인증이 최종 완료되면 Session 에 SecurityContext를 저장한다.
3). 인증 후
- Session에서 SecurityContext 꺼내어 SecurityContextHolder에서 저장한다.
- SecurityContext안에 Authentication 객체가 존재하면 계속 인증을 유지한다.
4). 최종 응답 시 공통
- SecurityContextHolder안의 SecurityContext객체에서 보관하던 인증정보를 반드시 초기화 해줘야 한다.
- SecurityContextHolder.clearContext() 메서드를 호출해 인증 정보를 초기화 한다.
2. SecurityContextRepository란?
3. 피드백 😮
4. 앞으로 해야 될 것 😮