Skip to content

Commit

Permalink
Merge pull request #128 from H-Hive/feature/user
Browse files Browse the repository at this point in the history
feat: Refresh Token 추가
  • Loading branch information
EUNCHAEv1006 authored Feb 6, 2024
2 parents bdb158f + 5305fc8 commit e2313bc
Show file tree
Hide file tree
Showing 17 changed files with 330 additions and 30 deletions.
16 changes: 9 additions & 7 deletions src/main/java/com/HHive/hhive/domain/user/UserDetailsImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import com.HHive.hhive.domain.user.entity.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Collections;

public class UserDetailsImpl implements UserDetails {

Expand All @@ -20,36 +22,36 @@ public User getUser() {

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
return Collections.emptyList();
}

@Override
public String getPassword() {
return null;
return user.getPassword();
}

@Override
public String getUsername() {
return null;
return user.getUsername();
}

@Override
public boolean isAccountNonExpired() {
return false;
return true;
}

@Override
public boolean isAccountNonLocked() {
return false;
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return false;
return true;
}

@Override
public boolean isEnabled() {
return false;
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@
import com.HHive.hhive.domain.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserDetailsService {
public class UserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService {

private final UserRepository userRepository;

public UserDetails getUserDetails(String username) {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(UserNotFoundException::new);
return new UserDetailsImpl(user);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
import com.HHive.hhive.domain.user.UserDetailsImpl;
import com.HHive.hhive.domain.user.dto.*;
import com.HHive.hhive.domain.user.entity.User;
import com.HHive.hhive.domain.user.service.AuthService;
import com.HHive.hhive.domain.user.service.EmailService;
import com.HHive.hhive.domain.user.service.KakaoService;
import com.HHive.hhive.domain.user.service.UserService;
import com.HHive.hhive.global.common.CommonResponse;
import com.HHive.hhive.global.jwt.JwtUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
Expand All @@ -19,6 +21,7 @@
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequiredArgsConstructor
Expand All @@ -29,6 +32,7 @@ public class UserController {
private final JwtUtil jwtUtil;
private final KakaoService kaKaoService;
private final EmailService emailService;
private final AuthService authService;

@PostMapping("/signup")
public ResponseEntity<CommonResponse<Void>> signup(
Expand Down Expand Up @@ -71,7 +75,13 @@ public ResponseEntity<CommonResponse<UserInfoResponseDTO>> login(

UserInfoResponseDTO userInfo = userService.login(requestDTO);

response.setHeader(JwtUtil.AUTHORIZATION_HEADER, jwtUtil.createToken(requestDTO.getUsername()));
// 액세스 토큰 생성 및 헤더에 저장
String accessToken = jwtUtil.createToken(requestDTO.getUsername());
response.setHeader("Access-Token", accessToken);

// 리프레시 토큰 생성 및 DB에 저장
String refreshToken = authService.createRefreshToken(userInfo.getUserId());
response.setHeader("Refresh-Token", refreshToken);

return ResponseEntity.ok().body(CommonResponse.of("로그인 성공", userInfo));
}
Expand Down Expand Up @@ -159,4 +169,13 @@ public ResponseEntity<CommonResponse<UserInfoResponseDTO>> kakaoLogin(@RequestPa

return ResponseEntity.ok().body(CommonResponse.of("카카오 로그인 성공", userInfo));
}

// 리프레시 토큰을 이용한 액세스 토큰 갱신
@PostMapping("/refresh-token")
public ResponseEntity<CommonResponse<String>> refreshToken(@RequestBody RefreshTokenDTO refreshTokenDTO) {

String newAccessToken = authService.createAccessTokenWithRefreshToken(refreshTokenDTO.getRefreshToken());

return ResponseEntity.ok().body(CommonResponse.of("액세스 토큰 갱신 성공", newAccessToken));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.HHive.hhive.domain.user.dto;

import lombok.Data;

@Data
public class RefreshTokenDTO {

String refreshToken;
}
39 changes: 39 additions & 0 deletions src/main/java/com/HHive/hhive/domain/user/entity/Token.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.HHive.hhive.domain.user.entity;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "tokens")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
public class Token {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column
private String refreshToken;

@Column
private boolean expired;

@Column
private boolean revoked;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;

public void setExpired(boolean expired) {
this.expired = expired;
}

public void setRevoked(boolean revoked) {
this.revoked = revoked;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.HHive.hhive.domain.user.repository;

import com.HHive.hhive.domain.user.entity.Token;
import com.HHive.hhive.domain.user.entity.User;
import com.HHive.hhive.global.exception.common.ErrorCode;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

public interface TokenRepository extends JpaRepository<Token, Long> {

List<Token> findByUser(User user);


Token findByRefreshToken(String token);

@Modifying
@Transactional
@Query("update Token t set t.expired = true, t.revoked = true where t.refreshToken = :token")
void setExpiredAndRevoked(String token);
}
88 changes: 88 additions & 0 deletions src/main/java/com/HHive/hhive/domain/user/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.HHive.hhive.domain.user.service;

import com.HHive.hhive.domain.user.entity.Token;
import com.HHive.hhive.domain.user.entity.User;
import com.HHive.hhive.domain.user.repository.TokenRepository;
import com.HHive.hhive.domain.user.repository.UserRepository;
import com.HHive.hhive.global.exception.jwt.ExpiredRefreshTokenException;
import com.HHive.hhive.global.exception.jwt.RefreshTokenNotFoundException;
import com.HHive.hhive.global.exception.jwt.RevokedRefreshTokenException;
import com.HHive.hhive.global.exception.user.UserNotFoundException;
import com.HHive.hhive.global.jwt.JwtUtil;
import io.jsonwebtoken.ExpiredJwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AuthService {

private final TokenRepository tokenRepository;
private final UserRepository userRepository;
private final JwtUtil jwtUtil;

@Autowired
public AuthService(TokenRepository tokenRepository, UserRepository userRepository, JwtUtil jwtUtil) {
this.tokenRepository = tokenRepository;
this.userRepository = userRepository;
this.jwtUtil = jwtUtil;
}

@Transactional
public String createRefreshToken(Long userId) {

// DB에서 User 객체를 조회
User user = userRepository.findById(userId)
.orElseThrow(UserNotFoundException::new);

// 토큰 만료시간 7일
long TOKEN_VALIDITY = 7 * 24 * 60 * 60 * 1000L;

// JwtUtil 클래스의 메서드를 호출하여 리프레시 토큰을 생성
String refreshToken = jwtUtil.createToken(user.getUsername(), TOKEN_VALIDITY);

Token tokenEntity = Token.builder()
.refreshToken(refreshToken)
.user(user)
.expired(false)
.revoked(false)
.build();

tokenRepository.save(tokenEntity);

return "Bearer " + refreshToken;
}

@Transactional
public String createAccessTokenWithRefreshToken(String refreshToken) {

System.out.println(refreshToken);

// 데이터베이스에서 리프레시 토큰 조회
Token tokenEntity = tokenRepository.findByRefreshToken(refreshToken);

if (tokenEntity == null) {
throw new RefreshTokenNotFoundException();
}

try {
// 리프레시 토큰을 검증
jwtUtil.validationToken(refreshToken);

// 리프레시 토큰이 사용 중지되었다면 예외를 발생
if (tokenEntity.isRevoked()) {
throw new RevokedRefreshTokenException();
}

// 액세스 토큰의 유효시간을 1시간으로 설정
long TOKEN_VALIDITY = 60 * 60 * 1000L;

// 새로운 액세스 토큰을 생성하고 반환
return jwtUtil.createToken(tokenEntity.getUser().getUsername(), TOKEN_VALIDITY);

} catch (ExpiredJwtException e) {
// 리프레시 토큰이 만료되었다면 예외를 발생
throw new ExpiredRefreshTokenException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.HHive.hhive.domain.user.service;

import com.HHive.hhive.domain.user.entity.Token;
import com.HHive.hhive.domain.user.repository.TokenRepository;
import jakarta.persistence.EntityManager;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class LogoutService implements LogoutHandler {

private final TokenRepository tokenRepository;
private final EntityManager entityManager;

public LogoutService(TokenRepository tokenRepository, EntityManager entityManager) {
this.tokenRepository = tokenRepository;
this.entityManager = entityManager;
}

@Override
@Transactional
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {

final String authHeader = request.getHeader("Refresh-Token");
final String refreshToken;

if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return;
}

refreshToken = authHeader.substring(7);

Token storedToken = tokenRepository.findByRefreshToken(refreshToken);

if (storedToken != null) {
storedToken.setExpired(true);
storedToken.setRevoked(true);

tokenRepository.setExpiredAndRevoked(refreshToken);
}
}
}
Loading

0 comments on commit e2313bc

Please sign in to comment.