Skip to content

Commit

Permalink
Merge branch 'develop' into be/feat/#244
Browse files Browse the repository at this point in the history
  • Loading branch information
Gwanghyeon-k committed Feb 4, 2025
2 parents 8d89c35 + 67bd91b commit aef931f
Show file tree
Hide file tree
Showing 14 changed files with 212 additions and 99 deletions.
9 changes: 6 additions & 3 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,15 @@ dependencies {

// Apache PDFBox for extracting text from PDF files
implementation 'org.apache.pdfbox:pdfbox:2.0.24'
}

tasks.named('test') {
useJUnitPlatform()
// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

//tasks.named('test') {
// useJUnitPlatform()
//}

// QueryDsl
def querydslSrcDir = 'src/main/generated'

Expand Down
5 changes: 2 additions & 3 deletions backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ services:
- backend-secret.env

redis:
image: redis:alpine
image: redis:6.0
container_name: redis
hostname: redis
ports:
- "6379:6379"
-

backend:
build:
context: .
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
package com.techeer.backend;

import io.github.cdimascio.dotenv.Dotenv;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.servers.Server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
@OpenAPIDefinition(
servers = {
@Server(url="/", description = "Default Server url")
}
)
@EnableCaching
public class BackendApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
package com.techeer.backend.api.user.controller;

import com.techeer.backend.api.user.converter.UserConverter;
import com.techeer.backend.api.user.domain.User;
import com.techeer.backend.api.user.dto.request.SignUpRequest;
import com.techeer.backend.api.user.dto.response.UserInfoResponse;
import com.techeer.backend.api.user.service.UserService;
import com.techeer.backend.global.common.response.CommonResponse;
import com.techeer.backend.global.error.ErrorCode;
import com.techeer.backend.global.error.exception.BusinessException;
import com.techeer.backend.global.jwt.JwtToken;
import com.techeer.backend.global.jwt.service.JwtService;
import com.techeer.backend.global.success.SuccessCode;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -24,6 +30,8 @@
@RequestMapping("/api/v1")
public class UserController {
private final UserService userService;
private final JwtService jwtService;
private final RedisTemplate<String, String> redisTemplate;

@Operation(summary = "유저 정보")
@GetMapping("/user")
Expand All @@ -48,12 +56,17 @@ public CommonResponse<Void> logoutUser() {

@Operation(summary = "액세스 토큰 재발급")
@PostMapping("/reissue")
public CommonResponse<Void> reGenerateAccessToken(@RequestHeader("Access-Token") String accessToken,
@RequestHeader("Refresh-Token") String refreshToken,
HttpServletResponse response) {
JwtToken result = userService.reissueToken(accessToken, refreshToken);
response.setHeader("Access-Token", result.getAccessToken());
response.setHeader("Refresh-Token", result.getRefreshToken());
public CommonResponse<Void> reGenerateAccessToken(@CookieValue(value = "refreshToken", required = false) String refreshToken,
HttpServletResponse response) throws IOException {
// Cookie에 있는 Refresh Token을 이용하여 새로운 Access Token을 발급
JwtToken token = userService.reissueAccessToken(refreshToken);

// 토큰을 못 만들었으면 메인 화면으로 리다이렉트
if(token == null) {throw new BusinessException(ErrorCode.INVALID_REFRESH_TOKEN);}

// Access Token과 Refresh Token을 쿠키에 저장
jwtService.addTokenCookies(response, token.getAccessToken(), token.getRefreshToken());

return CommonResponse.of(SuccessCode.TOKEN_REISSUE_OK, null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,10 @@ public void onLogout() {
this.refreshToken = null;
}

public void updateRefreshToken(String newRefreshToken) {
public String updateRefreshToken(String newRefreshToken) {
String oldRefreshToken = this.refreshToken;
this.refreshToken = newRefreshToken;
return oldRefreshToken;
}

public void addResume(Resume resume) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsernameAndSocialType(String username, SocialType socialType); // 사용자 이름으로 사용자 찾기

Optional<User> findByEmail(String email); // 이메일로 사용자 찾기

Optional<User> findByRefreshToken(String refreshToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
import com.techeer.backend.global.jwt.JwtToken;
import com.techeer.backend.global.jwt.service.JwtService;
import java.util.Map;
import java.util.Optional;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
Expand All @@ -27,6 +27,7 @@ public class UserService {

private final UserRepository userRepository;
private final JwtService jwtService;
private final RedisTemplate<String, String> redisTemplate;

public void signup(SignUpRequest signUpReq) {
User user = this.getLoginUser();
Expand Down Expand Up @@ -61,37 +62,16 @@ public User getLoginUser() {
}

@Transactional
public JwtToken reissueToken(String accessToken, String refreshToken) {
// Refresh Token 검증
if (!jwtService.isTokenValid(refreshToken)) {
throw new BusinessException(ErrorCode.INVALID_REFRESH_TOKEN);
}

Object[] emailAndSocialType = jwtService.extractEmailAndSocialType(accessToken);

if (emailAndSocialType.length < 1) {
throw new BusinessException(ErrorCode.USER_NOT_AUTHENTICATED);
}

String email = (String) emailAndSocialType[0];

Optional<User> user = userRepository.findByEmail(email);
if (user.isEmpty()) {
throw new BusinessException(ErrorCode.USER_NOT_AUTHENTICATED);
}

String userRefreshToken = user.get().getRefreshToken();
public JwtToken reissueAccessToken(String refreshToken) {

// db에 리프레시 토큰 없을 경우(logout)
if (userRefreshToken == null || !userRefreshToken.equals(refreshToken)) {
throw new BusinessException(ErrorCode.USER_NOT_AUTHENTICATED);
}
// Refresh Token 검증
if (!jwtService.isRefreshTokenValid(refreshToken)) {return null;}

User user = this.getLoginUser();
return JwtToken.builder()
.accessToken(jwtService.createAccessToken(email))
.refreshToken(jwtService.reIssueRefreshToken(user.orElse(null)))
.accessToken(jwtService.createAccessToken(user.getEmail()))
.refreshToken(refreshToken)
.build();

}

public String mockSignup(String id) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.techeer.backend.global.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
@Value("${REDIS_PORT}")
private int redisPort;

@Value("${REDIS_HOST}")
private String redisHost;

@Value("${REDIS_PASSWORD}")
private String redisPassword;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(redisHost);
redisStandaloneConfiguration.setPort(redisPort);
redisStandaloneConfiguration.setPassword(redisPassword);
return new LettuceConnectionFactory(redisStandaloneConfiguration);
}

@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);

// 키와 값의 직렬화 방식 설정
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new StringRedisSerializer());

template.afterPropertiesSet();
return template;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// .requestMatchers("/api/v1/resumes/search").hasRole("ADMIN") // resume
// .requestMatchers("/api/v1/resumes/**").hasAnyRole("ADMIN", "REGULAR") // resume
// .anyRequest().permitAll()
// )
// .authorizeHttpRequests(requests ->
// requests.anyRequest().permitAll() // 모든 요청을 모든 사용자에게 허용
// )
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(
Expand All @@ -77,7 +74,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
"/swagger-ui/index.html/**",
"/api-docs/**",
"/signup.html",
"/api/v1/reissue",
"/login",
"/api/v1/mock/signup"
).permitAll()
.anyRequest().authenticated()
Expand All @@ -90,10 +87,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.successHandler(oAuth2LoginSuccessHandler) // 2.
.failureHandler(oAuth2LoginFailureHandler) // 3.
)
// .exceptionHandling(authenticationManager ->authenticationManager
// .authenticationEntryPoint(jwtAuthenticationEntryPoint)
// .accessDeniedHandler(jwtAccessDeniedHandler)
// )
.addFilterBefore(new JwtAuthenticationFilter(jwtService, userRepository),
UsernamePasswordAuthenticationFilter.class);
return http.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,22 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String requestURI = request.getRequestURI();
log.info("requestURI: {}", requestURI);

// 특정 경로 이외에는 필터를 건너뜀
if (!requestURI.startsWith("/api/v1/")) {
//log.info("requestURI: {}", requestURI);
filterChain.doFilter(request, response);
return;
}
log.info("requestURI: {}", requestURI);
checkAccessTokenAndAuthentication(request, response, filterChain);
}

public void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
log.info("checkAccessTokenAndAuthentication() 호출");
jwtService.extractAccessTokenFromCookie(request)
.filter(jwtService::isTokenValid)
.filter(jwtService::isAccessTokenValid)
.ifPresent(accessToken -> {
log.info("유효한 Access Token이 발견되었습니다: {}", accessToken);

Expand Down
Loading

0 comments on commit aef931f

Please sign in to comment.