User
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "user_tb")
@Getter
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
private String email;
@Enumerated(EnumType.STRING)
private ProviderEnum provider;
@CreationTimestamp
private Timestamp createdAt;
@Builder
public User(Integer id, String username, String password, String email, ProviderEnum provider, Timestamp createdAt) {
this.id = id;
this.username = username;
this.password = password;
this.email = email;
this.provider = provider;
this.createdAt = createdAt;
}
}
UserController
@RequiredArgsConstructor
@Controller
public class UserController {
private final UserService userService;
private final HttpSession session;
@GetMapping("/oauth")
public String oauth(@RequestParam("code") String code) {
User sessionUser = userService.카카오로그인(code);
session.setAttribute("sessionUser", sessionUser);
return "redirect:/";
}
// 카카오 로그인 메서드 외에는 생략
}
UserRepository
// JpaRepository 상속하면 기본 CRUD 만들어줍니다.
public interface UserRepository extends JpaRepository<User, Integer> {
// 추가적으로 필요한 메서드는 개발자가 작성
@Query("SELECT u FROM User u WHERE u.username = :username")
Optional<User> findByUsername(@Param("username") String username);
}
UserService(로그인 핵심 로직)
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
public class UserService {
private final UserRepository userRepository;
public User 카카오로그인(String code) {
// 1. 카카오 로그인 요청
UserResponse.KakaoDTO kakaoDTO = MyHttpUtil.post(code);
// 2. Id token 검증
UserResponse.IdTokenDTO idTokenDTO = MyRSAUtil.verify(kakaoDTO.getIdToken());
// 3. 회원가입 유무 확인
String username = "kakao_" + idTokenDTO.getSub();
Optional<User> userOP = userRepository.findByUsername(username);
// 4. 회원가입 안 되어있다면 강제 회원가입
if (userOP.isEmpty()) {
User user = User.builder()
.username(username)
.password(UUID.randomUUID().toString())
.provider(ProviderEnum.KAKAO)
.build();
User userPS = userRepository.save(user);
return userPS;
}
// 5. User 객체 리턴
return userOP.get();
}
}
- String username = "kakao_" + idTokenDTO.getSub();
- id 토큰의 sub는 카카오 로그인 내에서는 중복되지 않습니다 → username으로 활용
- password(UUID.randomUUID().toString())
- 사용할 일이 없는 데이터입니다. 중복만 주의하면 됩니다.
- userRepository.save(user)
- JpaRepository에 정의되어 있습니다.
- <S extends T> S save(S entity)
- provider(ProviderEnum.KAKAO)
- Enum 클래스를 활용해 provider 분류를 했습니다.
public enum ProviderEnum {
LOCAL, KAKAO, APPLE...;
}
userPS → 영속성 컨텍스트(Persistence Context)임을 표시
userOP → Optional임을 표시
카카오 로그인 요청
MyHttpUtil
public class MyHttpUtil {
// 카카오 로그인 요청 메서드
public static UserResponse.KakaoDTO post(String code){
String redirectUri = "http://localhost:8080/oauth";
String clientId = "730a8ec7e91f04ab2647991ef34f1f81";
RestTemplate restTemplate = new RestTemplate();
String url = "https://kauth.kakao.com/oauth/token";
// 헤더 설정
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
// 요청 바디 설정
String requestBody = """
grant_type=authorization_code&client_id=${clientId}&redirect_uri=${redirectUri}&code=${code}
""".replace("${clientId}", clientId)
.replace("${redirectUri}", redirectUri)
.replace("${code}", code);
// HttpEntity에 헤더와 바디 추가
HttpEntity<String> request = new HttpEntity<>(requestBody, headers);
// POST 요청
ResponseEntity<UserResponse.KakaoDTO> response = restTemplate.exchange(url, HttpMethod.POST, request, UserResponse.KakaoDTO.class);
// 응답 출력
UserResponse.KakaoDTO kakaoDTO = response.getBody();
return kakaoDTO;
}
}
- RestTemplate
- Spring에서 제공하는 클래스 중 하나로, RESTful 웹 서비스와의 통신을 쉽게 구현할 수 있도록 도와줍니다.
- 이를 통해 HTTP 요청(예: GET, POST, PUT, DELETE 등)을 간단하게 보낼 수 있습니다.
- UserResponse.KakaoDTO
- access_token 정보와 id_token 정보가 담겨 있습니다.
UserResponse
public class UserResponse {
@Data
public static class IdTokenDTO {
private String sub;
private String nickname;
// 지금은 안 쓰는 정보
private String aud;
@JsonProperty("auth_time")
private String authTime;
private String iss;
private String exp;
private String iat;
}
@Data
public static class KakaoDTO {
@JsonProperty("access_token")
private String accessToken;
@JsonProperty("id_token")
private String idToken;
}
}
- @JsonProperty("access_token")
- JSON 문자열의 access_token 정보를 accessToken에 넣어줍니다.
자바는 언더바(_)를 쓰지 않는 것이 좋습니다.
Id token 검증
MyRSAUtil
// implementation 'com.nimbusds:nimbus-jose-jwt:9.31'
public class MyRSAUtil {
public static UserResponse.IdTokenDTO verify(String idToken) {
// 공개키 정보
String n = "qGWf6RVzV2pM8YqJ6by5exoixIlTvdXDfYj2v7E6xkoYmesAjp_1IYL7rzhpUYqIkWX0P4wOwAsg-Ud8PcMHggfwUNPOcqgSk1hAIHr63zSlG8xatQb17q9LrWny2HWkUVEU30PxxHsLcuzmfhbRx8kOrNfJEirIuqSyWF_OBHeEgBgYjydd_c8vPo7IiH-pijZn4ZouPsEg7wtdIX3-0ZcXXDbFkaDaqClfqmVCLNBhg3DKYDQOoyWXrpFKUXUFuk2FTCqWaQJ0GniO4p_ppkYIf4zhlwUYfXZEhm8cBo6H2EgukntDbTgnoha8kNunTPekxWTDhE5wGAt6YpT4Yw";
String e = "AQAB";
BigInteger bin = new BigInteger(1, Base64.getUrlDecoder().decode(n));
BigInteger bie = new BigInteger(1, Base64.getUrlDecoder().decode(e));
RSAKey rsaKey = new RSAKey.Builder(Base64URL.encode(bin), Base64URL.encode(bie)).build();
try {
// 1. 파싱
SignedJWT signedJWT = SignedJWT.parse(idToken);
// 2. 검증
RSASSAVerifier verifier = new RSASSAVerifier(rsaKey.toRSAPublicKey());
if (signedJWT.verify(verifier)) {
System.out.println("ID Token을 검증하였습니다");
String payload = signedJWT.getPayload().toString();
ObjectMapper om = new ObjectMapper();
UserResponse.IdTokenDTO idTokenDTO = om.readValue(payload, UserResponse.IdTokenDTO.class);
return idTokenDTO;
} else {
throw new RuntimeException("id토큰 검증 실패");
}
} catch (Exception ex) {
throw new RuntimeException(ex.getMessage());
}
}
}
- UserResponse.IdTokenDTO
- sub(username으로 사용), nickname을 포함한 여러 정보가 담겨 있습니다.
위 과정은 SSR에 맞춰 작성한 코드이고 토큰을 활용할 경우 토큰 기반 인증(Token-Based Authentication)으로 무상태(stateless) 애플리케이션(CSR 방식)에 많이 씁니다.
Share article