🎯 목표 : JWT 개념 학습, Spring Security 어플리케이션에 JWT를 구현하는 과정 학습
📒 JWT(JSON Web Token) ?
📌 JWT 종류
- Access Token : 정보들에 접근할 수 있는 권한 부여에 사용한다.
- 실제 권한을 얻는데 사용하는 토큰은 Access Token이다.
- Access Token이 탈취 되어도 오랜시간 사용할 수 없도록 만료기간을 짧게 설정한다.
- 하여 Refresh Token과 함께 사용하게 된다.
- Refresh Token : 만료된 Access Token을 재발급 받기위해 사용된다.
- Refresh Token 까지 탈취 당하게 되면 사용자에게 피해를 입힐 수 있다.
- 정보 보안이 중요한 어플리케이션에서는 Refresh Token을 사용하지 않는 경우도 있다.
📌 JWT 구조
- Header
- 어떤 종류의 토큰인지 어떤 알고리즘으로 sign 할지 정의한다.
- Json 포맷 형태로 정의한다.
- JWT 헤더 부분을 Decoded하면 아래와 같은 데이터 형태를 확인할 수 있다.
{
"alg": "HS256",
"typ": "JWT"
}
- Payload
- 서버에서 활용할 수 있는 사용자의 정보가 담겨 있다.
- 어떤 정보에 접근 가능한지에 대한 권한을 담을수 있고 사용자의 이름 등 필요한 데이터를 담을수 있다.
- 민감한 정보는 담지 않는것이 좋다.
- JWT 페이로드 부분을 Decoded하면 아래와 같은 데이터 형태를 확인할 수 있다.
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
- Signature
- Base64로 인코딩된 헤더와 페이로드가 완성되었다면, Signature에서 원하는 SecretKey와 헤더에서 지정한 알고리즘을 사용하여 헤더와 페이로드를 단방향 암호화 한다.
- Signature는 토큰의 위변조 유무를 검증하는데 사용한다.
- HMAC SHA256 알고리즘을 사용한다면 아래와 같이 생성된다.
HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret);
📌 JWT 기반 인증 절차
- 클라이언트가 서버에 아이디 비밀번호를 담아 로그인 요청을 보낸다.
- 아이디 비밀번호가 일치하는지 확인하고 클라이언트에게 보낼 암호화 토큰을 생성한다.
- Access Token과 Refresh Token을 모두 생성한다
- 두 종류의 토큰이 같은 정보를 담을 필요는 없다.
- 토큰을 클라이언트에게 전송하면 클라이언트는 토큰을 저장한다.(Local Storage, Session Storage, Cookie 등)
- 클라이언트가 HTTP Header 또는 쿠키에 토큰을 담아 요청을 전송한다.
- 요청으로 받은 토큰이 정상적으로 검증되면 클라이언트의 요청을 처리 후 응답한다.
📌 JWT 기반 인증 장단점
- 장점
- 상태를 유지하지 앟고 확장에 용이한 어플리케이션을 구현할 수 있다.
- 서버에서 클라이언트에 대한 정보를 저장할 필요가 없다.
- 클라이언트는 요청을 전송할때 토큰을 헤더 또는 쿠키에 포함시키면 된다.
- 클라이언트가 요청을 전송할때 마다 자격 증명 정보를 전송할 필요가 없다.
- HTTP Basic 방식은 요청시 마다 자격증명 정보를 포함해야하지만, JWT의 경우 토큰이 만료되기 전까지는 한번의 인증만 수행한다.
- 인증을 담당하는 시스템을 다른 플랫폼으로 분리하는 것이 가능하다.
- 사용자의 자격 증명 정보를 직접 관리하지 않고 다른 플랫폼의 자격 증명 정보로 인증하는 것이 가능하다.
- 권한 부여에 용이하다.
- 토큰의 페이로드 안에 해당 사용자의 권한 정보를 포함하는 것이 용이하다.
- 단점
- 페이로드는 디코딩이 용이하다.
- Base64로 인코딩 되기 때문에 토큰을 탈취하여 페이로드를 디코딩 하면 토큰 생서시 저장한 데이터를 확인 가능하다.
- 토큰의 길이가 길어지면 네트워크에 부하를 줄 수 있다.
- 저장하는 정보의 양이 많아질 수록 토큰의 길이는 길어진다. 요청을 전송할 때마다 긴 토큰을 함께 전송하면 네트워크에 부하를 줄 수 있다.
- 토큰은 자동으로 삭제되지 않는다.
- 한번 생성된 토큰은 자동으로 삭제되지 않기 때문에 토큰 만료 시간을 반드시 추가 해야 한다.
- 토큰이 탈취된 경우 기한이 만료될 때까지 토큰 탈취자가 해당 토큰을 정상적으로 이용할 수 있다.
📒 Spring Security + JWT
📌 Spring Security와 JWT 적용
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
}
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers().frameOptions().sameOrigin()
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authorizeHttpRequests(authorize -> authorize
.anyRequest().permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
- H2 콘솔을 사용하고 있어 콘솔 페이지를 렌더링 하기위해
.headers().frameOptions().sameOrigin()
를 추가해준다.
- 예제 프로젝트로 CSRF 설정을 비활성화 한다.
- JSON 포맷으로 데이터를 요청하고 응답하기 때문에
.formLogin().disable()
추가해 준다.
- Username/Password 정보를 헤더로 받지 않기 위해 HTTP Basic 인증을 disable 해준다.
- 폼 로그인과 Basic 인증을 disable 하면,
UsernamePasswordAuthenticationFilter
, BasicAuthenticationFilter
필터가 비활성화 된다.
PasswordEncoder
를 Bean 등록한다.
- 회원 정보를 담는 Entity와 DTO 클래스가 있다면, Password와 Roles 필드를 추가해 준다.
- Entity와 DTO 클래스는 생략한다.
📌 Service 로직 적용
@Transactional
@RequiredArgsConstructor
@Service
public class MemberService {
private final MemberRepository memberRepository;
private final ApplicationEventPublisher publisher;
private final PasswordEncoder passwordEncoder;
private final JwtAuthorityUtils authorityUtils;
public Member createMember(Member member) {
verifyExistsEmail(member.getEmail());
String encryptedPassword = passwordEncoder.encode(member.getPassword());
member.setPassword(encryptedPassword);
List<String> roles = authorityUtils.createRoles(member.getEmail());
member.setRoles(roles);
Member savedMember = memberRepository.save(member);
publisher.publishEvent(new MemberRegistrationApplicationEvent(this, savedMember));
return savedMember;
}
}
- 패스워드를 단방향 암호화 하기위한
PasswordEncoder
의존성 주입을 해준다.
- Utils 클래스는 사용자의 권한 정보를 생성을 도와 주는 클래스다.
📌 로그인 인증 구현
UserDetailsService
와 UserDetails
를 구현 해 준다.
@Component
@RequiredArgsConstructor
public class MemberDetailsService implements UserDetailsService {
private final MemberRepository memberRepository;
private final JwtAuthorityUtils authorityUtils;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> optionalMember = memberRepository.findByEmail(username);
Member member = optionalMember.orElseThrow(
() -> new ServiceLogicException(ErrorCode.MEMBER_NOT_FOUND)
);
return new MemberDetails(member);
}
private final class MemberDetails extends Member implements UserDetails {
public MemberDetails(Member member) {
setMemberId(member.getMemberId());
setEmail(member.getEmail());
setPassword(member.getPassword());
setRoles(member.getRoles());
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getUsername() { return getEmail(); }
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
@Override
public boolean isEnabled() { return true; }
}
}
- 클라이언트에서 전송 받은 Username/Password를 역직렬화 하기 위한 LoginDto를 생성한다.
@Getter
public class LoginDto {
private String username;
private String password;
}