Spring

Spring Boot 와 JWT 로 로그인하기

728x90

저번 포스팅(https://sdy-study.tistory.com/269) 에서 이메일로 회원가입을 보내는 방법을 알아봤다

 

이번에는 JWT 로 로그인을 하는 방법에 대해 알아본다

 

 

 

* 로그인을 위한 사전지식들

1) 인증

: 웹에서 인증을 하기 위해선 과거엔 쿠키, 세션을 사용했지만 현재는 토큰을 이용한 방식을 가장 많이 사용한다

토큰 기반의 인증의 가장 대표적인게 바로 JWT (JSON Web Token) 이다

(참조 : https://sdy-study.tistory.com/44)

 

 

 

2) JWT

: JWT (Json Web Token) 은 JSON 포맷을 이용하여, 유저에 대한 속성을 저장하는 Claim 기반의 Web Token 이다

JWT 는 토큰 자체를 정보로 사용하는 Self-Contained 방식으로 정보를 저장한다

(JWT 에 대한 자세한 구조는 링크 참조 : https://mangkyu.tistory.com/56)

 

 

 

 

3) JWT 를 통한 로그인 과정

: JWT 를 통한 로그인 과정을 요약하면 아래와 같다 (세부적인 내용은 구현하는 사람에 따라 약간 다를 수 있다)

출처 : https://sanghaklee.tistory.com/47

1. 먼저 클라이언트가 서버로 로그인 요청을 보낸다 

 

2. JWT 를 클라이언트가 가지고 있지 않으면 서버에서 생성한다.

-> 이 포스팅에선 공개키, 개인키를 기반으로 토큰을 생성할 것이다

-> 만약 토큰이 이미 있는 상태로 요청했다면, 서버에선 토큰 검증 절차를 치르고 그에따른 응답을 보낸다

 

3. (토큰을 생성한 경우) 클라이언트로 넘긴다

 

4. 다음 요청시에 HTTP Header 에 토큰을 첨부하여 요청한다

(Authorization: Bearer token-value)

 

5. 토큰 검증하고 그에 따른 응답을 한다

 

 

 

4) Spring Security 가 제공하는 로그인 관련 기본 클래스 및 인터페이스들

 

a) UsernamePasswordAuthenticationToken 클래스

: username 과 password 를 담고 있는 클래스로, 유저가 로그인시에 입력한 내용들을 담고 있다

 

b) AuthenticationManager 인터페이스

: authenticate 라는 메소드를 가지고 있으며, 이 메소드의 매개변수는 Authentication 이라는 인터페이스가 들어가고, 자체적인 인증 로직을 거치는 기능을 갖고 있다.

 

c) AuthenticationManagerBuilder 클래스

: AuthenticationManager 인터페이스를 생성하는 빌더이며, 인증 방식에 대한 설정을 지정한다

(DB 방식인지, In-memory 방식인지 등)

 

d) UserDetailsService 인터페이스

: 유저가 입력한 로그인 데이터를 다루는 인터페이스이며 이 인터페이스는 직접 구현을 해줘야한다

구현 방법은 해당 프로젝트가 어떤 방식의 인증을 쓰느냐에 따라 달라진다

Redis 와 같은 in-memory 를 이용해서 인증을 하는지

아니면 DB 를 이용해서 인증을 하는지

혹은 LDAP 를 이용해서 인증을 하는지 등등에 따라 달라질 수 있다.

(여기서는 유저 정보를 DB에 담아서 사용하므로 DB 인증 방식을 사용했다)

 

e) UserDetails 인터페이스

: DB 로 부터 가져온 유저 정보를 담는 인터페이스이다.

 

 

 

5) 4) 에서 언급한 기본 클래스 및 인터페이스를 이용하여 로그인을 하는 과정 요약

- 먼저 4)에서 언급한 AuthenticationManager 를 Spring Bean 으로 등록해야 한다 (인증 처리를 위함임)

 

- AuthenticationManagerBuilder 를 이용해서 위에서 언급한 인증 방식을 설정한다 (여기선 DB 방식으로)

 

- 설정을 모두 마친뒤, Controller 가 유저가 입력한 로그인정보를 받게 하고 Service 로 넘긴다

 

- Service 클래스에서 UsernamePasswordAuthenticationToken 인스턴스를 생성하여 유저의 입력정보를 담는다

 

- 그리고 이 인스턴스를 AuthenticationManager 가 인증처리하도록 authenticate() 메소드를 호출한다

