From 4e3a3ea04ae898daafbf3b77742db039b3adaac4 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sat, 24 Aug 2024 01:22:14 +0900 Subject: [PATCH 01/15] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20-=20accessTo?= =?UTF-8?q?ken=EC=9D=84=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EB=A8=BC=EC=A0=80=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20-=20UserDetail=EA=B3=BC=20UserDetailService=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20-=20UserRepository=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?-=20=ED=86=A0=ED=81=B0=20=EC=83=9D=EC=84=B1,=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D,=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=95=84=EC=9A=94=ED=95=9C=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20TokenProvide?= =?UTF-8?q?r=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1=20-=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=EB=B0=9B=EC=9D=80=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=EC=9D=84=20=EA=B2=80=EC=A6=9D=ED=95=98=EA=B3=A0=20=EB=8B=A4?= =?UTF-8?q?=EC=9D=8C=20=ED=95=84=ED=84=B0=EB=A1=9C=20=EC=A0=84=EB=8B=AC?= =?UTF-8?q?=ED=95=98=EB=8A=94=20TokenAuthenticationFilter=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1=20-=20Spring=20Security?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=EC=9D=84=20=EC=9C=84=ED=95=9C=20SecurityC?= =?UTF-8?q?onfig=20=EC=9E=91=EC=84=B1=20-=20UserController=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../media/user/config/SecurityConfig.java | 35 +++++++++ .../config/TokenAuthenticationFilter.java | 29 +++++++ .../media/user/config/TokenProvider.java | 76 +++++++++++++++++++ .../media/user/controller/UserController.java | 11 +++ .../wanted/media/user/domain/UserDetail.java | 61 +++++++++++++++ .../media/user/dto/UserLoginRequestDto.java | 15 ++++ .../media/user/dto/UserLoginResponseDto.java | 16 ++++ .../media/user/repository/UserRepository.java | 2 + .../media/user/service/UserDetailService.java | 36 +++++++++ .../media/user/service/UserService.java | 13 ++++ 11 files changed, 295 insertions(+) create mode 100644 src/main/java/wanted/media/user/config/TokenAuthenticationFilter.java create mode 100644 src/main/java/wanted/media/user/config/TokenProvider.java create mode 100644 src/main/java/wanted/media/user/domain/UserDetail.java create mode 100644 src/main/java/wanted/media/user/dto/UserLoginRequestDto.java create mode 100644 src/main/java/wanted/media/user/dto/UserLoginResponseDto.java create mode 100644 src/main/java/wanted/media/user/service/UserDetailService.java diff --git a/build.gradle b/build.gradle index cd13b6d..99b5fd2 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'io.jsonwebtoken:jjwt:0.9.1' + implementation 'javax.xml.bind:jaxb-api:2.3.1' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/main/java/wanted/media/user/config/SecurityConfig.java b/src/main/java/wanted/media/user/config/SecurityConfig.java index 0d84f89..2970150 100644 --- a/src/main/java/wanted/media/user/config/SecurityConfig.java +++ b/src/main/java/wanted/media/user/config/SecurityConfig.java @@ -1,7 +1,42 @@ package wanted.media.user.config; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +@Configuration @EnableWebSecurity +@RequiredArgsConstructor public class SecurityConfig { + + private final TokenProvider tokenProvider; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .httpBasic(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests((auth) -> auth + .requestMatchers("/user/**").permitAll() // 어떤 사용자든 접근 가능 + .anyRequest().authenticated()) + .exceptionHandling(e -> e + .authenticationEntryPoint((request, response, exception) -> { + response.sendError(HttpStatus.UNAUTHORIZED.value(), "인증이 필요합니다."); + }) + .accessDeniedHandler((request, response, exception) -> { + response.sendError(HttpStatus.FORBIDDEN.value(), "접근권한이 없습니다."); + })) + .addFilterBefore(new TokenAuthenticationFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } } diff --git a/src/main/java/wanted/media/user/config/TokenAuthenticationFilter.java b/src/main/java/wanted/media/user/config/TokenAuthenticationFilter.java new file mode 100644 index 0000000..4aa72e7 --- /dev/null +++ b/src/main/java/wanted/media/user/config/TokenAuthenticationFilter.java @@ -0,0 +1,29 @@ +package wanted.media.user.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@RequiredArgsConstructor +public class TokenAuthenticationFilter extends OncePerRequestFilter { + + private final TokenProvider tokenProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String token = tokenProvider.resolveToken(request); + // 토큰 유효성 검증 + if (token != null && tokenProvider.validToken(token)) { + Authentication authentication = tokenProvider.getAuthentication(token); // 인증 정보 + SecurityContextHolder.getContext().setAuthentication(authentication); // 인증 정보를 보안 컨텍스트에 설정 + } + filterChain.doFilter(request, response); // 응답과 요청을 다음 필터로 전달 + } +} diff --git a/src/main/java/wanted/media/user/config/TokenProvider.java b/src/main/java/wanted/media/user/config/TokenProvider.java new file mode 100644 index 0000000..542933d --- /dev/null +++ b/src/main/java/wanted/media/user/config/TokenProvider.java @@ -0,0 +1,76 @@ +package wanted.media.user.config; + +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import wanted.media.user.domain.UserDetail; +import wanted.media.user.service.UserDetailService; + +import java.util.Date; + +@Component +@RequiredArgsConstructor +public class TokenProvider { + + @Value("&{jwt.secret_key}") + private String key; + + private long tokenValidTime = 1000L * 60 * 60; // 1시간 + + private final UserDetailService userDetailService; + + public String makeToken(String account) { + Date now = new Date(); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // 헤더 타입 + .setIssuedAt(now) // 발급시간 + .setExpiration(new Date(now.getTime() + tokenValidTime)) // 만료시간 + .setClaims(Jwts.claims().setSubject(account)) // 회원 계정 (사용자 식별값) + .signWith(SignatureAlgorithm.HS256, key) // HS256 방식으로 key와 함께 암호화 + .compact(); + } + + // 토큰 유효성 검증 + public boolean validToken(String token) { + try { + Jwts.parser().setSigningKey(key) + .parseClaimsJws(token); + return true; + } catch (Exception e) { + return false; + } + } + + // 토큰으로 인증 정보 담은 Authentication 반환 + public Authentication getAuthentication(String token) { + UserDetail userDetail = (UserDetail) userDetailService.loadUserByUsername(getUserAccount(token)); + return new UsernamePasswordAuthenticationToken(userDetail, "", userDetail.getAuthorities()); + /* principal : 인증된 사용자 정보 + credentials : 사용자의 인증 자격 증명 (인증 완료된 상태이므로 빈 문자열 사용) + authorities : 사용자의 권한목록*/ + } + + public String getUserAccount(String token) { + try { // JWT를 파싱해서 JWT 서명 검증 후 클레임을 반환하여 payload에서 subject 클레임 추출 + return Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody().getSubject(); + } catch (ExpiredJwtException e) { + return e.getClaims().getSubject(); + } + } + + // 토큰 Header에서 꺼내오기 + public String resolveToken(HttpServletRequest request) { + String header = request.getHeader("Authorization"); + if (header != null && header.startsWith("Bearer ")) + return header.substring(7); + return null; + } +} diff --git a/src/main/java/wanted/media/user/controller/UserController.java b/src/main/java/wanted/media/user/controller/UserController.java index 867ccff..91027ec 100644 --- a/src/main/java/wanted/media/user/controller/UserController.java +++ b/src/main/java/wanted/media/user/controller/UserController.java @@ -1,8 +1,13 @@ package wanted.media.user.controller; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import wanted.media.user.dto.UserLoginRequestDto; +import wanted.media.user.dto.UserLoginResponseDto; import wanted.media.user.service.UserService; @RestController @@ -11,4 +16,10 @@ public class UserController { private final UserService userService; + + @PostMapping("/login") + public ResponseEntity loginUser(@RequestBody UserLoginRequestDto requestDto) { + UserLoginResponseDto responseDto = userService.loginUser(requestDto); + return ResponseEntity.ok().body(responseDto); + } } diff --git a/src/main/java/wanted/media/user/domain/UserDetail.java b/src/main/java/wanted/media/user/domain/UserDetail.java new file mode 100644 index 0000000..55177f5 --- /dev/null +++ b/src/main/java/wanted/media/user/domain/UserDetail.java @@ -0,0 +1,61 @@ +package wanted.media.user.domain; + +import lombok.Builder; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +public class UserDetail implements UserDetails { + + private String account; + private String password; + private List authorities; + + @Builder + public UserDetail(String account, String password, List authorities) { + this.account = account; + this.password = password; + this.authorities = authorities; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return account; + } + + // 계정 만료 여부 + @Override + public boolean isAccountNonExpired() { + return true; + } + + // 계정 잠금 여부 + @Override + public boolean isAccountNonLocked() { + return true; + } + + // 비밀번호 만료 여부 + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + // 계정 사용 가능 여부 + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/wanted/media/user/dto/UserLoginRequestDto.java b/src/main/java/wanted/media/user/dto/UserLoginRequestDto.java new file mode 100644 index 0000000..cc46ffd --- /dev/null +++ b/src/main/java/wanted/media/user/dto/UserLoginRequestDto.java @@ -0,0 +1,15 @@ +package wanted.media.user.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class UserLoginRequestDto { + + private String account; + private String password; +} diff --git a/src/main/java/wanted/media/user/dto/UserLoginResponseDto.java b/src/main/java/wanted/media/user/dto/UserLoginResponseDto.java new file mode 100644 index 0000000..9ba4e93 --- /dev/null +++ b/src/main/java/wanted/media/user/dto/UserLoginResponseDto.java @@ -0,0 +1,16 @@ +package wanted.media.user.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class UserLoginResponseDto { + private UUID userId; + private String token; +} diff --git a/src/main/java/wanted/media/user/repository/UserRepository.java b/src/main/java/wanted/media/user/repository/UserRepository.java index 51fa606..2be54a2 100644 --- a/src/main/java/wanted/media/user/repository/UserRepository.java +++ b/src/main/java/wanted/media/user/repository/UserRepository.java @@ -3,7 +3,9 @@ import org.springframework.data.jpa.repository.JpaRepository; import wanted.media.user.domain.User; +import java.util.Optional; import java.util.UUID; public interface UserRepository extends JpaRepository { + Optional findByAccount(String account); } diff --git a/src/main/java/wanted/media/user/service/UserDetailService.java b/src/main/java/wanted/media/user/service/UserDetailService.java new file mode 100644 index 0000000..c282738 --- /dev/null +++ b/src/main/java/wanted/media/user/service/UserDetailService.java @@ -0,0 +1,36 @@ +package wanted.media.user.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import wanted.media.user.domain.User; +import wanted.media.user.domain.UserDetail; +import wanted.media.user.repository.UserRepository; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class UserDetailService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException { + User user = userRepository.findByAccount(account) + .orElseThrow(() -> new IllegalArgumentException("계정이 존재하지 않습니다.")); + List roles = new ArrayList<>(); + roles.add(new SimpleGrantedAuthority(user.getGrade().toString())); + + return UserDetail.builder() + .account(user.getAccount()) + .password(user.getPassword()) + .authorities(roles) + .build(); + } +} diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index e642526..218e2d5 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -2,6 +2,10 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import wanted.media.user.config.TokenProvider; +import wanted.media.user.domain.User; +import wanted.media.user.dto.UserLoginRequestDto; +import wanted.media.user.dto.UserLoginResponseDto; import wanted.media.user.repository.UserRepository; @Service @@ -9,4 +13,13 @@ public class UserService { private final UserRepository userRepository; + private final TokenProvider tokenProvider; + + public UserLoginResponseDto loginUser(UserLoginRequestDto requestDto) { + User user = userRepository.findByAccount(requestDto.getAccount()) + .orElseThrow(() -> new IllegalArgumentException("account나 password를 다시 확인해주세요.")); + if (!requestDto.getPassword().equals(user.getPassword())) // password 암호화 저장시 변경하기 + throw new IllegalArgumentException("account나 password를 다시 확인해주세요."); + return new UserLoginResponseDto(user.getUserId(), tokenProvider.makeToken(requestDto.getAccount())); + } } From 99ebf13d020bd813b33913b26ae315321d3a8e03 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 02:53:23 +0900 Subject: [PATCH 02/15] =?UTF-8?q?feat=20:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=EC=8B=9C=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=A0=80=EC=9E=A5=20=EB=B0=8F=20=EB=A6=AC=ED=94=84?= =?UTF-8?q?=EB=A0=88=EC=8B=9C=ED=86=A0=ED=81=B0=EA=B3=BC=20=EC=95=A1?= =?UTF-8?q?=EC=84=B8=EC=8A=A4=ED=86=A0=ED=81=B0=20=EC=9E=AC=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=20-=20=EA=B8=B0=EC=A1=B4=EC=9D=98=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=EC=8B=9C=20=EC=95=A1=EC=84=B8=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=ED=81=B0=EB=A7=8C=20=EB=B0=9C=EA=B8=89=EB=90=98?= =?UTF-8?q?=EB=8D=98=20=EC=BD=94=EB=93=9C=EC=97=90=20=EB=A6=AC=ED=94=84?= =?UTF-8?q?=EB=A0=88=EC=8B=9C=ED=86=A0=ED=81=B0=EB=8F=84=20=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=EB=90=98=EC=96=B4=20=EC=A0=80=EC=9E=A5=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=20UserService=EC=97=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=ED=95=B4?= =?UTF-8?q?=EB=8F=84=20=ED=86=A0=ED=81=B0=EC=9D=B4=20=EB=B0=94=EB=80=8C?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20TokenProv?= =?UTF-8?q?ider=EC=97=90=EC=84=9C=20=EC=88=98=EC=A0=95=20--=20claim?= =?UTF-8?q?=EC=97=90=20=EB=B0=9C=EA=B8=89=20=EC=8B=9C=EA=B0=84=EA=B3=BC=20?= =?UTF-8?q?=EB=A7=8C=EB=A3=8C=20=EC=8B=9C=EA=B0=84=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?-=20TokenService=EC=97=90=EC=84=9C=20=EB=A6=AC=ED=94=84?= =?UTF-8?q?=EB=A0=88=EC=8B=9C=20=ED=86=A0=ED=81=B0=EC=9D=84=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=ED=95=B4=20=EC=95=A1=EC=84=B8=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=ED=81=B0=EA=B3=BC=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=9E=AC=EB=B0=9C=EA=B8=89=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/TokenAuthenticationFilter.java | 2 +- .../media/user/config/TokenProvider.java | 10 ++-- .../user/controller/TokenController.java | 26 +++++++++ .../java/wanted/media/user/domain/Token.java | 10 ++++ .../media/user/dto/TokenRequestDto.java | 14 +++++ .../media/user/dto/TokenResponseDto.java | 14 +++++ .../user/repository/TokenRepository.java | 14 +++++ .../media/user/service/TokenService.java | 54 +++++++++++++++++++ .../media/user/service/UserService.java | 16 +++++- 9 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 src/main/java/wanted/media/user/controller/TokenController.java create mode 100644 src/main/java/wanted/media/user/dto/TokenRequestDto.java create mode 100644 src/main/java/wanted/media/user/dto/TokenResponseDto.java create mode 100644 src/main/java/wanted/media/user/repository/TokenRepository.java create mode 100644 src/main/java/wanted/media/user/service/TokenService.java diff --git a/src/main/java/wanted/media/user/config/TokenAuthenticationFilter.java b/src/main/java/wanted/media/user/config/TokenAuthenticationFilter.java index 4aa72e7..8f18dc1 100644 --- a/src/main/java/wanted/media/user/config/TokenAuthenticationFilter.java +++ b/src/main/java/wanted/media/user/config/TokenAuthenticationFilter.java @@ -18,7 +18,7 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - String token = tokenProvider.resolveToken(request); + String token = tokenProvider.resolveToken(request); // 헤더에서 가져온 액세스토큰 // 토큰 유효성 검증 if (token != null && tokenProvider.validToken(token)) { Authentication authentication = tokenProvider.getAuthentication(token); // 인증 정보 diff --git a/src/main/java/wanted/media/user/config/TokenProvider.java b/src/main/java/wanted/media/user/config/TokenProvider.java index 542933d..839c6bd 100644 --- a/src/main/java/wanted/media/user/config/TokenProvider.java +++ b/src/main/java/wanted/media/user/config/TokenProvider.java @@ -19,21 +19,21 @@ @RequiredArgsConstructor public class TokenProvider { - @Value("&{jwt.secret_key}") + @Value("${jwt.secret_key}") private String key; private long tokenValidTime = 1000L * 60 * 60; // 1시간 + private long RefreshTokenValidTime = 1000L * 60 * 60 * 24 * 7; // 7일 private final UserDetailService userDetailService; - public String makeToken(String account) { + public String makeToken(String account, String type) { Date now = new Date(); + long time = type.equals("access") ? tokenValidTime : RefreshTokenValidTime; return Jwts.builder() .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // 헤더 타입 - .setIssuedAt(now) // 발급시간 - .setExpiration(new Date(now.getTime() + tokenValidTime)) // 만료시간 - .setClaims(Jwts.claims().setSubject(account)) // 회원 계정 (사용자 식별값) + .setClaims(Jwts.claims().setSubject(account).setAudience(type).setIssuedAt(now).setExpiration(new Date(now.getTime() + time))) .signWith(SignatureAlgorithm.HS256, key) // HS256 방식으로 key와 함께 암호화 .compact(); } diff --git a/src/main/java/wanted/media/user/controller/TokenController.java b/src/main/java/wanted/media/user/controller/TokenController.java new file mode 100644 index 0000000..85da174 --- /dev/null +++ b/src/main/java/wanted/media/user/controller/TokenController.java @@ -0,0 +1,26 @@ +package wanted.media.user.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import wanted.media.user.dto.TokenRequestDto; +import wanted.media.user.dto.TokenResponseDto; +import wanted.media.user.service.TokenService; + +@RestController +@RequestMapping("/api/token") +@RequiredArgsConstructor +public class TokenController { + + private final TokenService tokenService; + + @PostMapping("/reissue") + public ResponseEntity reIssueToken(@RequestBody TokenRequestDto requestDto) { + TokenResponseDto responseDto = tokenService.reIssueToken(requestDto); + return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); + } +} diff --git a/src/main/java/wanted/media/user/domain/Token.java b/src/main/java/wanted/media/user/domain/Token.java index 81fc2f5..d4e9793 100644 --- a/src/main/java/wanted/media/user/domain/Token.java +++ b/src/main/java/wanted/media/user/domain/Token.java @@ -23,4 +23,14 @@ public class Token { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private User user; + + public Token(String refreshToken, User user) { + this.refreshToken = refreshToken; + this.user = user; + } + + public Token updateToken(String refreshToken) { + this.refreshToken = refreshToken; + return this; + } } diff --git a/src/main/java/wanted/media/user/dto/TokenRequestDto.java b/src/main/java/wanted/media/user/dto/TokenRequestDto.java new file mode 100644 index 0000000..f3c22bb --- /dev/null +++ b/src/main/java/wanted/media/user/dto/TokenRequestDto.java @@ -0,0 +1,14 @@ +package wanted.media.user.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class TokenRequestDto { + private String accessToken; + private String refreshToken; +} diff --git a/src/main/java/wanted/media/user/dto/TokenResponseDto.java b/src/main/java/wanted/media/user/dto/TokenResponseDto.java new file mode 100644 index 0000000..789e867 --- /dev/null +++ b/src/main/java/wanted/media/user/dto/TokenResponseDto.java @@ -0,0 +1,14 @@ +package wanted.media.user.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class TokenResponseDto { + private String accessToken; + private String refreshToken; +} diff --git a/src/main/java/wanted/media/user/repository/TokenRepository.java b/src/main/java/wanted/media/user/repository/TokenRepository.java new file mode 100644 index 0000000..efd7f78 --- /dev/null +++ b/src/main/java/wanted/media/user/repository/TokenRepository.java @@ -0,0 +1,14 @@ +package wanted.media.user.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import wanted.media.user.domain.Token; + +import java.util.Optional; +import java.util.UUID; + +public interface TokenRepository extends JpaRepository { + @Query("SELECT t FROM Token t WHERE t.user.userId = :userId") + Optional findByUserId(@Param("userId") UUID userID); +} diff --git a/src/main/java/wanted/media/user/service/TokenService.java b/src/main/java/wanted/media/user/service/TokenService.java new file mode 100644 index 0000000..0c00375 --- /dev/null +++ b/src/main/java/wanted/media/user/service/TokenService.java @@ -0,0 +1,54 @@ +package wanted.media.user.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import wanted.media.user.config.TokenProvider; +import wanted.media.user.domain.Token; +import wanted.media.user.domain.User; +import wanted.media.user.domain.UserDetail; +import wanted.media.user.dto.TokenRequestDto; +import wanted.media.user.dto.TokenResponseDto; +import wanted.media.user.repository.TokenRepository; +import wanted.media.user.repository.UserRepository; + +@Service +@RequiredArgsConstructor +public class TokenService { + + private final TokenProvider tokenProvider; + private final TokenRepository tokenRepository; + private final UserRepository userRepository; + + // 액세스 토큰 재발행 + public TokenResponseDto reIssueToken(TokenRequestDto requestDto) { + if (!tokenProvider.validToken(requestDto.getRefreshToken())) { // 리프레시 토큰 만료 기간 지났을 경우 + throw new IllegalArgumentException("다시 로그인해주세요."); + } + + User user = findUserByToken(requestDto); + + Token storedToken = tokenRepository.findByUserId(user.getUserId()) + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 토큰입니다.")); + + if (!storedToken.getRefreshToken().equals(requestDto.getRefreshToken())) { + throw new IllegalArgumentException("유효하지 않은 토큰입니다."); + } + + String accessToken = tokenProvider.makeToken(user.getAccount(), "access"); + String refreshToken = tokenProvider.makeToken(user.getAccount(), "refresh"); + + storedToken.updateToken(refreshToken); + tokenRepository.save(storedToken); + + return new TokenResponseDto(accessToken, refreshToken); + } + + public User findUserByToken(TokenRequestDto requestDto) { + Authentication authentication = tokenProvider.getAuthentication(requestDto.getAccessToken()); + UserDetail userDetail = (UserDetail) authentication.getPrincipal(); + String account = userDetail.getUsername(); + return userRepository.findByAccount(account).orElseThrow(() -> new IllegalArgumentException("회원을 찾을 수 없습니다.")); + } + +} diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 218e2d5..5b1e8bf 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -3,16 +3,21 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import wanted.media.user.config.TokenProvider; +import wanted.media.user.domain.Token; import wanted.media.user.domain.User; import wanted.media.user.dto.UserLoginRequestDto; import wanted.media.user.dto.UserLoginResponseDto; +import wanted.media.user.repository.TokenRepository; import wanted.media.user.repository.UserRepository; +import java.util.Optional; + @Service @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; + private final TokenRepository tokenRepository; private final TokenProvider tokenProvider; public UserLoginResponseDto loginUser(UserLoginRequestDto requestDto) { @@ -20,6 +25,15 @@ public UserLoginResponseDto loginUser(UserLoginRequestDto requestDto) { .orElseThrow(() -> new IllegalArgumentException("account나 password를 다시 확인해주세요.")); if (!requestDto.getPassword().equals(user.getPassword())) // password 암호화 저장시 변경하기 throw new IllegalArgumentException("account나 password를 다시 확인해주세요."); - return new UserLoginResponseDto(user.getUserId(), tokenProvider.makeToken(requestDto.getAccount())); + + Optional refreshToken = tokenRepository.findByUserId(user.getUserId()); // 리프레시 토큰 있는지 확인 + String newRefreshToken = tokenProvider.makeToken(requestDto.getAccount(), "refresh"); // 새 리프레시 토큰 + if (refreshToken.isPresent()) { // 리프레시 토큰 있을 경우 + tokenRepository.save(refreshToken.get().updateToken(newRefreshToken)); // 새 토큰으로 업데이트 + } else { // 리프레시 토큰 없을 경우 + tokenRepository.save(new Token(newRefreshToken, user)); // 새 토큰 저장 + } + + return new UserLoginResponseDto(user.getUserId(), tokenProvider.makeToken(requestDto.getAccount(), "access")); } } From 376565708a67cfcd60ab305ef1742f23f863ddab Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 22:28:59 +0900 Subject: [PATCH 03/15] =?UTF-8?q?refactor=20:=20=EA=B0=80=EB=8F=85?= =?UTF-8?q?=EC=84=B1=20=EC=9C=84=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EC=97=90=EB=9F=AC=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=A7=81=20=EC=B6=94=EA=B0=80=20-=20TokenProvider=EC=9D=98=20m?= =?UTF-8?q?akeToken=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B0=80=EB=8F=85?= =?UTF-8?q?=EC=84=B1=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?-=20=ED=9A=8C=EC=9B=90=EC=9D=B4=20=EC=A1=B4=EC=9E=AC=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EA=B2=BD=EC=9A=B0=EC=9D=98=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=ED=95=B8=EB=93=A4=EB=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../media/exception/NotFoundException.java | 7 +++++++ .../handler/GlobalExceptionHandler.java | 18 ++++++++++++------ .../media/user/config/TokenProvider.java | 7 ++++++- .../media/user/service/TokenService.java | 4 +++- .../media/user/service/UserDetailService.java | 3 ++- 5 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 src/main/java/wanted/media/exception/NotFoundException.java diff --git a/src/main/java/wanted/media/exception/NotFoundException.java b/src/main/java/wanted/media/exception/NotFoundException.java new file mode 100644 index 0000000..b86cf33 --- /dev/null +++ b/src/main/java/wanted/media/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package wanted.media.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java b/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java index c96a472..c86c3c0 100644 --- a/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/wanted/media/exception/handler/GlobalExceptionHandler.java @@ -1,18 +1,24 @@ package wanted.media.exception.handler; import org.apache.coyote.BadRequestException; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; - import wanted.media.exception.ErrorResponse; +import wanted.media.exception.NotFoundException; @RestControllerAdvice public class GlobalExceptionHandler { - @ExceptionHandler(BadRequestException.class) - public ResponseEntity handleBadRequestException(BadRequestException e) { - return ResponseEntity.badRequest() - .body(new ErrorResponse(400, e.getMessage())); - } + @ExceptionHandler(BadRequestException.class) + public ResponseEntity handleBadRequestException(BadRequestException e) { + return ResponseEntity.badRequest() + .body(new ErrorResponse(400, e.getMessage())); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFoundException(NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } } diff --git a/src/main/java/wanted/media/user/config/TokenProvider.java b/src/main/java/wanted/media/user/config/TokenProvider.java index 839c6bd..2700f52 100644 --- a/src/main/java/wanted/media/user/config/TokenProvider.java +++ b/src/main/java/wanted/media/user/config/TokenProvider.java @@ -33,7 +33,12 @@ public String makeToken(String account, String type) { return Jwts.builder() .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // 헤더 타입 - .setClaims(Jwts.claims().setSubject(account).setAudience(type).setIssuedAt(now).setExpiration(new Date(now.getTime() + time))) + .setClaims(Jwts.claims(). + setSubject(account). + setAudience(type). + setIssuedAt(now). + setExpiration(new Date(now.getTime() + time)) + ) .signWith(SignatureAlgorithm.HS256, key) // HS256 방식으로 key와 함께 암호화 .compact(); } diff --git a/src/main/java/wanted/media/user/service/TokenService.java b/src/main/java/wanted/media/user/service/TokenService.java index 0c00375..1801ab4 100644 --- a/src/main/java/wanted/media/user/service/TokenService.java +++ b/src/main/java/wanted/media/user/service/TokenService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; +import wanted.media.exception.NotFoundException; import wanted.media.user.config.TokenProvider; import wanted.media.user.domain.Token; import wanted.media.user.domain.User; @@ -48,7 +49,8 @@ public User findUserByToken(TokenRequestDto requestDto) { Authentication authentication = tokenProvider.getAuthentication(requestDto.getAccessToken()); UserDetail userDetail = (UserDetail) authentication.getPrincipal(); String account = userDetail.getUsername(); - return userRepository.findByAccount(account).orElseThrow(() -> new IllegalArgumentException("회원을 찾을 수 없습니다.")); + return userRepository.findByAccount(account) + .orElseThrow(() -> new NotFoundException("회원을 찾을 수 없습니다.")); } } diff --git a/src/main/java/wanted/media/user/service/UserDetailService.java b/src/main/java/wanted/media/user/service/UserDetailService.java index c282738..c5d1a6d 100644 --- a/src/main/java/wanted/media/user/service/UserDetailService.java +++ b/src/main/java/wanted/media/user/service/UserDetailService.java @@ -7,6 +7,7 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; +import wanted.media.exception.NotFoundException; import wanted.media.user.domain.User; import wanted.media.user.domain.UserDetail; import wanted.media.user.repository.UserRepository; @@ -23,7 +24,7 @@ public class UserDetailService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException { User user = userRepository.findByAccount(account) - .orElseThrow(() -> new IllegalArgumentException("계정이 존재하지 않습니다.")); + .orElseThrow(() -> new NotFoundException("계정이 존재하지 않습니다.")); List roles = new ArrayList<>(); roles.add(new SimpleGrantedAuthority(user.getGrade().toString())); From d888f838f77904851006167eefeec9481163a04f Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 22:35:30 +0900 Subject: [PATCH 04/15] =?UTF-8?q?refactor=20:=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=A7=8C=EB=A3=8C=EC=8B=9C=EA=B0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/config/TokenProvider.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/wanted/media/user/config/TokenProvider.java b/src/main/java/wanted/media/user/config/TokenProvider.java index 2700f52..c034492 100644 --- a/src/main/java/wanted/media/user/config/TokenProvider.java +++ b/src/main/java/wanted/media/user/config/TokenProvider.java @@ -21,9 +21,10 @@ public class TokenProvider { @Value("${jwt.secret_key}") private String key; - - private long tokenValidTime = 1000L * 60 * 60; // 1시간 - private long RefreshTokenValidTime = 1000L * 60 * 60 * 24 * 7; // 7일 + @Value("${jwt.access_token_expiration}") + private long tokenValidTime; + @Value("${jwt.refresh_token_expiration}") + private long RefreshTokenValidTime; private final UserDetailService userDetailService; From 848ce307fde40ffc508e2785ed6d66e776827c05 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 22:48:56 +0900 Subject: [PATCH 05/15] =?UTF-8?q?refactor=20:=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EA=B3=B5=EB=B0=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/config/SecurityConfig.java | 1 - src/main/java/wanted/media/user/domain/UserDetail.java | 1 - src/main/java/wanted/media/user/dto/UserLoginRequestDto.java | 1 - src/main/java/wanted/media/user/service/TokenService.java | 1 - src/main/java/wanted/media/user/service/UserDetailService.java | 1 - src/main/java/wanted/media/user/service/UserService.java | 1 - 6 files changed, 6 deletions(-) diff --git a/src/main/java/wanted/media/user/config/SecurityConfig.java b/src/main/java/wanted/media/user/config/SecurityConfig.java index 2970150..0957987 100644 --- a/src/main/java/wanted/media/user/config/SecurityConfig.java +++ b/src/main/java/wanted/media/user/config/SecurityConfig.java @@ -15,7 +15,6 @@ @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { - private final TokenProvider tokenProvider; @Bean diff --git a/src/main/java/wanted/media/user/domain/UserDetail.java b/src/main/java/wanted/media/user/domain/UserDetail.java index 55177f5..899d9e9 100644 --- a/src/main/java/wanted/media/user/domain/UserDetail.java +++ b/src/main/java/wanted/media/user/domain/UserDetail.java @@ -8,7 +8,6 @@ import java.util.List; public class UserDetail implements UserDetails { - private String account; private String password; private List authorities; diff --git a/src/main/java/wanted/media/user/dto/UserLoginRequestDto.java b/src/main/java/wanted/media/user/dto/UserLoginRequestDto.java index cc46ffd..820b751 100644 --- a/src/main/java/wanted/media/user/dto/UserLoginRequestDto.java +++ b/src/main/java/wanted/media/user/dto/UserLoginRequestDto.java @@ -9,7 +9,6 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor public class UserLoginRequestDto { - private String account; private String password; } diff --git a/src/main/java/wanted/media/user/service/TokenService.java b/src/main/java/wanted/media/user/service/TokenService.java index 1801ab4..568a048 100644 --- a/src/main/java/wanted/media/user/service/TokenService.java +++ b/src/main/java/wanted/media/user/service/TokenService.java @@ -16,7 +16,6 @@ @Service @RequiredArgsConstructor public class TokenService { - private final TokenProvider tokenProvider; private final TokenRepository tokenRepository; private final UserRepository userRepository; diff --git a/src/main/java/wanted/media/user/service/UserDetailService.java b/src/main/java/wanted/media/user/service/UserDetailService.java index c5d1a6d..4795e4d 100644 --- a/src/main/java/wanted/media/user/service/UserDetailService.java +++ b/src/main/java/wanted/media/user/service/UserDetailService.java @@ -18,7 +18,6 @@ @Service @RequiredArgsConstructor public class UserDetailService implements UserDetailsService { - private final UserRepository userRepository; @Override diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 5b1e8bf..779a35a 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -15,7 +15,6 @@ @Service @RequiredArgsConstructor public class UserService { - private final UserRepository userRepository; private final TokenRepository tokenRepository; private final TokenProvider tokenProvider; From 979198ca75a4fec16238c4ce390de211e3f7caa3 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 22:55:05 +0900 Subject: [PATCH 06/15] =?UTF-8?q?refactor=20:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=AC=EB=B0=9C=EA=B8=89=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EB=AA=85=EC=B9=AD=20=EC=88=98=EC=A0=95=20-=20TokenController?= =?UTF-8?q?=EA=B3=BC=20TokenService=EC=9D=98=20=EC=9E=AC=EB=B0=9C=EA=B8=89?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=AA=85=EC=B9=AD=20getToken?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95=20-=20=EC=A7=81?= =?UTF-8?q?=EA=B4=80=EC=84=B1=EC=9D=84=20=EC=9C=84=ED=95=B4=20URL=EB=8F=84?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wanted/media/user/controller/TokenController.java | 8 ++++---- src/main/java/wanted/media/user/service/TokenService.java | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/wanted/media/user/controller/TokenController.java b/src/main/java/wanted/media/user/controller/TokenController.java index 85da174..f4f5d37 100644 --- a/src/main/java/wanted/media/user/controller/TokenController.java +++ b/src/main/java/wanted/media/user/controller/TokenController.java @@ -12,15 +12,15 @@ import wanted.media.user.service.TokenService; @RestController -@RequestMapping("/api/token") +@RequestMapping("/api") @RequiredArgsConstructor public class TokenController { private final TokenService tokenService; - @PostMapping("/reissue") - public ResponseEntity reIssueToken(@RequestBody TokenRequestDto requestDto) { - TokenResponseDto responseDto = tokenService.reIssueToken(requestDto); + @PostMapping("/token") + public ResponseEntity getToken(@RequestBody TokenRequestDto requestDto) { + TokenResponseDto responseDto = tokenService.getToken(requestDto); return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); } } diff --git a/src/main/java/wanted/media/user/service/TokenService.java b/src/main/java/wanted/media/user/service/TokenService.java index 568a048..e07cc30 100644 --- a/src/main/java/wanted/media/user/service/TokenService.java +++ b/src/main/java/wanted/media/user/service/TokenService.java @@ -20,8 +20,8 @@ public class TokenService { private final TokenRepository tokenRepository; private final UserRepository userRepository; - // 액세스 토큰 재발행 - public TokenResponseDto reIssueToken(TokenRequestDto requestDto) { + // 액세스 토큰, 리프레시 토큰 재발행 + public TokenResponseDto getToken(TokenRequestDto requestDto) { if (!tokenProvider.validToken(requestDto.getRefreshToken())) { // 리프레시 토큰 만료 기간 지났을 경우 throw new IllegalArgumentException("다시 로그인해주세요."); } From e2ff106856fcc0de9cadc572612500eefb4ba600 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 22:59:29 +0900 Subject: [PATCH 07/15] =?UTF-8?q?refactor=20:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=AA=85=EC=B9=AD=20?= =?UTF-8?q?=E3=85=85=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/controller/UserController.java | 2 +- src/main/java/wanted/media/user/service/UserService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/wanted/media/user/controller/UserController.java b/src/main/java/wanted/media/user/controller/UserController.java index 91027ec..9191334 100644 --- a/src/main/java/wanted/media/user/controller/UserController.java +++ b/src/main/java/wanted/media/user/controller/UserController.java @@ -19,7 +19,7 @@ public class UserController { @PostMapping("/login") public ResponseEntity loginUser(@RequestBody UserLoginRequestDto requestDto) { - UserLoginResponseDto responseDto = userService.loginUser(requestDto); + UserLoginResponseDto responseDto = userService.login(requestDto); return ResponseEntity.ok().body(responseDto); } } diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 779a35a..4e0fecf 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -19,7 +19,7 @@ public class UserService { private final TokenRepository tokenRepository; private final TokenProvider tokenProvider; - public UserLoginResponseDto loginUser(UserLoginRequestDto requestDto) { + public UserLoginResponseDto login(UserLoginRequestDto requestDto) { User user = userRepository.findByAccount(requestDto.getAccount()) .orElseThrow(() -> new IllegalArgumentException("account나 password를 다시 확인해주세요.")); if (!requestDto.getPassword().equals(user.getPassword())) // password 암호화 저장시 변경하기 From 9455bed04006953e0bd94c3ac4d4085f30266656 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 23:19:58 +0900 Subject: [PATCH 08/15] =?UTF-8?q?refactor=20:=20TokenRequestDto=20record?= =?UTF-8?q?=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/wanted/media/user/dto/TokenRequestDto.java | 12 +----------- .../java/wanted/media/user/service/TokenService.java | 6 +++--- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/main/java/wanted/media/user/dto/TokenRequestDto.java b/src/main/java/wanted/media/user/dto/TokenRequestDto.java index f3c22bb..d734138 100644 --- a/src/main/java/wanted/media/user/dto/TokenRequestDto.java +++ b/src/main/java/wanted/media/user/dto/TokenRequestDto.java @@ -1,14 +1,4 @@ package wanted.media.user.dto; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -public class TokenRequestDto { - private String accessToken; - private String refreshToken; +public record TokenRequestDto(String accessToken, String refreshToken) { } diff --git a/src/main/java/wanted/media/user/service/TokenService.java b/src/main/java/wanted/media/user/service/TokenService.java index e07cc30..6b44669 100644 --- a/src/main/java/wanted/media/user/service/TokenService.java +++ b/src/main/java/wanted/media/user/service/TokenService.java @@ -22,7 +22,7 @@ public class TokenService { // 액세스 토큰, 리프레시 토큰 재발행 public TokenResponseDto getToken(TokenRequestDto requestDto) { - if (!tokenProvider.validToken(requestDto.getRefreshToken())) { // 리프레시 토큰 만료 기간 지났을 경우 + if (!tokenProvider.validToken(requestDto.refreshToken())) { // 리프레시 토큰 만료 기간 지났을 경우 throw new IllegalArgumentException("다시 로그인해주세요."); } @@ -31,7 +31,7 @@ public TokenResponseDto getToken(TokenRequestDto requestDto) { Token storedToken = tokenRepository.findByUserId(user.getUserId()) .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 토큰입니다.")); - if (!storedToken.getRefreshToken().equals(requestDto.getRefreshToken())) { + if (!storedToken.getRefreshToken().equals(requestDto.refreshToken())) { throw new IllegalArgumentException("유효하지 않은 토큰입니다."); } @@ -45,7 +45,7 @@ public TokenResponseDto getToken(TokenRequestDto requestDto) { } public User findUserByToken(TokenRequestDto requestDto) { - Authentication authentication = tokenProvider.getAuthentication(requestDto.getAccessToken()); + Authentication authentication = tokenProvider.getAuthentication(requestDto.accessToken()); UserDetail userDetail = (UserDetail) authentication.getPrincipal(); String account = userDetail.getUsername(); return userRepository.findByAccount(account) From e21ab02ae4e45a514c69d82db82561d484182891 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 23:35:28 +0900 Subject: [PATCH 09/15] =?UTF-8?q?refactor=20:=20JPA=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=20=EA=B0=90=EC=A7=80=20=EA=B8=B0=EB=8A=A5=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20save()=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/service/UserService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 4e0fecf..686e7a8 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -28,7 +28,7 @@ public UserLoginResponseDto login(UserLoginRequestDto requestDto) { Optional refreshToken = tokenRepository.findByUserId(user.getUserId()); // 리프레시 토큰 있는지 확인 String newRefreshToken = tokenProvider.makeToken(requestDto.getAccount(), "refresh"); // 새 리프레시 토큰 if (refreshToken.isPresent()) { // 리프레시 토큰 있을 경우 - tokenRepository.save(refreshToken.get().updateToken(newRefreshToken)); // 새 토큰으로 업데이트 + refreshToken.get().updateToken(newRefreshToken); // 새 토큰으로 업데이트 } else { // 리프레시 토큰 없을 경우 tokenRepository.save(new Token(newRefreshToken, user)); // 새 토큰 저장 } From 382d0322ecae008533b9e3d0194187f56a6bdce8 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 23:50:04 +0900 Subject: [PATCH 10/15] =?UTF-8?q?refactor=20:=20Service=EC=97=90=20?= =?UTF-8?q?=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/service/TokenService.java | 3 +++ src/main/java/wanted/media/user/service/UserService.java | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/main/java/wanted/media/user/service/TokenService.java b/src/main/java/wanted/media/user/service/TokenService.java index 6b44669..defd287 100644 --- a/src/main/java/wanted/media/user/service/TokenService.java +++ b/src/main/java/wanted/media/user/service/TokenService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import wanted.media.exception.NotFoundException; import wanted.media.user.config.TokenProvider; import wanted.media.user.domain.Token; @@ -21,6 +22,7 @@ public class TokenService { private final UserRepository userRepository; // 액세스 토큰, 리프레시 토큰 재발행 + @Transactional public TokenResponseDto getToken(TokenRequestDto requestDto) { if (!tokenProvider.validToken(requestDto.refreshToken())) { // 리프레시 토큰 만료 기간 지났을 경우 throw new IllegalArgumentException("다시 로그인해주세요."); @@ -44,6 +46,7 @@ public TokenResponseDto getToken(TokenRequestDto requestDto) { return new TokenResponseDto(accessToken, refreshToken); } + @Transactional(readOnly = true) public User findUserByToken(TokenRequestDto requestDto) { Authentication authentication = tokenProvider.getAuthentication(requestDto.accessToken()); UserDetail userDetail = (UserDetail) authentication.getPrincipal(); diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 686e7a8..0800bd8 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import wanted.media.user.config.TokenProvider; import wanted.media.user.domain.Token; import wanted.media.user.domain.User; @@ -19,6 +20,7 @@ public class UserService { private final TokenRepository tokenRepository; private final TokenProvider tokenProvider; + @Transactional public UserLoginResponseDto login(UserLoginRequestDto requestDto) { User user = userRepository.findByAccount(requestDto.getAccount()) .orElseThrow(() -> new IllegalArgumentException("account나 password를 다시 확인해주세요.")); From 720d726ca48b4df1b8ccd019a516b6eb8dc56f80 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Sun, 25 Aug 2024 23:59:07 +0900 Subject: [PATCH 11/15] =?UTF-8?q?refactor=20:TokenProvider=EC=97=90=20?= =?UTF-8?q?=EC=83=81=EC=88=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/config/TokenProvider.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/wanted/media/user/config/TokenProvider.java b/src/main/java/wanted/media/user/config/TokenProvider.java index c034492..070efef 100644 --- a/src/main/java/wanted/media/user/config/TokenProvider.java +++ b/src/main/java/wanted/media/user/config/TokenProvider.java @@ -25,6 +25,7 @@ public class TokenProvider { private long tokenValidTime; @Value("${jwt.refresh_token_expiration}") private long RefreshTokenValidTime; + private final String BEARER_PREFIX = "Bearer "; private final UserDetailService userDetailService; @@ -75,8 +76,8 @@ public String getUserAccount(String token) { // 토큰 Header에서 꺼내오기 public String resolveToken(HttpServletRequest request) { String header = request.getHeader("Authorization"); - if (header != null && header.startsWith("Bearer ")) - return header.substring(7); + if (header != null && header.startsWith(BEARER_PREFIX)) + return header.substring(BEARER_PREFIX.length()); return null; } } From 679147710d30e546f11473ec02d8830b2ce943a2 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Mon, 26 Aug 2024 00:25:37 +0900 Subject: [PATCH 12/15] =?UTF-8?q?refactor=20:=20TokenRepository=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=EB=AC=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/wanted/media/user/repository/TokenRepository.java | 4 +--- src/main/java/wanted/media/user/service/TokenService.java | 2 +- src/main/java/wanted/media/user/service/UserService.java | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/wanted/media/user/repository/TokenRepository.java b/src/main/java/wanted/media/user/repository/TokenRepository.java index efd7f78..09fe379 100644 --- a/src/main/java/wanted/media/user/repository/TokenRepository.java +++ b/src/main/java/wanted/media/user/repository/TokenRepository.java @@ -1,7 +1,6 @@ package wanted.media.user.repository; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import wanted.media.user.domain.Token; @@ -9,6 +8,5 @@ import java.util.UUID; public interface TokenRepository extends JpaRepository { - @Query("SELECT t FROM Token t WHERE t.user.userId = :userId") - Optional findByUserId(@Param("userId") UUID userID); + Optional findByUser_UserId(@Param("userId") UUID userId); } diff --git a/src/main/java/wanted/media/user/service/TokenService.java b/src/main/java/wanted/media/user/service/TokenService.java index defd287..0fb2b7f 100644 --- a/src/main/java/wanted/media/user/service/TokenService.java +++ b/src/main/java/wanted/media/user/service/TokenService.java @@ -30,7 +30,7 @@ public TokenResponseDto getToken(TokenRequestDto requestDto) { User user = findUserByToken(requestDto); - Token storedToken = tokenRepository.findByUserId(user.getUserId()) + Token storedToken = tokenRepository.findByUser_UserId(user.getUserId()) .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 토큰입니다.")); if (!storedToken.getRefreshToken().equals(requestDto.refreshToken())) { diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index 0800bd8..c391357 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -27,7 +27,7 @@ public UserLoginResponseDto login(UserLoginRequestDto requestDto) { if (!requestDto.getPassword().equals(user.getPassword())) // password 암호화 저장시 변경하기 throw new IllegalArgumentException("account나 password를 다시 확인해주세요."); - Optional refreshToken = tokenRepository.findByUserId(user.getUserId()); // 리프레시 토큰 있는지 확인 + Optional refreshToken = tokenRepository.findByUser_UserId(user.getUserId()); // 리프레시 토큰 있는지 확인 String newRefreshToken = tokenProvider.makeToken(requestDto.getAccount(), "refresh"); // 새 리프레시 토큰 if (refreshToken.isPresent()) { // 리프레시 토큰 있을 경우 refreshToken.get().updateToken(newRefreshToken); // 새 토큰으로 업데이트 From e330442ee7f7ccee4484dcc1f1a6799baee4d58c Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Mon, 26 Aug 2024 11:53:52 +0900 Subject: [PATCH 13/15] =?UTF-8?q?refactor=20:=20yml=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/resources/application-dev.yml diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..afc3723 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,8 @@ +spring: + jpa: + show-sql: true + properties: + hibernate: + format_sql: true + hibernate: + ddl-auto: update \ No newline at end of file From ca15823376f9def4b553f07c0109b5559fdc7984 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Mon, 26 Aug 2024 14:11:33 +0900 Subject: [PATCH 14/15] =?UTF-8?q?refactor=20:=20dev=20yml=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index afc3723..e5f26d3 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -1,4 +1,8 @@ spring: + datasource: + url: ${db.dev.datasource.url} + username: ${db.dev.datasource.username} + password: ${db.dev.datasource.password} jpa: show-sql: true properties: From 7f57a62d6caa932c27bef7118e8b42920c309c60 Mon Sep 17 00:00:00 2001 From: LeeJiWon Date: Mon, 26 Aug 2024 16:54:15 +0900 Subject: [PATCH 15/15] =?UTF-8?q?refactor=20:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=EC=8B=9C=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EC=95=94=ED=98=B8=ED=99=94=20=EC=A0=81=EC=9A=A9=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/wanted/media/user/config/SecurityConfig.java | 6 ++++++ src/main/java/wanted/media/user/service/UserService.java | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/wanted/media/user/config/SecurityConfig.java b/src/main/java/wanted/media/user/config/SecurityConfig.java index 0957987..ee7440c 100644 --- a/src/main/java/wanted/media/user/config/SecurityConfig.java +++ b/src/main/java/wanted/media/user/config/SecurityConfig.java @@ -8,6 +8,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -17,6 +18,11 @@ public class SecurityConfig { private final TokenProvider tokenProvider; + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http diff --git a/src/main/java/wanted/media/user/service/UserService.java b/src/main/java/wanted/media/user/service/UserService.java index c391357..e4747c9 100644 --- a/src/main/java/wanted/media/user/service/UserService.java +++ b/src/main/java/wanted/media/user/service/UserService.java @@ -1,6 +1,7 @@ package wanted.media.user.service; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import wanted.media.user.config.TokenProvider; @@ -17,6 +18,7 @@ @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; + private final BCryptPasswordEncoder passwordEncoder; private final TokenRepository tokenRepository; private final TokenProvider tokenProvider; @@ -24,7 +26,7 @@ public class UserService { public UserLoginResponseDto login(UserLoginRequestDto requestDto) { User user = userRepository.findByAccount(requestDto.getAccount()) .orElseThrow(() -> new IllegalArgumentException("account나 password를 다시 확인해주세요.")); - if (!requestDto.getPassword().equals(user.getPassword())) // password 암호화 저장시 변경하기 + if (!passwordEncoder.matches(requestDto.getPassword(), user.getPassword())) throw new IllegalArgumentException("account나 password를 다시 확인해주세요."); Optional refreshToken = tokenRepository.findByUser_UserId(user.getUserId()); // 리프레시 토큰 있는지 확인