Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix :: 로그인 401 에러 수정 #46

Merged
merged 1 commit into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand All @@ -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) {
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,28 +13,20 @@
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
@RequiredArgsConstructor
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)
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
import java.io.IOException;
import java.time.LocalDateTime;

/*
요청 -> JwtExceptionFilter -> JwtAuthenticationFilter로 필터를 구성해서 JwtAuthenticationFilter에서 던진 예외를 JwtExceptionFilter에서 처리
*/

@Component
public class JwtExceptionFilter extends OncePerRequestFilter {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ public void handle(HttpServletRequest request, HttpServletResponse response, Acc
//필요한 권한 없이 접근 시 403
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
// }
}