AuthenticationManager 가 처리를할때, UserDetailsService 를 구현한 자체 클래스에서 정의한 방식대로 (DB 방식인지, in-memory 방식인지) 에 맞춰서 처리한다.

(DB 방식으로 구현했으므로, 유저 정보를 가져와서 인증 절차를 실시하고 이에 대한 결과물은 UserDetails 와 Authentication 에 담긴다, 그리고 이들은 다시 AuthService 로 전달된다)

 

- SecurityContextHolder 에 이 인증 내용을 담는다

 

- JWT 를 생성하고 유저에게 넘겨준다.

 

 

 

 

*  Spring Boot 와 JWT 를 통한 로그인 코드들

 : 위 내용을 Spring Boot 에서 코드로 구현하면 다음과 같다

 

0) SecurityConfig

: 이전 포스팅(https://sdy-study.tistory.com/269) 에서 아래 내용을 추가

@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
    private final UserDetailsService userDetailsService;
 
    @Bean(BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
 
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
    }
}
cs

 

 

 

1) Controller

@RestController
@RequestMapping("/api/auth")
@AllArgsConstructor
public class AuthController {
 
    private final AuthService authService;
 
    @PostMapping("/login")
    public AuthenticationResponse login(@RequestBody LoginRequest loginRequest) {
        return authService.login(loginRequest);
    }
}
cs

LoginRequest 는 유저가 입력한 로그인 값을 담는 DTO 이다.

 

 

2) Service

@Service
@Slf4j
@AllArgsConstructor
public class AuthService {
 
    private final AuthenticationManager authenticationManager;
    private final JWTProvider jwtProvider;
    
    public AuthenticationResponse login(LoginRequest loginRequest) {
        Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(),
                loginRequest.getPassword()));
        SecurityContextHolder.getContext().setAuthentication(authenticate);
        String authenticationToken = jwtProvider.generateToken(authenticate);
        return new AuthenticationResponse(authenticationToken, loginRequest.getUsername());
    }
}
cs

 

 

3) JWT 생성

 

* JWT 생성을 위한 비대칭 암호화 방식(RSA)

: 자바에서 제공하는 KeyStore 를 기반으로 RSA 방식으로 JWT 에 서명을하여 인증을 할 수 있다.

(RSA 에 대한 자세한 설명은 링크 참조 : https://mkil.tistory.com/461)

 

먼저 jks 파일을 만들어줘야 하는데 다음과 같은 명령어를 커맨드 창에 작성한다

keytool -genkey -alias [alias-이름] -keyalg [사용할 알고리즘, RSA 사용] -keystore [keystore 파일이름 test.jks 이런식] -keysize [keystore 크기, 여기선 2048 사용]
cs

입력하고 나면 비밀번호와 여러가지 내용들을 적을 것을 요청한다

임의로 작성가능 하되 비밀번호는 반드시 기억하고 있어야한다

비밀번호는 application.properties 같은 암호 정보를 담는 파일에 넣는게 좋다

 

간혹 경고문으로 PKCS12 로 바꾸라고 뜨기도 하는데 그냥 생략해도 된다.

 

만든 jks 파일은 spring boot 의 resources 폴더 안에 넣어야 한다.

 

이를 토대로 JWT 를 생성하는 자바 코드를 아래 처럼 작성한다

@Service
public class JwtProvider {
 
    private KeyStore keyStore;
 
    @PostConstruct
    public void init() {
        try {
            keyStore = KeyStore.getInstance("JKS");
            InputStream resourceAsStream = getClass().getResourceAsStream("/jks 파일 이름.jks");
            keyStore.load(resourceAsStream, "jks 생성시 지정한 비밀번호".toCharArray());
        } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
            throw new SpringRedditException("keystore 로딩 중 에러 발생");
        }
 
    }
 
    public String generateToken(Authentication authentication) {
        org.springframework.security.core.userdetails.User principal = (User) authentication.getPrincipal();
        return Jwts.builder()
                .setSubject(principal.getUsername())
                .signWith(getPrivateKey())
                .compact();
    }
 
    private PrivateKey getPrivateKey() {
        try {
            return (PrivateKey) keyStore.getKey("jks 파일 생성시 만든 alias""jks 생성시 지정한 비밀번호".toCharArray());
        } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
            throw new SpringRedditException("Exception occured while retrieving public key from keystore");
        }
    }
}
cs

 

여기까지 하면 아래 처럼 "JWT 생성" 까지만 끝난 것이다.

 

 

아직 JWT 에 대한 검증 작업이 전혀 이뤄지지 않았다

이는 다음 포스팅에서 로그아웃과 함께 다룬다.

728x90