본문 바로가기
Spring/Spring

[Spring] JWT 구현하기 (1)

by J4J 2021. 4. 8.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 Server에서 JWT 구현하는 방법에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

들어가기에 앞서 이전에 작성된 포스팅을 참고하시면 이해가 더 잘되실 겁니다.

 

2021.04.07 - [Spring/Spring] - [Spring] JWT란?

 

 

 

이번 포스팅에서는 스프링 설정만 진행해보고자 합니다.

 

Client 부분은 다음 포스팅에서 작성하여 이번에 구현한 스프링과 서로 통신되는 과정을 보여드리도록 하겠습니다.

 

스프링에서 JWT를 구현을 위한 키워드는 Interceptor를 이용하는 것입니다.

 ※ Interceptor에 대해 모르신다면? 2021.03.01 - [Spring/Spring] - [Spring] Filter / Interceptor

 

Client에서 보낸 요청이 넘어오면 Interceptor에서 토큰 정보를 확인한 뒤 유효한 토큰이라면 비즈니스 로직 처리를 진행하고 유효한 토큰이 아니라면 에러를 강제로 유발하는 것입니다.

 

지금부터 Interceptor를 활용하여 JWT를 구현해보도록 하겠습니다.

 

 

JWT 구현 방법

 

[ 1. pom.xml에 maven 설정 추가 ]

 

<!-- JWT -->
<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.9.1</version>
</dependency>

 

 

[ 2. 사용자 정보를 담는 User 클래스 생성 ]

 

package com.spring.jwt.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
	private String id;
	private String password;
	private String name;
	private int age;
}

 

 

※ Lombok에 대해 모르신다면? 2021.03.02 - [Spring/Spring] - [Spring] Lombok

 

 

반응형

 

 

[ 3. Token 관련 메서드가 담겨있는 JwtServiceImpl 클래스 생성 ]

 

package com.spring.jwt.service;

import java.util.Date;
import java.util.Map;

import org.springframework.stereotype.Component;

import com.spring.jwt.dto.User;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

@Component
public class JwtServiceImpl {
	private String secretKey = "myKey"; // 서명에 사용할 secretKey
	private long exp = 1000L * 60 * 60; // 토큰 사용가능 시간, 1시간
	
	// 토큰 생성하는 메서드
	public String createToken(User user) { // 토큰에 담고싶은 값 파라미터로 가져오기
		return Jwts.builder()
				   .setHeaderParam("typ", "JWT") // 토큰 타입
				   .setSubject("userToken") // 토큰 제목
				   .setExpiration(new Date(System.currentTimeMillis() + exp)) // 토큰 유효시간
				   .claim("user", user) // 토큰에 담을 데이터
				   .signWith(SignatureAlgorithm.HS256, secretKey.getBytes()) // secretKey를 사용하여 해싱 암호화 알고리즘 처리
				   .compact(); // 직렬화, 문자열로 변경
	}
	
	// 토큰에 담긴 정보를 가져오기 메서드
	public Map<String, Object> getInfo(String token) throws Exception {
		Jws<Claims> claims = null;
		try {
			claims = Jwts.parser().setSigningKey(secretKey.getBytes()).parseClaimsJws(token); // secretKey를 사용하여 복호화
		} catch(Exception e) {
			throw new Exception();
		}
		
		return claims.getBody();
	}
	
	// interceptor에서 토큰 유효성을 검증하기 위한 메서드
	public void checkValid(String token) {
		Jwts.parser().setSigningKey(secretKey.getBytes()).parseClaimsJws(token);
	}
}

 

 

[ 4. JWT 적용을 위한 JwtInterceptor 클래스 생성 ]

 

package com.spring.jwt.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import com.spring.jwt.service.JwtServiceImpl;

@Component
public class JwtInterceptor implements HandlerInterceptor {
	
	@Autowired
	private JwtServiceImpl jwtService;
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		
		if(request.getMethod().equals("OPTIONS")) { // preflight로 넘어온 options는 통과
			return true;
		} else {
			String token = request.getHeader("jwt-auth-token"); // client에서 요청할 때 header에 넣어둔 "jwt-auth-token"이라는 키 값을 확인
			if(token != null && token.length() > 0) {
				jwtService.checkValid(token); // 토큰 유효성 검증
				return true;
			} else { // 유효한 인증토큰이 아닐 경우
				throw new Exception("유효한 인증토큰이 존재하지 않습니다.");
			}
		}
	}
}

 

 

[ 5. ServletContext에 JwtIntetceptor 등록 ]

 

