diff --git a/src/main/java/com/danpoong/onchung/domain/auth/service/AuthService.java b/src/main/java/com/danpoong/onchung/domain/auth/service/AuthService.java index 3531a56..60cd3de 100644 --- a/src/main/java/com/danpoong/onchung/domain/auth/service/AuthService.java +++ b/src/main/java/com/danpoong/onchung/domain/auth/service/AuthService.java @@ -34,13 +34,13 @@ public LoginResponseDto login(HttpServletResponse response, KakaoLoginParam para UserInfo userInfo = findOrCreateUser(kakaoUserInfo); TokenDto tokenDto = tokenProvider.generateToken(userInfo.getId()); - userInfo.updateRefreshToken(tokenDto.refreshToken()); - TokenUtil.saveRefreshToken(response, tokenDto.refreshToken()); + userInfo.updateRefreshToken(tokenDto.getRefreshToken()); + TokenUtil.saveRefreshToken(response, tokenDto.getRefreshToken()); return LoginResponseDto.builder() .isNewUser(userInfo.getBirthDate() == null) .username(userInfo.getNickname()) - .accessToken(tokenDto.accessToken()) + .accessToken(tokenDto.getAccessToken()) .build(); } @@ -58,11 +58,11 @@ public ReissueResponseDto reissueToken(HttpServletRequest request, HttpServletRe throw new RefreshTokenMismatchException("Refresh Token = " + refreshToken); } - TokenDto tokenDto = tokenProvider.reissueToken(userInfo.getId()); - userInfo.updateRefreshToken(tokenDto.refreshToken()); + TokenDto tokenDto = tokenProvider.reissue(userInfo.getId()); + userInfo.updateRefreshToken(tokenDto.getRefreshToken()); TokenUtil.updateRefreshTokenCookie(request, response, refreshToken); - return ReissueResponseDto.builder().accessToken(tokenDto.accessToken()).build(); + return ReissueResponseDto.builder().accessToken(tokenDto.getAccessToken()).build(); } private UserInfo findOrCreateUser(KakaoUserInfo kakaoUserInfo) { @@ -73,7 +73,7 @@ private UserInfo findOrCreateUser(KakaoUserInfo kakaoUserInfo) { private UserInfo createNewUser(KakaoUserInfo kakaoUserInfo) { UserInfo userInfo = UserInfo.builder() .email(kakaoUserInfo.getEmail()) - .nickname(kakaoUserInfo.getNickname()) + .nickname(kakaoUserInfo.getName()) .build(); return userInfoRepository.save(userInfo); diff --git a/src/main/java/com/danpoong/onchung/global/config/SecurityConfig.java b/src/main/java/com/danpoong/onchung/global/config/SecurityConfig.java index 28a9524..ba1d53b 100644 --- a/src/main/java/com/danpoong/onchung/global/config/SecurityConfig.java +++ b/src/main/java/com/danpoong/onchung/global/config/SecurityConfig.java @@ -1,6 +1,8 @@ package com.danpoong.onchung.global.config; -import com.danpoong.onchung.global.security.jwt.filter.JwtAuthenticationFilter; +import com.danpoong.onchung.global.security.jwt.TokenProvider; +import com.danpoong.onchung.global.security.jwt.filter.JwtAuthorizationFilter; +import com.danpoong.onchung.global.security.jwt.filter.JwtExceptionFilter; import com.danpoong.onchung.global.security.jwt.filter.handler.JwtAccessDeniedHandler; import com.danpoong.onchung.global.security.jwt.filter.handler.JwtAuthenticationEntryPoint; import lombok.RequiredArgsConstructor; @@ -11,11 +13,8 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.web.cors.CorsConfigurationSource; @Configuration @EnableWebSecurity @@ -23,16 +22,11 @@ public class SecurityConfig { private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; private final JwtAccessDeniedHandler jwtAccessDeniedHandler; - private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final TokenProvider tokenProvider; @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http, CorsConfigurationSource corsConfigurationSource) throws Exception { - return http + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http .csrf(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) @@ -43,8 +37,10 @@ public SecurityFilterChain filterChain(HttpSecurity http, CorsConfigurationSourc .accessDeniedHandler(jwtAccessDeniedHandler); }) .authorizeHttpRequests((authorizeRequests) -> authorizeRequests.anyRequest().authenticated()) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .build(); + .addFilterBefore(new JwtAuthorizationFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(new JwtExceptionFilter(), JwtAuthorizationFilter.class); + + return http.build(); } @Bean diff --git a/src/main/java/com/danpoong/onchung/global/config/WebConfig.java b/src/main/java/com/danpoong/onchung/global/config/WebConfig.java index 7f2d1cd..0f32e33 100644 --- a/src/main/java/com/danpoong/onchung/global/config/WebConfig.java +++ b/src/main/java/com/danpoong/onchung/global/config/WebConfig.java @@ -9,7 +9,7 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:5173", "http://localhost:8080", "https://youthmap.site", "https://api.youthmap.site") + .allowedOrigins("http://localhost:5173", "http://localhost:8080", "http://localhost:3000", "https://youthmap.site", "https://api.youthmap.site") .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE") .allowCredentials(true) .maxAge(3600); diff --git a/src/main/java/com/danpoong/onchung/global/security/jwt/TokenProvider.java b/src/main/java/com/danpoong/onchung/global/security/jwt/TokenProvider.java index 1efc746..995809c 100644 --- a/src/main/java/com/danpoong/onchung/global/security/jwt/TokenProvider.java +++ b/src/main/java/com/danpoong/onchung/global/security/jwt/TokenProvider.java @@ -8,25 +8,27 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import java.security.Key; import java.util.Collections; import java.util.Date; -@Component @Slf4j +@Component public class TokenProvider { - @Value("${jwt.expiration.access}") - private String accessTokenExpireTime; + private static final String BEARER_TYPE = "Bearer"; + + @Value("${jwt.expiration.access}") //access token 만료 시간 + private Long accessTokenExpiration; + + //refresh token 만료 시간 @Value("${jwt.expiration.refresh}") - private String refreshTokenExpireTime; + private Long refreshTokenExpiration; - private final Key key; + private final Key key; //jwt의 토큰 서명을 생성하고 검증하는데 사용 - public TokenProvider(@Value("${jwt.secret}") String secretKey) { + public TokenProvider(@Value("${jwt.secret}") String secretKey) { //secret key 생성 byte[] keyBytes = Decoders.BASE64.decode(secretKey); this.key = Keys.hmacShaKeyFor(keyBytes); } @@ -36,76 +38,78 @@ public TokenDto generateToken(Long userId) { String refreshToken = generateRefreshToken(); return TokenDto.builder() + .grantType(BEARER_TYPE) .accessToken(accessToken) .refreshToken(refreshToken) .build(); } - //Access Token 생성 + //Access Token 만료 시 사용 + public TokenDto reissue(Long userId) { + String newAccessToken = generateAccessToken(userId); + String newRefreshToken = generateRefreshToken(); + + return TokenDto.builder() + .grantType(BEARER_TYPE) + .accessToken(newAccessToken) + .refreshToken(newRefreshToken) + .build(); + } + public String generateAccessToken(Long userId) { Date now = new Date(); - Date accessExpiryDate = new Date(now.getTime() + Long.parseLong(accessTokenExpireTime)); + Date accessTokenExpireTime = new Date(now.getTime() + accessTokenExpiration); + //Payload에 사용자를 찾을 수 있게 정보와 권한이 저장되어야 한다. return Jwts.builder() .setSubject(String.valueOf(userId)) - .setExpiration(accessExpiryDate) + .setExpiration(accessTokenExpireTime) .signWith(key, SignatureAlgorithm.HS512) - .compact(); + .compact(); //컴팩트화 -> JWT를 문자열로 반환하는 역할 } - //Refresh Token 생성 + //refresh token은 access token과 다르게 재발급을 위한 것이므로 중요 정보(claim) 없이 만료 시간만 담아도 된다. public String generateRefreshToken() { Date now = new Date(); - Date refreshExpiryDate = new Date(now.getTime() + Long.parseLong(refreshTokenExpireTime)); + Date refreshTokenExpireTime = new Date(now.getTime() + refreshTokenExpiration); return Jwts.builder() - .setExpiration(refreshExpiryDate) + .setExpiration(refreshTokenExpireTime) .signWith(key, SignatureAlgorithm.HS512) - .compact(); + .compact(); //컴팩트화 -> JWT를 문자열로 반환하는 역할 } - public TokenDto reissueToken(Long userId) { - String newAccessToken = generateAccessToken(userId); - String newRefreshToken = generateRefreshToken(); - - return TokenDto.builder() - .accessToken(newAccessToken) - .refreshToken(newRefreshToken) - .build(); - } - - public Authentication getAuthentication(String accessToken) { + public Authentication getAuthentication(String token) { Claims claims = Jwts.parserBuilder() .setSigningKey(key) .build() - .parseClaimsJws(accessToken) + .parseClaimsJws(token) .getBody(); - UserDetails principal = new User(claims.getSubject(), "", Collections.emptyList()); + Long userId = Long.parseLong(claims.getSubject()); - return new UsernamePasswordAuthenticationToken(principal, "", Collections.emptyList()); + return new UsernamePasswordAuthenticationToken(userId, "", Collections.emptyList()); } public boolean validateToken(String token) { try { + //setSigningKey() -> JWT의 서명 확인 시 필요한 key 설정 + //parseClaimsJws() -> JWT 토큰을 분석, 확인 Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token); return true; - } catch (UnsupportedJwtException | MalformedJwtException e) { //형식이 잘못되었거나, 지원되지 않는 형식 - log.error("JWT is not supported or has an incorrect format"); - } catch (SignatureException e) { //서명이 올바르지 않을 때 - log.error("JWT signature validation failed"); - } catch (ExpiredJwtException e) { //만료 - log.error("JWT is expired"); - } catch (IllegalArgumentException e) { //토큰이 비어있거나, 공백이 들었을 때 - log.error("JWT is null or empty or only white space"); - } catch (Exception e) { //이외의 예외 - log.error("JWT Exception other than the above cases"); + } catch (UnsupportedJwtException | MalformedJwtException exception) { + log.info("유효하지 않은 JWT 토큰"); + } catch (ExpiredJwtException exception) { + log.info("만료된 JWT 토큰입니다."); + } catch (IllegalArgumentException exception) { + log.info("JWT 토큰 값이 들어있지 않습니다."); + } catch (SignatureException exception) { + log.info("JWT 토큰 서명이 유효하지 않습니다."); } - return false; } } diff --git a/src/main/java/com/danpoong/onchung/global/security/jwt/dto/TokenDto.java b/src/main/java/com/danpoong/onchung/global/security/jwt/dto/TokenDto.java index c4ddde9..5c77a4f 100644 --- a/src/main/java/com/danpoong/onchung/global/security/jwt/dto/TokenDto.java +++ b/src/main/java/com/danpoong/onchung/global/security/jwt/dto/TokenDto.java @@ -1,10 +1,20 @@ package com.danpoong.onchung.global.security.jwt.dto; import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; -@Builder -public record TokenDto( - String accessToken, - String refreshToken -) { +@Getter +@NoArgsConstructor +public class TokenDto { + private String grantType; + private String accessToken; + private String refreshToken; + + @Builder + public TokenDto(String grantType, String accessToken, String refreshToken) { + this.grantType = grantType; + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } } diff --git a/src/main/java/com/danpoong/onchung/global/security/jwt/filter/JwtAuthenticationFilter.java b/src/main/java/com/danpoong/onchung/global/security/jwt/filter/JwtAuthorizationFilter.java similarity index 54% rename from src/main/java/com/danpoong/onchung/global/security/jwt/filter/JwtAuthenticationFilter.java rename to src/main/java/com/danpoong/onchung/global/security/jwt/filter/JwtAuthorizationFilter.java index 037b620..83a5fa2 100644 --- a/src/main/java/com/danpoong/onchung/global/security/jwt/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/danpoong/onchung/global/security/jwt/filter/JwtAuthorizationFilter.java @@ -14,33 +14,36 @@ import java.io.IOException; +/* +HTTP 요청을 중간에서 가로채서 jwt 처리, 해당 토큰으로 사용자 인증 +jwt 토큰 추출 -> 유효성 검사 -> if 유효: 토큰으로 사용자 인증 후 SecurityContextHolder에 인증 정보 설정 + */ -@RequiredArgsConstructor @Component -public class JwtAuthenticationFilter extends OncePerRequestFilter { +@RequiredArgsConstructor +public class JwtAuthorizationFilter extends OncePerRequestFilter { //커스텀 필터 클래스 private static final String AUTHORIZATION_HEADER = "Authorization"; private static final String BEARER_PREFIX = "Bearer "; private final TokenProvider tokenProvider; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - String token = resolveToken(request); + String jwt = resolveToken(request); - if (StringUtils.hasText(token) && tokenProvider.validateToken(token)) { - Authentication authentication = tokenProvider.getAuthentication(token); - SecurityContextHolder.getContext().setAuthentication(authentication); + if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { + Authentication authentication = tokenProvider.getAuthentication(jwt); //토큰 사용하여 사용자 인증 + SecurityContextHolder.getContext().setAuthentication(authentication); //SecurityContextHolder에 인증 정보 설정 } - filterChain.doFilter(request, response); + filterChain.doFilter(request, response); //이 필터의 작업이 끝난 후 다음 필터로 http 요청 전달 } - private String resolveToken(HttpServletRequest request) { + private String resolveToken(HttpServletRequest request) { //여기서 HttpServeletRequest는 HTTP 요청 정보를 캡슐화한 객체 String token = request.getHeader(AUTHORIZATION_HEADER); - if (StringUtils.hasText(token) && token.startsWith(BEARER_PREFIX)) { - return token.substring(BEARER_PREFIX.length()); + if (StringUtils.hasText(token) && token.startsWith(BEARER_PREFIX)) { //토큰에서 추출한 헤더 값이 null이 아닌지 && "Bearer "로 시작하는지 -> "Bearer " 다음에 토큰이 오는 것이 관례 + return token.substring(BEARER_PREFIX.length()); //"Bearer " 제외 후 실제 토큰 문자열을 반환 } - return null; } } diff --git a/src/main/java/com/danpoong/onchung/global/security/jwt/filter/JwtExceptionFilter.java b/src/main/java/com/danpoong/onchung/global/security/jwt/filter/JwtExceptionFilter.java index a8be64f..58b473e 100644 --- a/src/main/java/com/danpoong/onchung/global/security/jwt/filter/JwtExceptionFilter.java +++ b/src/main/java/com/danpoong/onchung/global/security/jwt/filter/JwtExceptionFilter.java @@ -15,6 +15,10 @@ import java.io.IOException; import java.time.LocalDateTime; +/* +요청 -> JwtExceptionFilter -> JwtAuthenticationFilter로 필터를 구성해서 JwtAuthenticationFilter에서 던진 예외를 JwtExceptionFilter에서 처리 + */ + @Component public class JwtExceptionFilter extends OncePerRequestFilter { @Override diff --git a/src/main/java/com/danpoong/onchung/global/security/jwt/filter/handler/JwtAccessDeniedHandler.java b/src/main/java/com/danpoong/onchung/global/security/jwt/filter/handler/JwtAccessDeniedHandler.java index d221d62..1da393a 100644 --- a/src/main/java/com/danpoong/onchung/global/security/jwt/filter/handler/JwtAccessDeniedHandler.java +++ b/src/main/java/com/danpoong/onchung/global/security/jwt/filter/handler/JwtAccessDeniedHandler.java @@ -20,4 +20,4 @@ public void handle(HttpServletRequest request, HttpServletResponse response, Acc //필요한 권한 없이 접근 시 403 response.sendError(HttpServletResponse.SC_FORBIDDEN); } -} +} \ No newline at end of file diff --git a/src/main/java/com/danpoong/onchung/global/security/oauth/kakao/KakaoUserInfo.java b/src/main/java/com/danpoong/onchung/global/security/oauth/kakao/KakaoUserInfo.java index 2f3aa50..f6ff389 100644 --- a/src/main/java/com/danpoong/onchung/global/security/oauth/kakao/KakaoUserInfo.java +++ b/src/main/java/com/danpoong/onchung/global/security/oauth/kakao/KakaoUserInfo.java @@ -11,16 +11,20 @@ public class KakaoUserInfo { private KakaoAccount kakaoAccount; @JsonIgnoreProperties(ignoreUnknown = true) - record KakaoAccount(String email, Profile profile) {} + record KakaoAccount(String email, String name) {} - @JsonIgnoreProperties(ignoreUnknown = true) - record Profile(String nickname) {} +// @JsonIgnoreProperties(ignoreUnknown = true) +// record Profile(String nickname) {} public String getEmail() { return kakaoAccount.email; } - public String getNickname() { - return kakaoAccount.profile.nickname; + public String getName() { + return kakaoAccount.name; } + +// public String getNickname() { +// return kakaoAccount.profile.nickname; +// } } \ No newline at end of file