[Spring] Spring Security + JWT_01

Gogh·2023년 1월 2일
0

Spring

목록 보기
21/23

🎯 목표 : 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 구조

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 기반 인증 절차

image

  • 클라이언트가 서버에 아이디 비밀번호를 담아 로그인 요청을 보낸다.
  • 아이디 비밀번호가 일치하는지 확인하고 클라이언트에게 보낼 암호화 토큰을 생성한다.
    • Access Token과 Refresh Token을 모두 생성한다
    • 두 종류의 토큰이 같은 정보를 담을 필요는 없다.
  • 토큰을 클라이언트에게 전송하면 클라이언트는 토큰을 저장한다.(Local Storage, Session Storage, Cookie 등)
  • 클라이언트가 HTTP Header 또는 쿠키에 토큰을 담아 요청을 전송한다.
  • 요청으로 받은 토큰이 정상적으로 검증되면 클라이언트의 요청을 처리 후 응답한다.

📌 JWT 기반 인증 장단점

  • 장점
    • 상태를 유지하지 앟고 확장에 용이한 어플리케이션을 구현할 수 있다.
      • 서버에서 클라이언트에 대한 정보를 저장할 필요가 없다.
      • 클라이언트는 요청을 전송할때 토큰을 헤더 또는 쿠키에 포함시키면 된다.
    • 클라이언트가 요청을 전송할때 마다 자격 증명 정보를 전송할 필요가 없다.
      • HTTP Basic 방식은 요청시 마다 자격증명 정보를 포함해야하지만, JWT의 경우 토큰이 만료되기 전까지는 한번의 인증만 수행한다.
    • 인증을 담당하는 시스템을 다른 플랫폼으로 분리하는 것이 가능하다.
      • 사용자의 자격 증명 정보를 직접 관리하지 않고 다른 플랫폼의 자격 증명 정보로 인증하는 것이 가능하다.
    • 권한 부여에 용이하다.
      • 토큰의 페이로드 안에 해당 사용자의 권한 정보를 포함하는 것이 용이하다.
  • 단점
    • 페이로드는 디코딩이 용이하다.
      • Base64로 인코딩 되기 때문에 토큰을 탈취하여 페이로드를 디코딩 하면 토큰 생서시 저장한 데이터를 확인 가능하다.
    • 토큰의 길이가 길어지면 네트워크에 부하를 줄 수 있다.
      • 저장하는 정보의 양이 많아질 수록 토큰의 길이는 길어진다. 요청을 전송할 때마다 긴 토큰을 함께 전송하면 네트워크에 부하를 줄 수 있다.
    • 토큰은 자동으로 삭제되지 않는다.
      • 한번 생성된 토큰은 자동으로 삭제되지 않기 때문에 토큰 만료 시간을 반드시 추가 해야 한다.
      • 토큰이 탈취된 경우 기한이 만료될 때까지 토큰 탈취자가 해당 토큰을 정상적으로 이용할 수 있다.

📒 Spring Security + JWT

📌 Spring Security와 JWT 적용

  • build.gradle 의존성 설정
#...
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 추가

@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;

    // Password Encoder
    private final PasswordEncoder passwordEncoder;
    // Utils 클래스 추가  -  세부 내용 아래 설명 예정
    private final JwtAuthorityUtils authorityUtils;


    public Member createMember(Member member) {
        verifyExistsEmail(member.getEmail());

        // Password 암호화
        String encryptedPassword = passwordEncoder.encode(member.getPassword());
        member.setPassword(encryptedPassword);

        // DB에 User Role 저장
        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 클래스는 사용자의 권한 정보를 생성을 도와 주는 클래스다.

📌 로그인 인증 구현

  • UserDetailsServiceUserDetails를 구현 해 준다.

@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;
        }

        // 해당 프로젝트는 Email을 Username으로 사용중이다.
        @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;
}
profile
컴퓨터가 할일은 컴퓨터가

0개의 댓글