package com.spring.jwt.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.spring.jwt.interceptor.JwtInterceptor;

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.spring.jwt.controller", "com.spring.jwt.interceptor"}) // Interceptor도 스캔
public class ServletContext implements WebMvcConfigurer {

	...
    
	// Interceptor 등록
	@Autowired
	JwtInterceptor jwtInterceptor;
	
	@Override
	public void addCorsMappings(CorsRegistry registry) { // client에서 header추출이 가능하도록 하기 위해 등록
		registry.addMapping("/**")
				.allowedOrigins("*")
				.allowedMethods("*")
				.allowedHeaders("*")
				.exposedHeaders("jwt-auth-token");
	}
	
	@Override
	public void addInterceptors(InterceptorRegistry registry) { // 인터셉터 등록
		registry.addInterceptor(jwtInterceptor)
				.addPathPatterns("/**") // Interceptor가 적용될 경로
				.excludePathPatterns(new String[]{"/excludePath/**"}); // Interceptor가 적용되지 않을 경로
	}
}

 

 

728x90

 

 

참고적으로 JwtServiceImpl 클래스는 RootContext에서 패키지를 스캔하고 있습니다.

 

package com.spring.jwt.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = {"com.spring.jwt.service"})
public class RootContext {

}

 

 

[ 6. Controller 클래스 생성 ]

 

package com.spring.jwt.controller;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.spring.jwt.dto.User;
import com.spring.jwt.service.JwtServiceImpl;

@RestController
@CrossOrigin("*")
public class MyController {
	
	@Autowired
	JwtServiceImpl jwtService;
	
	@GetMapping("/getUser") // 토큰에 담겨있는 사용자 정보를 리턴, 토큰이 필요한 경로
	public ResponseEntity<Object> getUser(HttpServletRequest request) {
		try {
			String token = request.getHeader("jwt-auth-token");
			Map<String, Object> tokenInfoMap = jwtService.getInfo(token);
			
			User user = new ObjectMapper().convertValue(tokenInfoMap.get("user"), User.class);
			
			return new ResponseEntity<Object>(user, HttpStatus.OK);
		} catch(Exception e) {
			return new ResponseEntity<Object>(null, HttpStatus.CONFLICT);
		}
	}
	
	@PostMapping("/excludePath/login") // 로그인, 토큰이 필요하지 않는 경로
	public ResponseEntity<Object> login(@RequestBody User user, HttpServletResponse response) {
		try {
			User DBUser = new User(); // 원래는 DB에 저장되어 있는 사용자 정보 가져와야 하는 부분
			DBUser.setId("rhemddj");
			DBUser.setPassword("12345");
			
			if(DBUser.getId().equals(user.getId()) && DBUser.getPassword().equals(user.getPassword())) { // 유효한 사용자일 경우
				String token = jwtService.createToken(user); // 사용자 정보로 토큰 생성
				response.setHeader("jwt-auth-token", token); // client에 token 전달
				return new ResponseEntity<Object>("login Success", HttpStatus.OK);
			} else {
				return new ResponseEntity<Object>("login Fail", HttpStatus.OK);
			}
		} catch(Exception e) {
			return new ResponseEntity<Object>(null, HttpStatus.CONFLICT);
		}
	}
}

 

 

ServletContext에 Interceptor를 등록할 때 /excludePath로 시작하는 경로는 JWT 토큰을 확인하지 않도록 설정해놨습니다.

 

그리고 JWT 토큰을 확인하지 않아도 되는 경우는 일반적으로 다음과 같은 경우가 존재합니다.

 

  • 사용자 로그인이 필요 없는 기능을 사용하는 경우
  • 사용자 로그인을 할 경우

 

그렇기 때문에 로그인을 위한 매핑은 JWT 토큰을 확인하지 않고 존재하는 사용자 정보일 경우 토큰을 발급하는 역할을 합니다.

 

그리고 토큰이 정상적으로 발급된 후 발급된 토큰을 담아 getUser요청을 보내게 되면 Token에 담긴 user정보를 리턴해주고 만약 토큰을 담지 않고 getUser요청을 보내면 에러가 발생되며 "유효한 인증 토큰이 존재하지 않습니다."라는 메시지가 나오게 됩니다.

 

제가 말한 대로 동작이 되는지는 다음 포스팅에서 Client 코드를 작성한 뒤 테스트를 해보도록 하겠습니다.

 

참고로 React를 이용하여 구현할 예정입니다.

 

 

참조

 

Vue+Boot 환경에서 JWT를 이용한 인증 처리 2

 

 

 

이상으로 Server에서 JWT 구현하는 방법에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

728x90
반응형

댓글