JWT 맛보기의 마침표를 찍을 수 있는 포스팅이다. 이번에는 기존에 구현해놓은 기능들을 MariaDB와 JPA를 사용하여 완성해보고자 한다.
윈도우 로컬에 MariaDB와 WorkBench 격인 HeidiSQL을 미리 준비해준다. 또한 스프링 프로젝트의
build.gradle에도 관련 dependency를 추가해주고,// MariaDB runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' // JPA implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
application.properties에도 관련 내용을 추가해준다. 나는 database명을jpatest로 했다.spring.datasource.url=jdbc:mariadb://localhost:3307/jpatest spring.datasource.username=본인 마리아DB ID spring.datasource.password=본인 마리아DB 비밀번호 spring.jpa.hibernate.ddl-auto=update spring.datasource.driverClassName=org.mariadb.jdbc.Driver
JPA를 활용하기 위해서는, DTO class를
@Entity를 붙인다고 한다. 그러면 이게 하나의 테이블이 된다(사실 잘모르)고 하는 것 같다. 기존에 만들어 둔User.java를 다음과 같이 수정하자.
package com.example.securitytest;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.Serializable;
import java.util.*;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Entity
public class User implements UserDetails, Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String email;
private String password;
@ElementCollection(fetch = FetchType.EAGER) //"failed to lazily initialize a collection of role" 오류 발생하여 추가
@Builder.Default
private Set<Role> roles = new HashSet<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.roles;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.email;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
또한 RefreshToken을 저장해야하니 해당 클래스도 만들어 주자.
package com.example.securitytest;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.*;
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Getter
@Setter
public class RefreshToken {
@Id
private String email;
private String refreshToken;
}
또한 Repository도 만들어야 한댄다. 약간 DAO느낌?
package com.example.securitytest;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, String> {
Optional<User> findByEmail(String email);
}
package com.example.securitytest;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, String> {
Optional<RefreshToken> findByEmail(String email);
Optional<RefreshToken> findByRefreshToken(String refreshToken);
}
차근차근 수정해보자. 우선 회원가입. DB를 사용하지 않았기에 회원가입은 거의 스킵하다시피 했다. 이제 제대로 구현해보자.
//UserService.java
public User signup(SignUpRequest request){
userRepository.findByEmail(request.email()).ifPresent(
(user) -> {throw new RuntimeException("이미 가입된 이메일입니다.");}
);
User user = User.builder()
.email(request.email()).password(request.password())
.roles(Set.of(Role.USER))
.build();
return userRepository.save(user);
}
사실 별거 없다. User테이블에 이메일이 있는지 검사하고, 없으면 User테이블에 추가해준다. 아까 위에서 만든
UserRepository를 사용하는건데,.save()메소드를 통해 데이터를 추가할 수 있다고 한다. (아까UserRepository만들었을 때save()메소드는 따로 재정의하지 않았다.)
이제 로그인을 수정해보자.
//UserController.java
@PostMapping("/login")
public JwtTokenResponse login(@RequestBody LoginUserRequest request, HttpServletResponse response){
log.info("controller login 진입");
User user = userService.login(request);
JwtTokenResponse jwtTokenResponse = jwtTokenProvider.makeJwtTokenResponse(user);
//DB에 refreshtoken 저장
System.out.println(refreshTokenRepository);
Optional<RefreshToken> currentRefreshTokenDto = refreshTokenRepository.findByEmail(user.getEmail());
if(currentRefreshTokenDto.isPresent()){
refreshTokenRepository.delete(currentRefreshTokenDto.get());
}
refreshTokenRepository.save(
RefreshToken.builder()
.email(user.getEmail()).refreshToken(jwtTokenResponse.refreshToken())
.build());
return jwtTokenResponse;
}
//UserService.java
public User login(LoginUserRequest request){
log.info("login 진입 {}",request);
//DB에서 id, pw 확인
Optional<User> foundUser = userRepository.findByEmail(request.email());
if(foundUser.isPresent()){
User user = foundUser.get();
if(request.password().equals(foundUser.get().getPassword())){
return user;
}
}
throw new RuntimeException("아이디가 존재하지 않거나 아이디나 비밀번호가 일치하지 않습니다.");
}
우선,
UserService의login메소드에서는 DB에서 해당 이메일이 있는지Optional<>을 이용해 확인하고,isPresent()함수를 이용해 있다면 비밀번호까지 같은지 비교해준다. 아닐 경우에는 Exception을 발생시켜 주도록 했다.
그다음엔,
jwtTokenProvider에서accessToken과refreshToken을 만들어주도록 한다. 만드는 과정은 이전에 포스팅했으니 생략하고, 이 때 만든refreshToken을 다시UserController로 가져와서 DB에 저장을 한다. 이때, 혹시나 남아있다면 삭제를 하고 추가해주도록 한다.
/test)
UserController를 아래처럼 수정해주었다.
// @Transactional //"failed to lazily initialize a collection of role" 오류 발생하여 추가
@GetMapping("/test")
public String test(@AuthenticationPrincipal User user){
log.info("test완료 : {}",user);
return "test완료 " + user.getEmail();
}
아직 잘 모르겠지만 주석과 같은 오류가 났고,
@Transactional붙이면 해결된다길래 붙였다가 해결이 안 돼서User.java의roles필드 위의@ElementCollection옆에(fetch = FetchType.EAGER)를 붙였다. 사실상 지금은@Transactional를 지워도 오류가 안 난다.
AccessToken 재발급
accessToken만료 시refreshToken을 가지고accessToken을 재발급하는 부분이 있었다. 아래와 같이 수정하자.
//UserController.java
@PostMapping("/refreshtoken")
public JwtTokenResponse updateAccessToken(@RequestBody UpdateAccessTokenRequest request){
//DB에 해당 RefreshToken이 있는지 확인
// String email = userService.findEmailByRefreshToken(request.refreshToken());
Optional<RefreshToken> refreshTokenDto = refreshTokenRepository.findByRefreshToken(request.refreshToken());
log.info("refreshToken : {}", refreshTokenDto);
if(refreshTokenDto.isEmpty()) {
throw new RuntimeException("해당 이메일로 로그인된 기록이 없습니다.");
}
//DB에 해당 RefreshToken이 있어도 만료여부를 검사해야 함.
if (jwtTokenProvider.validateToken(refreshTokenDto.get().getRefreshToken()) != JwtCode.ACCESS) {
// return jwtTokenProvider.makeJwtTokenResponseWithNull();
refreshTokenRepository.delete(refreshTokenDto.get());
throw new RuntimeException("refreshtoken이 남아있지만 만료되었습니다. 자동삭제합니다.");
}
User user = userService.findUserByEmail(refreshTokenDto.get().getEmail());
String accessToken = jwtTokenProvider.makeAccessToken(user.getEmail(), user.getRoles());
return jwtTokenProvider.makeJwtTokenResponseWithToken(accessToken, request.refreshToken());
}
//UserService.java
public User findUserByEmail(String email) {
//실제 DB에서 User 찾아서 갖고옴.
return userRepository.findByEmail(email)
.orElseThrow(() -> new RuntimeException("email에 해당하는 user가 없습니다."));
}
코드는 길어보이지만 우리는 이미 로직을 이해하고 있기에 어렵지 않다. 처음에 DB에
refreshToken이 있는지 확인하고(사실상 없으면 말이 안 됨), 있을 때 가져와서 만료여부를 검사한다. 만약에 만료된 토큰이라면 DB에서 해당 토큰을 삭제하고 예외를 발생시킨다. 아직 만료가 되지 않은 토큰이라면 그refreshToken에서 User 정보를 뽑아와 새롭게accessToken을 만들어서 보내주는 로직이다.
이전에 로직을 거의 다 짜놓고 이번에는 DB와 연결한 것 뿐이니까 어렵지 않았다. 다만 기타 작은 이슈들은 트러블슈팅에 정리해야겠다.