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
복사