spring boot와 flutter를 연동한 소셜로그인 구현(feat. spring security)-2

ga-bin·2023년 12월 26일
0

springsecurity

목록 보기
20/23

이번에는 jwt토큰과 관련된 로직을 해결해야한다.


jwt를 이용한 인증 과정 절차

일반적으로 jwt를 이용한 로그인 구현은 다음과 같은 과정을 통해 진행된다.

  1. 유저가 로그인에 성공하면 accessToken과 refreshToken을 발급한다.
  2. 발급한 accessToken과 refreshToken을 클라이언트에게 넘겨준다. 이 때, refreshToken은 db에 저장한다.
  3. 유저는 인증이 필요한 요청시 마다 accessToken을 넘겨준다.
  4. 서버에서는 filter를 통해 해당 accessToken을 검증한다.
  5. 검증에 실패할 경우 refreshToken을 클라이언트에게 다시 요청하고, 검증이 성공할 경우 클라이언트의 요청을 수행한다.
  6. 클라이언트가 refreshToken을 넘겨주면 해당 refreshToken을 검증한다.
  7. refreshToken도 검증이 실패하면, 로그아웃을 시키고 다시 로그인을 유도한다.

이 부분은 크게 다르지 않으므로, 해당 순서와 동일하게 진행하면 된다.


토큰 처리 클래스

우선 jwt토큰과 관련된 부분을 처리할 클래스를 생성한다. 이 클래스에는 토큰 발급, 검증, 추출, 업데이트 등 토큰과 관련된 기능들이 모여있다.

package com.project.bookforeast.common.security.service;


import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import com.project.bookforeast.common.security.error.TokenErrorResult;
import com.project.bookforeast.common.security.error.TokenException;
import com.project.bookforeast.user.dto.UserDTO;
import com.project.bookforeast.user.entity.User;
import com.project.bookforeast.user.repository.UserRepository;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import jakarta.servlet.http.HttpServletRequest;

@Service
public class JwtUtil {

	
	private final UserRepository userRepository;
	
	@Autowired
	public JwtUtil(UserRepository userRepository) {
		this.userRepository = userRepository;
	}
	
	
	@Value("${jwt.secret-key}")
	private String SECRETKEY;
	
	
	private Long ACCESS_TOKEN_EXPIRATION_PERIOD = 600000L;

	
	private Long REFRESH_TOKEN_EXPIRATION_PERIOD = 3600000L;


	public String generateAccessToken(UserDTO userDTO) {
		return createToken(ACCESS_TOKEN_EXPIRATION_PERIOD, userDTO);
	}

	
	public String generateRefreshToken(UserDTO userDTO) {
		return createToken(REFRESH_TOKEN_EXPIRATION_PERIOD, userDTO);
	}
	
	
	private String createToken(Long expirationPeriod, UserDTO userDTO) {
		Map<String, Object> claims = new HashMap<>();
		claims.put("sub", userDTO.getSocialId());
		claims.put("iss", userDTO.getSocialProvider());
		
		return Jwts.builder()
				.addClaims(claims)
				.setIssuedAt(new Date(System.currentTimeMillis()))
				.setExpiration(new Date(System.currentTimeMillis() + expirationPeriod))
				.signWith(SignatureAlgorithm.HS256, SECRETKEY)
				.compact();
	}


	
	public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
		final Claims claims = Jwts.parser().setSigningKey(SECRETKEY).parseClaimsJws(token).getBody();
		return claimsResolver.apply(claims);
	}
	
	
	
	public boolean validateAccessToken(String accessToken) {
		if(accessToken == null || accessToken.length() <= 0) {
			throw new TokenException(TokenErrorResult.ACCESS_TOKEN_NEED);
		}
		
		boolean isTokenExpired = checkTokenExpired(accessToken);
		if(isTokenExpired == true) {
			throw new TokenException(TokenErrorResult.TOKEN_EXPIRED);
		} else {
			return isTokenExpired;
		}
	}
	
	
	public Boolean validateRefreshToken(String refreshToken) {
		User user = userRepository.findByRefreshToken(refreshToken);
		
		if(user == null) {
			new TokenException(TokenErrorResult.TOKEN_EXPIRED);
		}
		
		String refreshTokenInDB = user.getRefreshToken(); 
		if(!refreshToken.equals(refreshTokenInDB) || checkTokenExpired(refreshTokenInDB)) {
			new TokenException(TokenErrorResult.TOKEN_EXPIRED);
		}
		
		return true;
	}
	
	
	public boolean checkTokenExpired(String token) {
		Date expirationDate = extractClaim(token, Claims::getExpiration);
		boolean isTokenExpired = expirationDate.after(new Date());
		return isTokenExpired;
	}

	
	public Map<String, String> initToken(UserDTO savedOrFindUser) {
		Map<String, String> tokenMap = new HashMap<>();
		String accessToken = generateAccessToken(savedOrFindUser);
		String refreshToken = generateRefreshToken(savedOrFindUser);
		
		tokenMap.put("accessToken", accessToken);
		tokenMap.put("refreshToken", refreshToken);
		
		updRefreshTokenInDB(refreshToken, savedOrFindUser);
		
		return tokenMap;
	}
	

	public Map<String, String> refreshingAccessToken(UserDTO userDTO, String refreshToken) {
		Map<String, String> tokenMap = new HashMap<>();
		String accessToken = generateAccessToken(userDTO);
		
		tokenMap.put("accessToken", accessToken);
		tokenMap.put("refreshToken", refreshToken);
		
		return tokenMap;
	}
	
	
	private void updRefreshTokenInDB(String refreshToken, UserDTO savedOrFindUser) {
		savedOrFindUser.setRefreshToken(refreshToken);
		userRepository.save(savedOrFindUser.toEntity());
	}

	
	public String extractTokenFromHeader(HttpServletRequest request) {
		String header = request.getHeader("Authorization");
		if(StringUtils.hasText(header) && header.startsWith("Bearer ")) {
			return header.substring(7);
		} else {
			throw new TokenException(TokenErrorResult.ACCESS_TOKEN_NEED);
		}
	}
}

물론 처음부터 이 메소드들을 다 작성한 것은 아니지만, 편의를 위해 한 번에 코드로 적어두었다.


토큰 발급

1편을 통해 SecurityContext에 인증정보를 등록하는 것을 성공했으면, controller에서 initToken을 통해서 토큰을 생성해 준다.

	@PostMapping("/social-login")
	public ResponseEntity<Map<String, String>> socialLogin(@RequestBody @Valid SocialLoginDTO socialLoginDTO)
	{
		
		UserDTO savedOrFindUser = userService.socialLogin(socialLoginDTO);
		securityService.saveUserInSecurityContext(socialLoginDTO);
		Map<String, String> tokenMap = jwtUtil.initToken(savedOrFindUser);
	
		return ResponseEntity.ok(tokenMap);
	}

토큰 생성시에는 refreshToken정보를 db에서 update해준다.
그 후 토큰 정보를 클라이언트에게 전달한다. 그러면 유저에게 다음과 같은 형태로 토큰 정보가 전달된다.

이번 글에서는 토큰 생성까지만 해보도록 하고, 발급된 토큰을 이용한 검증은 다음 글에서 작성하겠다.

0개의 댓글