Backend
home
🕒

2024. 9. 13 (JWT 실습)

생성일
2025/01/24 05:52
태그
JWT 토큰 활용한 프로젝트 - 미니 이커머스 프로젝트 진행 과정 정리

Springboot (패키지별로 정리)

config
WebSecurityConfig
package com.kosta.config; import java.util.Collections; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 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.annotation.web.configurers.HttpBasicConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import com.kosta.repository.UserRepository; import com.kosta.security.JwtAuthenticationFilter; import com.kosta.security.JwtAuthenticationService; import com.kosta.security.JwtProperties; import com.kosta.security.JwtProvider; import com.kosta.security.LoginCustomAuthenticationFilter; import com.kosta.util.TokenUtils; import lombok.RequiredArgsConstructor; @Configuration @EnableWebSecurity @RequiredArgsConstructor public class WebSecurityConfig { private final UserDetailsService userDetailsService; private final UserRepository userRepository; private final JwtProperties jwtProperties; // JWT PROVIDER 생성자 호출 private JwtProvider jwtProvider() { return new JwtProvider(userDetailsService, jwtProperties); } // TokenUtils 생성자 호출 private TokenUtils tokenUtils() { return new TokenUtils(jwtProvider(), jwtProperties); } // JwtAuthenticationService 생성자 호출 private JwtAuthenticationService jwtAuthenticationService() { return new JwtAuthenticationService(tokenUtils(), userRepository); } // 인증 관리자 (AuthenticationManager) 설정 @Bean AuthenticationManager authenticationManager() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailsService); authProvider.setPasswordEncoder(bCryptPasswordEncoder()); return new ProviderManager(authProvider); } // 암호화 빈 @Bean BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } // HTTP 요청에 따른 보안 구성 @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 경로 권한 설정 http.authorizeHttpRequests(auth -> // 특정 URL 경로에 대해서는 인증 없이 접근 가능 auth.requestMatchers( new AntPathRequestMatcher("/api/join"), // 회원가입 new AntPathRequestMatcher("/api/duplicate"), // 이메일 중복체크 new AntPathRequestMatcher("/api/refresh"), // 토큰 재발급 new AntPathRequestMatcher("/api/oauth/**") // 토큰 재발급 ).permitAll() // AuthController 중 나머지들은 "ADMIN"만 가능 .requestMatchers( new AntPathRequestMatcher("/api/product", "POST"), new AntPathRequestMatcher("/api/product", "DELETE"), new AntPathRequestMatcher("/api/product", "PATCH") ).hasRole("ADMIN") // 그 밖의 다른 요청들은 인증을 통과한(로그인한) 사용자라면 모두 접근할 수 있도록 한다. .anyRequest().authenticated() ); // 무상태성 세션 관리 http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); // 특정 경로(로그인)에 대한 필터 추가 [authenticationManager, jwtAuthenticationService 생성자를 호출해 매개변수로 등록] http.addFilterBefore(new LoginCustomAuthenticationFilter(authenticationManager(), jwtAuthenticationService()), UsernamePasswordAuthenticationFilter.class); // (토큰을 통해 검증할 수 있도록) 필터 추가 [jwtProvider 생성자를 호출해 매개변수로 등록] http.addFilterAfter(new JwtAuthenticationFilter(jwtProvider()), UsernamePasswordAuthenticationFilter.class); // HTTP 기본 설정 http.httpBasic(HttpBasicConfigurer::disable); // CSRF 비활성화 http.csrf(AbstractHttpConfigurer::disable); // CORS 설정 http.cors(corsConfig -> corsConfig.configurationSource(corsConfigurationSource())); return http.getOrBuild(); } @Bean CorsConfigurationSource corsConfigurationSource() { return request -> { CorsConfiguration config = new CorsConfiguration(); config.setAllowedHeaders(Collections.singletonList("*")); config.setAllowedMethods(Collections.singletonList("*")); config.setAllowedOriginPatterns(Collections.singletonList("http://localhost:3000")); config.setAllowCredentials(true); return config; }; } }
Java
복사
controller
CommonController
package com.kosta.controller; import java.util.Map; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; 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.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.fasterxml.jackson.databind.JsonNode; import com.kosta.domain.request.JoinUser; import com.kosta.domain.response.ErrorResponse; import com.kosta.domain.response.TokenResponse; import com.kosta.service.UserService; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @RestController @RequestMapping("/api") @RequiredArgsConstructor public class CommonController { private final UserService us; // 이메일 중복체크 -> /api/duplicate [GET] // RequestParam {email} // Response 200 or 409 @GetMapping("/duplicate") public ResponseEntity<?> checkEmailDuplicate(@RequestParam("email") String email){ boolean exists = us.checkEmailExists(email); if (exists) { return ResponseEntity.status(HttpStatus.CONFLICT).body(new ErrorResponse("이미 사용 중인 이메일")); } return ResponseEntity.ok().build(); } // 회원가입 -> /api/join [POST] // Request {name, email, password} // Response 201 {???} or 400 @PostMapping("/join") public ResponseEntity<?> registerUser(@RequestBody JoinUser joinUser) { try { us.registerUser(joinUser); return ResponseEntity.status(HttpStatus.CREATED).build(); } catch (Exception e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse("입력 값 오류")); } } // 토큰 재발급 -> /api/refresh [POST] // Response 200 {accessToken} or 401 @PostMapping("/refresh") public ResponseEntity<?> refreshToken(HttpServletRequest request, HttpServletResponse response) { Map<String, String> newTokenMap = us.refreshToken(request, response); if (newTokenMap == null) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse("리프레시 토큰 오류")); } TokenResponse tokenResponse = TokenResponse.builder().accessToken(newTokenMap.get("accessToken")).build(); return ResponseEntity.ok(tokenResponse); } @GetMapping("/oauth/{company}") public ResponseEntity<?> google(@RequestParam("code") final String code, @PathVariable("company") final String company, HttpServletResponse response) { Map<String, String> tokenMap = us.oAuthLogin(code, company, response); if (tokenMap == null) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse("로그인 오류")); } TokenResponse tokenResponse = TokenResponse.builder().accessToken(tokenMap.get("accessToken")).build(); return ResponseEntity.ok(tokenResponse); } }
Java
복사
ProductController
package com.kosta.controller; import java.util.List; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; 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.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.kosta.domain.request.ProductAdd; import com.kosta.domain.request.ProductEdit; import com.kosta.domain.response.ErrorResponse; import com.kosta.domain.response.ProductResponse; import com.kosta.service.ProductService; import lombok.RequiredArgsConstructor; @RestController @RequestMapping("/api/product") @RequiredArgsConstructor public class ProductController { private final ProductService ps; // 상품 등록 -> /api/product [POST] ★관리자만 // Request {name, price} // Response 201 {id, name, price} or 401 or 403 @PostMapping("") public ResponseEntity<?> addProduct(@RequestBody ProductAdd productAdd) { try { ProductResponse productReponse = ps.addProduct(productAdd); return ResponseEntity.status(HttpStatus.CREATED).body(productReponse); } catch (Exception e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse("상품 등록 실패")); } } // 상품 수정 -> /api/product [PATCH] ★관리자만 // Request {id, name, price} // Response 200 {id, name, price} or 401 or 403 @PatchMapping("") public ResponseEntity<?> updateProduct(@RequestBody ProductEdit productEdit) { try { ProductResponse productReponse = ps.updateProduct(productEdit); return ResponseEntity.ok().body(productReponse); } catch (Exception e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse("상품 수정 실패")); } } // 상품 삭제 -> /api/product?id=~ [DELETE] ★관리자만 // RequestParam {id} // Response 200 {id, name, price} or 401 or 403 @DeleteMapping("") public ResponseEntity<?> deleteProduct(@RequestParam("id") Long id) { try { ProductResponse productReponse = ps.deleteProduct(id); return ResponseEntity.ok().body(productReponse); } catch (Exception e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse("상품 삭제 실패")); } } @GetMapping("") public ResponseEntity<?> getProduct(@RequestParam(name = "id", required = false) Long id) { try { if (id == null) { // 상품 전체 목록 -> /api/product [GET] ☆로그인해야만 // Response 200 List<id, name, price> or 401 or 403 List<ProductResponse> allProducts = ps.getAllProducts(); return ResponseEntity.ok().body(allProducts); } // 상품 디테일 -> /api/product?id=~ [GET] ☆로그인해야만 RequestParam {id} Response 200 {id, name, price} or 401 or 403 ProductResponse productReponse = ps.getProductById(id); return ResponseEntity.ok().body(productReponse); } catch (Exception e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse("상품 조회 실패")); } } }
Java
복사
domain
request
JoinUser
package com.kosta.domain.request; import lombok.Getter; @Getter public class JoinUser { private String name, email, password; }
Java
복사
LoginUser
package com.kosta.domain.request; import lombok.Getter; @Getter public class LoginUser { private String email, password; }
Java
복사
ProductAdd
package com.kosta.domain.request; import lombok.Getter; @Getter public class ProductAdd { private String name; private int price; }
Java
복사
ProductEdit
package com.kosta.domain.request; import lombok.Getter; @Getter public class ProductEdit { private Long id; private String name; private int price; }
Java
복사
response
ErrorResponse
package com.kosta.domain.response; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor public class ErrorResponse { private String msg; }
Java
복사
ProductResponse
package com.kosta.domain.response; import lombok.Builder; import lombok.Getter; @Getter @Builder public class ProductResponse { private Long id; private String name; private int price; }
Java
복사
TokenResponse
package com.kosta.domain.response; import lombok.Builder; import lombok.Getter; @Getter @Builder public class TokenResponse { private String accessToken; }
Java
복사
entity
Product
package com.kosta.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Entity @Data @Builder @AllArgsConstructor @NoArgsConstructor public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(updatable = false) private Long id; @Column(nullable = false) private String name; @Column(nullable = false) private int price; }
Java
복사
User
package com.kosta.entity; import java.util.Collection; import java.util.List; import java.util.Map; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.oauth2.core.user.OAuth2User; import com.kosta.domain.RoleEnum; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Entity @Data @Builder @AllArgsConstructor @NoArgsConstructor public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(updatable = false) private Long id; @Column(nullable = false, unique = true) private String email; @Column(nullable = false) private String name; @Column(nullable = true) private String password; @Column(nullable = false) private boolean oAuth = false; @Column(nullable = false) @Enumerated(EnumType.STRING) @Builder.Default private RoleEnum role = RoleEnum.ROLE_USER; // 권한 컬럼 추가 (기본값 ROLE_USER) @Column(name="refresh_token", nullable = true) private String refreshToken; @Override public Collection<? extends GrantedAuthority> getAuthorities() { // 권한 목록 반환 return List.of(new SimpleGrantedAuthority(role.name())); } @Override public String getUsername() { // 로그인할 사용자 명을 이메일로 대체 return email; } }
Java
복사
repository
ProductRepository
package com.kosta.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.kosta.entity.Product; @Repository public interface ProductRepository extends JpaRepository<Product, Long>{ }
Java
복사
UserRepository
package com.kosta.repository; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import com.kosta.entity.User; @Repository public interface UserRepository extends JpaRepository<User, Long>{ Optional<User> findByEmail(String email); boolean existsByEmail(String email); }
Java
복사
security
JwtAuthenticationFilter
package com.kosta.security; import java.io.IOException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtProvider jwtProvider; private final static String HEADER_AUTHORIZATION = "Authorization"; private final static String TOKEN_PREFIX = "Bearer "; // HTTP 요청이 들어올 때마다 실행되는 필터 @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String header = request.getHeader(HEADER_AUTHORIZATION); String token = getAccessToken(header); if (token != null && jwtProvider.validateToken(token)) { // 유효한 토큰인 경우 Authentication authentication = jwtProvider.getAuthenticationByToken(token); SecurityContextHolder.getContext().setAuthentication(authentication); } // 그 다음 요청 처리 체인을 이어서 진행 filterChain.doFilter(request, response); } private String getAccessToken(String header) { log.info("[getAccessToken] 토큰 값 추출, {}", header); if (header != null && header.startsWith(TOKEN_PREFIX)) { return header.substring(TOKEN_PREFIX.length()); } return null; } }
Java
복사
JwtAuthenticationService
package com.kosta.security; import java.io.IOException; import java.util.Map; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import com.kosta.domain.response.TokenResponse; import com.kosta.entity.User; import com.kosta.repository.UserRepository; import com.kosta.util.TokenUtils; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor public class JwtAuthenticationService { private final TokenUtils tokenUtils; private final UserRepository userRepository; void successAuthentication(HttpServletResponse response, Authentication authResult) throws IOException { User user = (User) authResult.getPrincipal(); Map<String, String> tokenMap = tokenUtils.generateToken(user); String accessToken = tokenMap.get("accessToken"); String refreshToken = tokenMap.get("refreshToken"); // 리프레시 토큰을 DB에 저장! user.setRefreshToken(refreshToken); userRepository.save(user); // 생성된 리프레시 토큰을 cookie에 담아 응답 tokenUtils.setRefreshTokenCookie(response, refreshToken); // 생성된 액세스 토큰을 LoginResponse에 담아 응답 TokenResponse loginResponse = TokenResponse.builder().accessToken(accessToken).build(); tokenUtils.writeResponse(response, loginResponse); } }
Java
복사
JwtProperties
package com.kosta.security; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import lombok.Getter; import lombok.Setter; @Getter @Setter @Component @ConfigurationProperties("jwt") // application.yml에 설정한 jwt.issuer, jwt.secret_key,...이 매핑된다. public class JwtProperties { private String issuer; private String secretKey; private int accessDuration; private int refreshDuration; }
Java
복사
JwtProvider
package com.kosta.security; import java.util.Date; import javax.crypto.SecretKey; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Service; import com.kosta.entity.User; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @Service @RequiredArgsConstructor public class JwtProvider { private final UserDetailsService userDetailsService; // JWT 관련 설정 정보 객체 주입 private final JwtProperties jwtProperties; // JWT Access 토큰 생성 메소드 public String generateAccessToken(User user) { log.info("[generateAccessToken] 액세스 토큰을 생성합니다."); Date now = new Date(); Date expiredDate = new Date(now.getTime() + jwtProperties.getAccessDuration()); return makeToken(user, expiredDate); } // JWT Refresh 토큰 생성 메소드 public String generateRefreshToken(User user) { log.info("[generateRefreshToken] 리프레시 토큰을 생성합니다."); Date now = new Date(); Date expiredDate = new Date(now.getTime() + jwtProperties.getRefreshDuration()); return makeToken(user, expiredDate); } // 토큰 생성 공통 메소드 (실제로 JWT 토큰 생성) private String makeToken(User user, Date expiredDate) { String token = Jwts.builder() .header().add("typ", "JWT") // JWT 타입을 명시 .and() .issuer(jwtProperties.getIssuer()) // 발행자 정보 설정 .issuedAt(new Date()) // 발행일시 설정 .expiration(expiredDate) // 만료 시간 설정 .subject(user.getEmail()) // 토큰의 주제(Subject) 설정 _ 사용자 이메일 .claim("id", user.getId()) .claim("role", user.getRole().name()) .signWith(getSecretKey(), Jwts.SIG.HS256) // 비밀키와 해시 알고리즘 사용하여 토큰 설명값 설정 .compact(); // 토큰 정보들을 최종적으로 압축해서 문자열로 반환 log.info("[makeToken] 완성된 토큰 : {}", token); return token; } // 비밀키 만드는 메소드 private SecretKey getSecretKey() { return Keys.hmacShaKeyFor(jwtProperties.getSecretKey().getBytes()); } // 토큰이 유효한지 검증 메소드 public boolean validateToken(String token) { log.info("[validateToken] 토큰 검증을 시작합니다."); try { Jwts.parser() .verifyWith(getSecretKey()) // 비밀키로 서명 검증 .build() .parseSignedClaims(token); // 서명된 클레임을 파싱.. log.info("토큰 검증 통과"); return true; } catch (Exception e) { e.printStackTrace(); } log.info("토큰 검증 실패"); return false; } // 토큰에서 정보(Claim) 추출 메소드 private Claims getClaims(String token) { return Jwts.parser() .verifyWith(getSecretKey()) // 비밀키로 서명 검증 .build() .parseSignedClaims(token) // 서명된 클레임을 파싱.. .getPayload(); // 파싱된 클레임에서 페이로드(실제 클레임)를 반환 } // 토큰에서 인증 정보 반환하는 메소드 public Authentication getAuthenticationByToken(String token) { log.info("[getAuthenticationByToken] 토큰 인증 정보 조회"); String userEmail = getUserEmailByToken(token); User user = (User) userDetailsService.loadUserByUsername(userEmail); Authentication authentication = new UsernamePasswordAuthenticationToken( user, token, user.getAuthorities() ); return authentication; } // 토큰에서 사용자 이메일만 추출하는 메소드 public String getUserEmailByToken(String token) { log.info("[getUserEmailByToken] 토큰 기반 회원 식별 정보 추출"); Claims claims = getClaims(token); String email = claims.get("sub", String.class); return email; } }
Java
복사
LoginCustomAuthenticationFilter
package com.kosta.security; import java.io.IOException; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import com.fasterxml.jackson.databind.ObjectMapper; import com.kosta.domain.request.LoginUser; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; @Slf4j public class LoginCustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private JwtAuthenticationService jwtAuthenticationService; // 로그인되는 경로 및 메소드 타입 지정 private static final AntPathRequestMatcher LOGIN_PATH = new AntPathRequestMatcher("/api/login", "POST"); public LoginCustomAuthenticationFilter(AuthenticationManager authenticationManager, JwtAuthenticationService jwtAuthenticationService) { super(LOGIN_PATH); setAuthenticationManager(authenticationManager); this.jwtAuthenticationService = jwtAuthenticationService; } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { // POST, /api/login 에 요청이 들어오면 진행 LoginUser loginUser= null; // 1. Body에 있는 로그인 정보 ({"email": "~~", "password: "~~" } 를 가져오기 try { log.info("[attemptAuthentication] 로그인 정보 가져오기"); ObjectMapper objectMapper = new ObjectMapper(); loginUser = objectMapper.readValue(request.getInputStream(), LoginUser.class); } catch (IOException e) { throw new RuntimeException("로그인 요청 파라미터 이름 확인 필요 (로그인 불가)"); } // 2. email과 password를 기반으로 AuthenticationToken 생성! log.info("[attemptAuthentication] AuthenticationToken 생성"); UsernamePasswordAuthenticationToken uPAT = new UsernamePasswordAuthenticationToken(loginUser.getEmail(), loginUser.getPassword()); // 3. 인증 시작 (AuthenticationManager의 authenticate 메소드가 동작할 때 -> loadUserByUsername) log.info("[attemptAuthentication] 인증 시작"); Authentication authenticate = getAuthenticationManager().authenticate(uPAT); return authenticate; } @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { log.info("[successfulAuthentication] 로그인 성공 -> 토큰 생성 시작"); jwtAuthenticationService.successAuthentication(response, authResult); } @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { log.info("[unsuccessfulAuthentication] 로그인 실패"); response.setStatus(HttpStatus.UNAUTHORIZED.value()); } }
Java
복사
service
ProductService
package com.kosta.service; import java.util.List; import com.kosta.domain.request.ProductAdd; import com.kosta.domain.request.ProductEdit; import com.kosta.domain.response.ProductResponse; public interface ProductService { ProductResponse addProduct(ProductAdd productAdd) throws Exception; ProductResponse updateProduct(ProductEdit productEdit) throws Exception; ProductResponse deleteProduct(Long id) throws Exception; List<ProductResponse> getAllProducts() throws Exception; ProductResponse getProductById(Long id) throws Exception; }
Java
복사
UserService
package com.kosta.service; import java.util.Map; import org.springframework.http.ResponseEntity; import com.fasterxml.jackson.databind.JsonNode; import com.kosta.domain.request.JoinUser; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; public interface UserService { boolean checkEmailExists(String email); void registerUser(JoinUser joinUser) throws Exception; Map<String, String> refreshToken(HttpServletRequest request, HttpServletResponse response); Map<String, String> oAuthLogin(String code, String company, HttpServletResponse response); }
Java
복사
Impl
ProductServiceImpl
package com.kosta.service.impl; import java.util.List; import org.springframework.stereotype.Service; import com.kosta.domain.request.ProductAdd; import com.kosta.domain.request.ProductEdit; import com.kosta.domain.response.ProductResponse; import com.kosta.entity.Product; import com.kosta.repository.ProductRepository; import com.kosta.service.ProductService; import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor public class ProductServiceImpl implements ProductService { private final ProductRepository pr; @Override public ProductResponse addProduct(ProductAdd productAdd) throws Exception { Product product = Product.builder() .name(productAdd.getName()) .price(productAdd.getPrice()) .build(); Product savedProduct = pr.save(product); return ProductResponse.builder() .id(savedProduct.getId()) .name(savedProduct.getName()) .price(savedProduct.getPrice()) .build(); } @Override public ProductResponse updateProduct(ProductEdit productEdit) throws Exception { Product product = pr.findById(productEdit.getId()).orElseThrow(() -> new Exception()); if (!product.getName().equals(productEdit.getName())) product.setName(productEdit.getName()); if (product.getPrice() != (productEdit.getPrice())) product.setPrice(productEdit.getPrice()); Product updatedProduct = pr.save(product); return ProductResponse.builder() .id(updatedProduct.getId()) .name(updatedProduct.getName()) .price(updatedProduct.getPrice()) .build(); } @Override public ProductResponse deleteProduct(Long id) throws Exception { Product product = pr.findById(id).orElseThrow(() -> new Exception()); pr.delete(product); return ProductResponse.builder() .id(product.getId()) .name(product.getName()) .price(product.getPrice()) .build(); } @Override public List<ProductResponse> getAllProducts() throws Exception { return pr.findAll().stream().map( product -> ProductResponse.builder() .id(product.getId()) .name(product.getName()) .price(product.getPrice()) .build() ).toList(); } @Override public ProductResponse getProductById(Long id) throws Exception { Product product = pr.findById(id).orElseThrow(() -> new Exception()); return ProductResponse.builder() .id(product.getId()) .name(product.getName()) .price(product.getPrice()) .build(); } }
Java
복사
UserDetailsServiceImpl
package com.kosta.service.impl; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import com.kosta.entity.User; import com.kosta.repository.UserRepository; import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor public class UserDetailsServiceImpl implements UserDetailsService { private final UserRepository ur; @Override public User loadUserByUsername(String email) throws UsernameNotFoundException { User user = ur.findByEmail(email).orElse(null); return user; } }
Java
복사
UserServiceImpl
package com.kosta.service.impl; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Optional; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import org.springframework.web.server.ResponseStatusException; import com.fasterxml.jackson.databind.JsonNode; import com.kosta.domain.request.JoinUser; import com.kosta.entity.User; import com.kosta.repository.UserRepository; import com.kosta.security.JwtProvider; import com.kosta.service.UserService; import com.kosta.util.TokenUtils; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @Service @RequiredArgsConstructor public class UserServiceImpl implements UserService { private final UserRepository ur; private final BCryptPasswordEncoder bCryptPasswordEncoder; private final TokenUtils tokenUtils; private final JwtProvider jwtProvider; @Override public boolean checkEmailExists(String email) { return ur.existsByEmail(email); } @Override public void registerUser(JoinUser joinUser) throws Exception { String encodedPassword = bCryptPasswordEncoder.encode(joinUser.getPassword()); User user = User.builder() .name(joinUser.getName()) .email(joinUser.getEmail()) .password(encodedPassword) .build(); ur.save(user); } @Override public Map<String, String> refreshToken(HttpServletRequest request, HttpServletResponse response) { String refreshToken = extractRefreshTokenFromCookie(request); if (refreshToken != null && jwtProvider.validateToken(refreshToken)) { String userEmail = jwtProvider.getUserEmailByToken(refreshToken); User user = ur.findByEmail(userEmail).orElse(null); if (user != null && user.getRefreshToken().equals(refreshToken)) { Map<String, String> tokenMap = tokenUtils.generateToken(user); user.setRefreshToken(tokenMap.get("refreshToken")); ur.save(user); tokenUtils.setRefreshTokenCookie(response, tokenMap.get("refreshToken")); return tokenMap; } } return null; } private String extractRefreshTokenFromCookie(HttpServletRequest req) { Cookie[] cookies = req.getCookies(); if (cookies != null) { for (Cookie c: cookies) { if (c.getName().equals("refreshToken")) { return c.getValue(); } } } return null; } @Override public Map<String, String> oAuthLogin(String code, String company, HttpServletResponse response) { // 클라이언트부터 가져온 정보를 통해 해당 SNS 회사에 accessToken 요청 String accessToken = getAccessToken(code, company); // accsssToken 정보를 통해 사용자 정보 생성 User user = generateOAuthUser(accessToken); // 사용자 로그인 진행 Map<String, String> tokenMap = tokenUtils.generateToken(user); user.setRefreshToken(tokenMap.get("refreshToken")); ur.save(user); tokenUtils.setRefreshTokenCookie(response, tokenMap.get("refreshToken")); return tokenMap; } private String getAccessToken(String code, String company) { // 코드 변환 String decodedCode = URLDecoder.decode(code, StandardCharsets.UTF_8); // 헤더 설정 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); String clientId = "1059700581094-jkoubuj3925vsp7fs5g2qncs8d7norem.apps.googleusercontent.com"; String clientSecret = "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"; headers.setBasicAuth(clientId, clientSecret); MultiValueMap<String, String> parameterMap = new LinkedMultiValueMap<>(); parameterMap.add("code", decodedCode); parameterMap.add("grant_type", "authorization_code"); parameterMap.add("redirect_uri", "http://localhost:3000/oauth"); HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(parameterMap, headers); // https://oauth2.googleapis.com/token RestTemplate restTemplate = new RestTemplate(); ResponseEntity<Map> response = restTemplate.postForEntity("https://oauth2.googleapis.com/token", requestEntity, Map.class); String accessToken = (String) response.getBody().get("access_token"); return accessToken; } private User generateOAuthUser(String accessToken) { String GOOGLE_USERINFO_REQUEST_URL = "https://www.googleapis.com/oauth2/v3/userinfo"; HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + accessToken); HttpEntity<?> entity = new HttpEntity<>(headers); RestTemplate restTemplate = new RestTemplate(); ResponseEntity<JsonNode> response = restTemplate.exchange( GOOGLE_USERINFO_REQUEST_URL, HttpMethod.GET, entity, JsonNode.class ); if (response.getStatusCode().is2xxSuccessful()) { JsonNode responseBody = response.getBody(); log.info("SNS 로그인 계정 정보 : {}", responseBody); if (responseBody == null) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Response body is null"); } if (responseBody.has("email") && responseBody.has("name")) { String email = responseBody.get("email").asText(); String name = responseBody.get("name").asText(); Optional<User> optUser = ur.findByEmail(email); User user = null; if (optUser.isEmpty()) { log.info("없는 사용자 -> 새로 가입합니다"); // 가입 처리 user = User.builder() .email(email) .name(name) .oAuth(true) .build(); ur.save(user); } else { user = optUser.get(); if (!user.isOAuth()) { user.setOAuth(true); ur.save(user); } } return user; } } throw new RuntimeException("SNS 로그인 에러"); } }
Java
복사
util
TokenUtils
package com.kosta.util; import java.io.IOException; import java.util.HashMap; import java.util.Map; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; import com.kosta.domain.response.TokenResponse; import com.kosta.entity.User; import com.kosta.security.JwtProperties; import com.kosta.security.JwtProvider; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @Component @RequiredArgsConstructor public class TokenUtils { private final JwtProvider jwtProvider; private final JwtProperties jwtProperties; // 토큰 생성 public Map<String, String> generateToken(User user) { String accessToken = jwtProvider.generateAccessToken(user); String refreshToken = jwtProvider.generateRefreshToken(user); Map<String, String> tokenMap = new HashMap<String, String>(); tokenMap.put("accessToken", accessToken); tokenMap.put("refreshToken", refreshToken); return tokenMap; } // JSON 응답 전송 public void writeResponse(HttpServletResponse response, TokenResponse loginResponse) throws IOException { ObjectMapper objectMapper = new ObjectMapper(); String jsonResponse = objectMapper.writeValueAsString(loginResponse); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().write(jsonResponse); } public void setRefreshTokenCookie(HttpServletResponse response, String refreshToken) { System.out.println(jwtProperties.getRefreshDuration()); Cookie refreshTokenCookie = new Cookie("refreshToken", refreshToken); refreshTokenCookie.setHttpOnly(true); // JavaScript에서 변경 불가 refreshTokenCookie.setSecure(false); // HTTPS가 아니여도 사용 가능! (지금은) refreshTokenCookie.setPath("/"); refreshTokenCookie.setMaxAge(jwtProperties.getRefreshDuration() / 1000); // Token 유효기간 1일 response.addCookie(refreshTokenCookie); } }
Java
복사

React

패키지 설치
yarn install yarn create react-app . yarn add styled-components yarn add react-router-dom yarn add @mui/material @emotion/react @emotion/styled @emotion/css yarn add jwt-decode yarn add react-cookie yarn add install sweetalert2
Shell
복사
폴더별로 정리
components
Navbar.jsx
import { AppBar, Button, Stack, Switch, Toolbar, Typography } from '@mui/material'; import { Link, useNavigate } from 'react-router-dom'; import { useCookies } from 'react-cookie'; import { useEffect, useState } from 'react'; import { jwtDecode } from 'jwt-decode'; const Navbar = ({ toggleDarkMode, isDarkMode }) => { const [ cookies, , removeCookie ] = useCookies(['accessToken']); const navigate = useNavigate(); const [role, setRole] = useState(); const handleLogout = () => { removeCookie('accessToken', { path: '/' }); navigate('/'); }; useEffect(() => { if (cookies.accessToken) { const role = jwtDecode(cookies.accessToken).role; setRole(role); } else { setRole(null); } }, [cookies.accessToken]); return ( <AppBar position='static'> <Toolbar> <Typography variant='h6' sx={{ flexGrow: 1}}> { role === "ROLE_USER" || role === "ROLE_ADMIN" && <Button color="inherit" LinkComponent={Link} to="/products">상품</Button> } { role === "ROLE_ADMIN" && <Button color="inherit" LinkComponent={Link} to="/admin">관리자</Button> } { !role && <Button color="inherit" LinkComponent={Link} to="/join">회원가입</Button> } { role && <Button color="inherit" onClick={handleLogout}> 로그아웃 </Button> } </Typography> <Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}> <Typography>Light</Typography> <Switch onChange={toggleDarkMode} defaultChecked={isDarkMode} /> <Typography>Dark</Typography> </Stack> </Toolbar> </AppBar> ); } export default Navbar;
JavaScript
복사
ProtectedRoutes.js
import { jwtDecode } from "jwt-decode"; import { useCookies } from "react-cookie"; import { Navigate } from 'react-router-dom'; export const LoginUserRoute = ({ comp: Comp }) => { const [ cookies ] = useCookies(["accessToken"]); const token = cookies.accessToken; if(!token) { return <Navigate to="/" /> } return <Comp /> } export const AdminRoute = ({ comp: Comp }) => { const [ cookies ] = useCookies(["accessToken"]); const token = cookies.accessToken; if(!token) { return <Navigate to="/" /> } const decodeToken = jwtDecode(token); if (decodeToken.role !== "ROLE_ADMIN") { return <Navigate to="/" /> } return <Comp /> }
JavaScript
복사
hooks
useDarkMode.js
import { useEffect, useState } from "react"; export const useDarkMode = () => { const [isDarkMode, setIsDarkMode] = useState(() => { const savedMode = localStorage.getItem('darkMode'); return savedMode === "true" ? true : false; }); const toggleDarkMode = () => setIsDarkMode(mode => !mode); useEffect(() => { localStorage.setItem('darkMode', isDarkMode); }, [isDarkMode]); return { isDarkMode, toggleDarkMode } }
JavaScript
복사
pages
AdminPage.jsx
import { useForm } from 'react-hook-form'; import { Container, TextField, Button, Typography, List, ListItem, ListItemText } from '@mui/material'; import { useEffect, useState } from 'react'; import { productAPI } from '../services/product'; const AdminPage = () => { const { register, handleSubmit, reset, setValue } = useForm(); const [products, setProducts] = useState([]); const [editingProduct, setEditingProduct] = useState(null); const getAllProducts = async () => { try { const response = await productAPI.getAll(); setProducts(response.data); } catch (error) { console.error('상품 가져오기 실패', error); } }; useEffect(() => { getAllProducts(); }, []); const onSubmit = async (data) => { try { if (editingProduct) { await productAPI.updateProduct({ ...data, id: editingProduct.id }); setEditingProduct(null); } else { await productAPI.addProduct(data); } reset(); getAllProducts(); } catch (error) { console.error('상품 등록 또는 수정 실패', error); } }; const handleEdit = (product) => { setEditingProduct(product); setValue('name', product.name); setValue('price', product.price) }; const handleDelete = async (id) => { try { await productAPI.deletePost(id); getAllProducts(); } catch (error) { console.error('삭제 실패', error); } }; return ( <Container> <Typography variant="h4" gutterBottom> Admin - Product Management(상품 관리) </Typography> <form onSubmit={handleSubmit(onSubmit)}> <TextField fullWidth margin="normal" label="Product Name" {...register('name', { required: true })} /> <TextField fullWidth margin="normal" label="Price" type="number" {...register('price', { required: true })} /> <Button type="submit" variant="contained" color="primary" sx={{ mt: 2 }}> {editingProduct ? 'Update Product' : 'Add Product'} </Button> </form> <Typography variant="h5" gutterBottom sx={{ mt: 4 }}> Product List </Typography> <List> {products.map((product) => ( <ListItem key={product.id} divider> <ListItemText primary={`${product.name} - $${product.price}`} /> <Button onClick={() => handleEdit(product)} color="primary">Edit</Button> <Button onClick={() => handleDelete(product.id)} color="secondary">Delete</Button> </ListItem> ))} </List> </Container> ); } export default AdminPage;
JavaScript
복사
JoinPage.jsx
import { userAPI } from '../services/user'; import { useForm } from 'react-hook-form'; import { TextField, Button, Container, Typography, Box } from '@mui/material'; import { useNavigate } from 'react-router-dom'; const JoinPage = () => { const { register, handleSubmit, formState: { errors } } = useForm(); const navigate = useNavigate(); const onSubmit = async (data) => { try { await userAPI.join(data); navigate('/'); } catch (error) { console.error('Join failed:', error); } }; return ( <Container maxWidth="xs"> <Box mt={5}> <Typography variant="h4" align="center">Sign Up</Typography> <form onSubmit={handleSubmit(onSubmit)}> <TextField fullWidth margin="normal" label="Name" variant="outlined" {...register('name', { required: 'Name is required' })} error={!!errors.name} helperText={errors.name?.message} /> <TextField fullWidth margin="normal" label="Email" variant="outlined" {...register('email', { required: 'Email is required' })} error={!!errors.email} helperText={errors.email?.message} /> <TextField fullWidth margin="normal" label="Password" type="password" variant="outlined" {...register('password', { required: 'Password is required' })} error={!!errors.password} helperText={errors.password?.message} /> <Button type="submit" variant="contained" color="primary" fullWidth sx={{ mt: 2 }} > Sign Up </Button> </form> </Box> </Container> ); } export default JoinPage;
JavaScript
복사
LoginPage.jsx
import { useForm } from 'react-hook-form'; import { TextField, Button, Container, Typography, Box } from '@mui/material'; import { userAPI } from '../services/user'; import { useCookies } from 'react-cookie'; import { useNavigate } from 'react-router-dom'; const LoginPage = () => { const { register, handleSubmit, formState: { errors }, setError } = useForm(); const [, setCookie] = useCookies(['accessToken']); const navigate = useNavigate(); const onSubmit = async (data) => { try { const response = await userAPI.login(data); setCookie('accessToken', response.data.accessToken, { path: '/' }); navigate('/products'); } catch (error) { if (error.status === 401) { setError("email", { type: "manual", message: "로그인 실패! 아이디를 확인해주세요" }); setError("password", { type: "manual", message: "로그인 실패! 비밀번호를 확인해주세요" }); } } }; return ( <Container maxWidth="xs"> <Box mt={5}> <Typography variant="h4" align="center">Login</Typography> <form onSubmit={handleSubmit(onSubmit)}> <TextField fullWidth margin="normal" label="Email" variant="outlined" {...register('email', { required: 'Email is required' })} error={!!errors.email} helperText={errors.email?.message} /> <TextField fullWidth margin="normal" label="Password" type="password" variant="outlined" {...register('password', { required: 'Password is required' })} error={!!errors.password} helperText={errors.password?.message} /> <Button type="submit" variant="contained" color="primary" fullWidth sx={{ mt: 2 }} > Login </Button> </form> <GoogleLogin></GoogleLogin> </Box> </Container> ); } const GoogleLogin = () => { const GOOGLE_URL = `https://accounts.google.com/o/oauth2/v2/auth?scope=email%20profile&response_type=code&redirect_uri=${process.env.REACT_APP_GOOGLE_REDIRECT_URL}&client_id=${process.env.REACT_APP_GOOGLE_ID}`; return ( <a href={GOOGLE_URL}> 구글 </a> ) } export default LoginPage;
JavaScript
복사
ProductPage.jsx
import { Container, Typography, List, ListItem, ListItemText } from '@mui/material'; import { productAPI } from '../services/product'; import { useEffect, useState } from 'react'; const ProductPage = () => { const [products, setProducts] = useState([]); useEffect(() => { const getAllProducts = async () => { try { const response = await productAPI.getAll(); setProducts(response.data); } catch (error) { console.error('상품 가져오기 실패', error); } }; getAllProducts(); }, []); return ( <Container> <Typography variant="h4" gutterBottom> Product List </Typography> <List> {products.map((product) => ( <ListItem key={product.id}> <ListItemText primary={`${product.name} - $${product.price}`} /> </ListItem> ))} </List> </Container> ); } export default ProductPage;
JavaScript
복사
services
api.js
import axios from "axios"; import { getCookie, removeCookie, setCookie } from "../utils/cookieUtil"; const api = axios.create({ baseURL: `${process.env.REACT_APP_REST_SERVER}`, withCredentials: true // HttpOnly 쿠키 속성으로 저장된 refreshToken 전송한다. }); api.interceptors.request.use( (config) => { const token = getCookie("accessToken"); if (token) { config.headers.Authorization = `Bearer ${token}`; } else { delete config.headers.Authorization; } return config; }, (err) => { return Promise.reject(err); } ); api.interceptors.response.use( (res) => { return res; }, async (err) => { // 원래 403으로 실패했던 요청 const originalReq = err.config; // 만약에 권한이 없다는 에러가 나오고 무한루프에 빠져있다면 if (err.response.status == 403 && !originalReq._retry) { originalReq._retry = true; // 플래그 설정 try { // 토큰 재발급 해주도록 할 것이다. const response = await refreshTokenHandler(); // 정상 재발급 시 if (response.status === 200) { // token값 로컬스토리지에 저장 // localStorage.setItem("token", response.data.accessToken); // token값 쿠키에 저장 setCookie("accessToken", response.data.accessToken); // 헤더에 새로운 token 추가해서 originalReq.headers.Authorization = `Bearer ${response.data.accessToken}`; // 실패했던 요청 다시 보내기 return api.request(originalReq); } } catch (error) { console.log("토큰 재발급 실패"); } } removeCookie("accessToken"); return Promise.reject(err); } ); const refreshTokenHandler = async () => { try { if (getCookie("accessToken")) { const response = await api.post("/refresh"); return response; } } catch (error) { throw error; } } export default api;
JavaScript
복사
product.js
import api from './api'; export const productAPI = { getAll : () => api.get("/product"), getProduct : (id) => api.get('/product', { params : {id} }), addProduct : (data) => api.post("/product", data), updateProduct : (data) => api.patch("/product", data), deletePost : (id) => api.delete(`/product`, { params : {id} }) }
JavaScript
복사
user.js
import api from './api'; export const userAPI = { login: (data) => api.post("/login", data), join : (data) => api.post("/join", data), duplicate: (email) => api.get("/duplicate", { params : {email} }), }
JavaScript
복사
theme
theme.js
import { createTheme } from "@mui/material"; export const lightTheme = createTheme({ palette: { mode: 'light', primary: { main: '#1976d2', }, background: { default: '#f5f5f5' } } }); export const darkTheme = createTheme({ palette: { mode: 'dark', primary: { main: '#90caf9', }, background: { default: '#121212' } } });
JavaScript
복사
utils
cookieUtil.js
import { Cookies } from "react-cookie"; const cookies = new Cookies(); export const setCookie = (name, value, options) => { return cookies.set(name, value, {...options}); } export const getCookie = (name) => { return cookies.get(name); } export const removeCookie = (name) => { return cookies.remove(name); }
JavaScript
복사
App.js
import { CssBaseline, ThemeProvider } from '@mui/material'; import './App.css'; import { useDarkMode } from './hooks/useDarkMode'; import { lightTheme, darkTheme } from './theme/theme'; import { useCookies } from 'react-cookie'; import { jwtDecode } from 'jwt-decode'; import { useEffect } from 'react'; import Navbar from './components/Navbar'; import { Route, BrowserRouter, Routes, useParams, useSearchParams, useLocation } from 'react-router-dom'; import LoginPage from './pages/LoginPage.jsx'; import JoinPage from './pages/JoinPage'; import ProductPage from './pages/ProductPage'; import AdminPage from './pages/AdminPage'; import { LoginUserRoute, AdminRoute } from './components/ProtectedRoutes'; import axios from 'axios'; function App() { const { isDarkMode, toggleDarkMode } = useDarkMode(); const [ cookies, setCookie, removeCookie ] = useCookies(['accessToken']); useEffect(() => { if (cookies.accessToken) { const decodeToken = jwtDecode(cookies.accessToken); console.log("로그인한 사용자 정보", decodeToken); } }, [cookies.accessToken]); return ( <ThemeProvider theme={isDarkMode ? darkTheme : lightTheme}> <CssBaseline /> <BrowserRouter> <Navbar toggleDarkMode={toggleDarkMode} isDarkMode={isDarkMode} /> <Routes> <Route path="" element={<LoginPage />} /> <Route path="/join" element={<JoinPage />} /> <Route path="/products" element={<LoginUserRoute comp={ProductPage} />} /> <Route path="/admin" element={<AdminRoute comp={AdminPage} />} /> <Route path="/oauth" element={<GoogleRedirect />} /> </Routes> </BrowserRouter> </ThemeProvider> ); } const GoogleRedirect = () => { const code = new URLSearchParams(window.location.search).get("code"); useEffect(()=> { axios.get(`${process.env.REACT_APP_REST_SERVER}/oauth/google?code=${code}`) }) } export default App;
JavaScript
복사