본문 바로가기

Project

[프로젝트 Day 4] JWT 토큰 이용하기

JWT 토큰이란?

JWT는 정보를 안전하게 주고받을 수 있는 방식을 정의한 표준이에요.

사용자 정보를 담은 암호화된 토큰으로 Header 와 Payload 그리고 Signature 부분으로 나뉩니다.

해당 토큰을 유저에게 발급해 준다면, 해당 토큰이 유효한 기간 동안 서버는 토큰의 유효성만 검증하면 로그인을 허가 해 줄 수 있고, db에 접근하여 암호를 대조하는 비용을 줄일 수 도 있고, 실제 암호가 통신 환경에 많이 노출되는 것을 막아 주므로 로그인 인증으로 널리 사용되는 기술 입니다. 

 

 

JWT 토큰 발행 방법.

우선 의존성 추가 부터 진행해 줍니다.

<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-api</artifactId>
			<version>0.12.6</version>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-impl</artifactId>
			<version>0.12.6</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-jackson</artifactId>
			<version>0.12.6</version>
			<scope>runtime</scope>
		</dependency>

 

 

이제 웹에서 로그인 요청이 post 형식으로 들어왔다고 생각해 보고, 이를 처리해줄 컨트롤러를 먼저 생성해 줍니다.

@PostMapping("/login")
	@Operation(summary = "사용자 로그인 ", description = "로그인을 합니다.")
	public ResponseEntity<?> logIn(@RequestBody User user) {
		
		try {

			TokenInfo tokenInfo = authService.login(user);
		
			//RefreshToken refreshToken = new RefreshToken();
			//refreshToken.setRefreshToken(tokenInfo.getRefreshToken());
			
			return new ResponseEntity<>(tokenInfo, HttpStatus.OK);

		} catch (IllegalArgumentException e) {
			return new ResponseEntity<>(e.getMessage(), HttpStatus.UNAUTHORIZED);
		}
	}

 

여기서 TokenInfo 는 JWT 토큰을 담을 객체입니다. 추후에 refrash 토큰이나 다양한 정보를 추가하기 위해서도 필요합니다. 이제 이 tokenInfo 를 생성해 줄 authService 객체를 만들어 줍니다.

 

	public TokenInfo login(User user) {
		System.out.println(user);
		User loginUser = null;

		if (user.getUserEmail() == null || user.getUserPassword() == null)
			throw new IllegalArgumentException("로그인 입력 정보 없음");

		// 입력된 이메일 날림 => user 정보 받아오기
		User getUser = userService.searchByEmail(user.getUserEmail());
		System.out.println(getUser);

		// 아이디 또는 비밀번호 없음
		String encodePw = getUser.getUserPassword();
		if (getUser == null || !passwordEncoder.matches(user.getUserPassword(), encodePw)) { // user가 입력한 pw와 db의 암호화된
			throw new IllegalArgumentException("아이디 또는 비밀번호 틀림");
		}

		// 로그인 성공
		loginUser = getUser;

		// 회원가입 정보가 맞을 경우, 로그인 요청 시 => JWT 발급
		return jwtUtil.createTokenInfo(loginUser);

	}

 

user 를 email로 조회 한 후, 해당하는 유저가 있을 때 암호화된 비밀번호의 유효성을 판단하는 코드에요. 

passwordEncoder 라는 객체의 의존성 주입이 필요합니다. 해당 비밀번호의 유효성을 검증 받으면 해당 user 객체를 createTokenInfo 메서드로 보냅니다.

 

	public TokenInfo createTokenInfo(User user) {
		// access-token 생성
		String accessToken = createAccessToken(user);
		// refresh-token 생성
		//String refreshToken = createRefreshToken(user);

		TokenInfo tokenInfo = new TokenInfo();
		//tokenInfo.setGrantType("Bearer");
		tokenInfo.setAccessToken(accessToken);
		//tokenInfo.setRefreshToken(refreshToken);

		return tokenInfo;
	}

이제 TokenInfo 라는 token 을 담은 객체를 생성하는데요. refrash token 이나 Grant type 에 대해서는 다음시간에 공부해 보겠습니다. 이제 createAccessToken 으로 가볼게요.

	// 2. 토큰을 생성하는 메서드
	public String createToken(User user, long expirationTimeMillis) {
		String userNickname = user.getUserNickname();
		int userId = user.getUserId();

		// 토큰 유효기간 설정
		Date exp = new Date(System.currentTimeMillis() + expirationTimeMillis);

		return Jwts.builder().header().add("type", "JWT").and().claim("userNickname", userNickname)
				.claim("userId", userId).expiration(exp).signWith(secretKey).compact();
	}

	// access-token 생성
	public String createAccessToken(User user) {
		return createToken(user, 1000 * 60); // 1분

	}

 

이제 여기서 생성되는 access-token 이 앞서 말씀드린 JWT 토큰 입니다. jwt 토큰에  해당 user 정보와 만료 시간에 대한 정보를 넣어주는 것이 header 부분과 payload 부분입니다. 토큰에 저희가 원하는 정보를 임의로 추가 할 수 있습니다. 이제 토큰 발급에 Jwts.builder() 를 이용하는데, claim 메서드를 통해 payload 부분에 key-value 형태를 통해 원하는 정보를 추가합니다. 이제 여기서 중요한 Signature 부분이 있는데요. 

서버에서 토큰의 유효성을 검증하기 위한 방법으로 jwt 를 발급하는 서버는 노출되지 않는 암호문을 하나 들고 있어야 합니다.

 

	/* 토큰 SECRET KEY */
	@Value("${JWT_SECRET_KEY}")
	private String JWT_SECRET_KEY;
	private SecretKey secretKey;

	// 1. 시크릿키를 생성하는 메서드
	// @PostConstruct로 secretKey 초기화
	// Spring Bean의 초기화 후, 딱 한 번만 실행되는 메서드를 지정할 때 사용
	@PostConstruct
	private void initSecretKey() {
		byte[] keyBytes = Decoders.BASE64.decode(JWT_SECRET_KEY); // deprecated 메서드 새로 작성 방법
		this.secretKey = Keys.hmacShaKeyFor(keyBytes);
	}

JWT_SECRET_KEY 는 저희가 설정하는 암호로, git으로 공유되지 않도록 환경변수 처리를하여 @Value 어노테이션으로 주입받아 처리 했습니다. 어쨋든, 저 key 를 바탕으로 secretkey를 암호화 하여 생성합니다.

 

그리고 그 secretkey 를 jwt 생성 시에 signature 메서드에 파라미터로 넘겨주면, 해당 토큰에서 secretkey에 해당하는 서명을 생성하여 토큰에 넣어줍니다. 따라서 추후에 서버는 signature 부분의 서명 유효성을 검증하여, 서버가 가지고 있는 secretkey 와 동일한지의 여부를 판단하여 유저들의 접근 허가를 해줄 수 있습니다.

 

이렇게 생성된 TokenInfo 를 ResponseEntitiy 에 담아 반환해 주면, front 에서 해당 accesstoken 을 session 에 담아두어 추후의 모든 요청의 header 에 access-token 을 넣어 요청을 보내면 login 된 유저임을 서버에서 쉽게 확인할 수 있습니다. 

또한 javaScript 를 통해 해당 토큰의 payLoad 부분을 디코딩 하여, 서버가 보내준 정보를 페이지 렌더링에 이용할 수 도 있습니다.