부트 캠프에서 Spring를 배우기 시작한지 6일차.
오늘은 로그인 구현 방법을 배워볼거다.
직접 세션과 쿠키를 구현해 보고 이어서 토큰이 생긴 이유도 알아보자.
하지만 나중에 Spring Security로 바꿀 것이니 구체적인 코드보다는
쿠키와 세션의 원리와 동작을 주목하자.
우선 등장 배경을 이해하기 위한 배경 지식이 필요하다.
Stateless
Connectionless
2가지 특징을 극복하기 위해 생긴 것이 세션과 쿠키이다.
- 세션은 서버가 저장하고
- 쿠키는 클라이언트가 저장한다.
HttpServletRequest 변수에는 요청의 모든 정보를 볼 수 있다.
세션과 쿠키도 여기 포함되어있다.
클라이언트가 쿠키를 들고 가면 서버는 세션을 가져다 준다.
세션ID는 클라이언트가 요청만 보내도 생성이 된다.
서버는 세션을 보고 요청을 구분하기 때문에 세션 하이재킹이 가능하다.
세션은 서버에 부담을 준다.
서버 이중화를 한다고 하면 세센 클러스터링이 필요하다.
그러면 세션이 메모리를 너무 많이 잡아먹는다. (복사, 동기화)
세션을 따로 저장하는 서버를 만든다. ? 서버를 더 늘릴 수 없다면?
세션을 아예 저장하지 않고 클라이언트에게만 저장시키면 되지 않을까...?
이 표가 토큰 기반 인증 방법이다.
로그인할 때 서버가 클라이언트에게 토큰을 발급 (인증)
(쿠키 등을 통해 클라이언트에 저장)
요청이 들어오면 클라이언트의 토큰이 유효한지 서버가 검사 후 서비스를 제공 (인가)
토큰을 쓸려면 보안이 중요하다.
보안 이야기 좀 하고 넘어가자.
SSH인증서도 마찬가지이다 배워두면 두고두고 쓴다.
1) 대칭키와 비대칭키 <공개키 , 개인키>
암호화와 복호화 시 쓰는 키가 같냐(대칭키), 다르냐(비대칭키)의 차이
인증서가 뭔데? 공개키가 누구껀지 알려주는 거
누가 : 제 3의 인증 기관 (구글 , 아마존)
무엇을 : 요청한 곳의 공개키가 맞는지를 알려준다.
언제 : ?
어떻게 : ?
왜 : 누군가 공개키를 바꿔치기 할 수 있어서
사용처
토큰이 변조되었는지 확인할 수 있음.
비밀번호를 관리자도 모르게 할 수 있음.
세션 쿠키는 HTTP가 기본으로 가지고 있는거라 구현이 비교적 쉬움
토큰은 어려움. 알아야하는 보안 알고리즘도 많고 구현 방법도 복잡함.
그럼 토큰만 쓰나요? 아니요. 정답은 없다.
둘 다 구현을 해볼 것이다.
궁극적으로는 MSA 설계 방식으로 계발을 할거기 때문에 토큰 방식으로 구현할 것이다. 게다가 스프링 시큐리티가 해줄거다.
스프링 시큐리티를 쓸려면 스프링 시큐리티 필터를 봐야하는데
직접해보면서 알아보자
스프링 시큐리티 쓰는 법은 간단하지만
구동 방식은 복잡하다.
기본 프로젝트에 시큐리티만 설치하고 무엇이 달라지는지 확인해보자.
(dependency 추가)
시큐리티 안에는 뷰와 컨트롤러가 기본으로 내장되어있다.
라이브러리 추가해서 실행만해도 기본화면이 다르다.
Spring Security를 추가한 직후
스프링 시큐리티의 필터들이 뭘 검증하는지 한 번 씩 읽어보자.
시큐리티 작동 방식
Spring Security 외부 라이브러리를 설정하기 위해 빈을 생성한다.
package com.example.securityTest.config; 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.WebSecurityConfiguration; import org.springframework.security.web.SecurityFilterChain; @Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity){ try { httpSecurity.csrf().disable()//(크로스 사이트 요청 변조 공격) 방어 설정 끄기, 프론트엔드 막을거다. .authorizeRequests() //인증받은 요청을 받겠다. .antMatchers().permitAll() //이거 허락하고 .anyRequest().denyAll();//나머지 요청 거부하겠음. return httpSecurity.build(); } catch (Exception e) { throw new RuntimeException(e); } } }
이거 추가해서 서버 실행시키면 요청 거부 화면이 나온다.
왜냐? 위 설정을 통해 인증 받지 않은 사용자는 더 이상 컨트롤러에 접근할 수 없기 때문이다.
그럼 인증받는 방법은 무엇인가?
member 서비스를 만든다.
package com.example.securityTest.member.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.util.Collection;
@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Integer id;
String username;
String password;
String authority;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> result = memberRepository.findByUsername(username);
Member member = null;
if(result.isPresent()) {
member = result.get();
httpServletRequest.getSession().setAttribute("isLogined", true); // 세션 방식 인증 처리
}
return member;
}
이 때, 비밀번호가 암호화가 되어야 스프링 시큐리티가 로그인 시켜준다.
암호화 방법
1.패스워드 인코더를 의존성 주입을 받을 수 있도록 별도의 클래스를 만든다.@Configuration public class PasswordEncorderConfig { @Bean public PasswordEncoder passwordEncoder(){ return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } }
- 서비스 클래스에서 패스워드 인코더를 의존성 주입을 받는다.
public void signup(String username, String password) { if(!memberRepository.findByUsername(username).isPresent()) { memberRepository.save(Member.builder() .username(username) .password(passwordEncoder.encode(password)) //패스워드를 인코딩한다. .authority("ROLE_USER") .build()); } }
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton((GrantedAuthority) () ->authority);
}