1. RefreshToken
@Getter
@NoArgsConstructor
@Entity
public class RefreshToken {
@Id
@Column(name = "rt_key")
private String key;
@Column(name = "rt_value")
private String value;
@Builder
public RefreshToken(String key, String value){
this.key = key;
this.value = value;
}
public RefreshToken updateValue(String token){
this.value = token;
return this;
}
}
보통은 Token 이 만료될 때 자동으로 삭제 처리 하기 위해 Redis 를 많이 사용하지만, 여기서는 RDB 에 저장하는 방식으로 구현했다. 만약 지금 예제처럼 RDB 를 저장소로 사용한다면 배치 작업을 통해 만료된 토큰들을 삭제해주는 작업이 필요한데, 생성/수정 시간 컬럼을 추가하여 배치 작업으로 만료된 토큰들을 삭제해주어야 한다. (여기서는 그 부분을 추가 안한 듯)
key에는 Member ID 값이 들어가고, value 에는 Refresh Token String 이 들어간다.
2. RefreshTokenRepository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
Optional<RefreshToken> findByKey(String key);
}
Member ID 값으로 토큰을 가져오기 위해 findByKey 를 추가한다.
3. AuthController
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
@Log4j2
public class AuthController {
private final AuthService authService;
@PostMapping("/signup") // 회원가입
public ResponseEntity<MemberResponseDto> signup(@RequestBody MemberRequestDto memberRequestDto){
return ResponseEntity.ok(authService.signup(memberRequestDto));
}
@PostMapping("/login") // 로그인
public ResponseEntity<TokenDto> login (@RequestBody MemberRequestDto memberRequestDto){
return ResponseEntity.ok(authService.login(memberRequestDto));
}
@PostMapping("/reissue") // 재발급
public ResponseEntity<TokenDto> reissue(@RequestBody TokenRequestDto tokenRequestDto){
return ResponseEntity.ok(authService.reissue(tokenRequestDto));
}
}
회원가입 / 로그인 / 재발급 을 처리하는 API. SecurityConfig 에서 /auth/** 요청은 전부 허용했기 때문에 토큰 검증 로직을 타지 않는다. MemberRequestDto 에는 사용자가 로그인 시도한 ID / PW String 이 존재하고, TokenRequestDto 에는 재발급을 위한 AccessToken / RefreshToken String 이 존재한다.
4. AuthService
@Service
@RequiredArgsConstructor
public class AuthService {
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final TokenProvider tokenProvider;
private final RefreshTokenRepository refreshTokenRepository;
@Transactional
public MemberResponseDto signup(MemberRequestDto memberRequestDto){
if (memberRepository.existsByUid(memberRequestDto.getUid())){
throw new RuntimeException("이미 가입되어 있는 회원입니다.");
}
Member member = memberRequestDto.toMember(passwordEncoder);
return MemberResponseDto.of(memberRepository.save(member));
}
@Transactional
public TokenDto login(MemberRequestDto memberRequestDto){
// 1. Login Id/PW 를 기반으로 AuthenticationToken 생성
UsernamePasswordAuthenticationToken authenticationToken = memberRequestDto.toAuthentication();
// 2. 실제로 검증 (사용자 비밀번호 체크) 이 이루어지는 부분
// authenticate 메서드가 실행이 될 때 CustomUserDetailsService 에서 만들었던 loadUserByUsername 메서드가 실행됨
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
// 3. 인증 정보를 기반으로 JWT 토큰 생성
TokenDto tokenDto = tokenProvider.generateTokenDto(authentication);
// 4. RefreshToken 저장
RefreshToken refreshToken = RefreshToken.builder()
.key(authentication.getName())
.value(tokenDto.getRefreshToken())
.build();
refreshTokenRepository.save(refreshToken);
// 5. 토큰 발급
return tokenDto;
}
@Transactional
public TokenDto reissue(TokenRequestDto tokenRequestDto){
// 1. Refresh Token 검증
if (!tokenProvider.validateToken(tokenRequestDto.getRefreshToken())){
throw new RuntimeException("Refresh Token 이 유효하지 않습니다.");
}
// 2. Access Token 에서 Member ID 가져오기
Authentication authentication = tokenProvider.getAuthentication(tokenRequestDto.getRefreshToken());
// 3. 저장소에서 Member ID 를 기반으로 Refresh Token 값 가져옴
RefreshToken refreshToken = refreshTokenRepository.findByKey(authentication.getName())
.orElseThrow(() -> new RuntimeException("로그아웃 된 회원입니다."));
// 4. Refresh Token 일치하는지 검사
if (!refreshToken.getValue().equals(tokenRequestDto.getRefreshToken())){
throw new RuntimeException("토큰의 유저 정보가 일치하지 않습니다.");
}
// 5. 새로운 토큰 생성
TokenDto tokenDto = tokenProvider.generateTokenDto(authentication);
// 6. 저장소 정보 업데이트
RefreshToken newRefreshToken = refreshToken.updateValue(tokenDto.getRefreshToken());
refreshTokenRepository.save(newRefreshToken);
// 토큰 발급
return tokenDto;
}
}
- signup (회원가입 메서드)
memberRequestDto 에 회원 정보를 받아서 저장한다.
- login (로그인 메서드)
1. login form을 받아 ID/PW로 UsernamePasswordAuthenticationToken을 생성한다.
2. AuthenticationManager의 authenticate() 메서드로 검증을 받고, Authentication 객체에 회원 정보를 넣는다.
3. tokenProvider의 generateTokenDto로 토큰을 생성한다. (Access Token, Refresh Token)
4. Refresh Token을 DB에 저장한다.
5. TokenDto를 반환한다. - 토큰 발급
- reissue (토큰 재발급)
1. 토큰이 만료되었는지 검증.
2. Access Token에서 Member ID 를 가져온다. (TokenProvider의 getAuthentication 메서드에서 토큰을 복호화 함)
3. Member ID를 기반으로 Refresh Token 값을 가져온다.
4. Dto로 받은 Refresh Token 가 저장소에 있는 Refresh Token 와 일치하는 지 검사.
5. 토큰을 생성한다.
6. Refresh 토큰의 재사용을 막기 위해 저장소의 Refresh Token을 업데이트 해 준다.
7. TokenDto를 반환한다. - 토큰 발급
5. CustomUserDetailsService
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return memberRepository.findByUid(username)
.map(this::createUserDetails)
.orElseThrow(() -> new UsernameNotFoundException(username + "데이터베이스에서 찾을 수 없습니다."));
}
// DB 에 User 값이 존재한다면 UserDetails 객체로 만들어서 리턴
private UserDetails createUserDetails(Member member) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(member.getMemberRole().toString());
return new User(
String.valueOf(member.getUid()),
member.getPw(),
Collections.singleton(grantedAuthority)
);
}
}
UserDetails와 Authentication의 비밀번호를 비교하고 검증한다.
createUserDetails : Member Entity를 UserDetails로 만든다. User 객체를 만들어서 반환한다.
loadUserByUsername : 이메일을 받아서 UserDetails 반환한다. email로 회원을 찾고 createUserDetails()로 UserDetails 객체로 만들어준다.
참고 : https://bcp0109.tistory.com/301
'springboot 개인프로젝트 기록' 카테고리의 다른 글
06. Spring Security + JWT를 이용한 로그인, 회원가입 구현 (3) (0) | 2024.03.30 |
---|---|
05. Spring Security + JWT를 이용한 로그인, 회원가입 구현 (2) (1) | 2024.03.29 |
04. Spring Security + JWT를 이용한 로그인 구현 (1) (0) | 2024.03.28 |
03. 부트스트랩 템플릿 적용 (0) | 2024.03.26 |
02. 1차 ERD 구성 및 구현할 기능들 (0) | 2024.03.22 |