Feat: JWT 토큰 추가 - S11P21S002-16

This commit is contained in:
김용수 2024-08-28 18:00:47 +09:00
parent dc3fe59d1f
commit d788a111c4
17 changed files with 369 additions and 156 deletions

View File

@ -19,4 +19,5 @@ public abstract class OAuth2Attribute {
public abstract String getEmail(); public abstract String getEmail();
public abstract String getProfileImage(); public abstract String getProfileImage();
} }

View File

@ -1,6 +1,5 @@
package com.worlabel.domain.auth.controller; package com.worlabel.domain.auth.controller;
import com.worlabel.domain.auth.dto.AuthMemberDto;
import com.worlabel.global.annotation.CurrentUser; import com.worlabel.global.annotation.CurrentUser;
import com.worlabel.global.response.SuccessResponse; import com.worlabel.global.response.SuccessResponse;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -16,9 +15,13 @@ import org.springframework.web.bind.annotation.RestController;
public class AuthController { public class AuthController {
// TODO: 리이슈 처리 // TODO: 리이슈 처리
@GetMapping("/")
public String login() {
return "로그인 성공";
}
@GetMapping("/user-info") @GetMapping("/user-info")
public SuccessResponse<AuthMemberDto> getMemberInfo(@CurrentUser AuthMemberDto currentMember){ public SuccessResponse<Integer> getMemberInfo(@CurrentUser Integer currentMember){
return SuccessResponse.of(currentMember); return SuccessResponse.of(currentMember);
} }
} }

View File

@ -1,6 +1,6 @@
package com.worlabel.domain.auth.entity; package com.worlabel.domain.auth.entity;
import com.worlabel.domain.auth.dto.AuthMemberDto; import com.worlabel.domain.auth.entity.dto.AuthMemberDto;
import com.worlabel.domain.member.entity.Member; import com.worlabel.domain.member.entity.Member;
import lombok.Getter; import lombok.Getter;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
@ -11,34 +11,38 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
public class CustomOAuth2Member implements OAuth2User { public class CustomOAuth2User implements OAuth2User {
@Getter @Getter
private final AuthMemberDto authMemberDto; private transient final AuthMemberDto authMember;
public CustomOAuth2Member(Member member) { public CustomOAuth2User(Member member) {
authMemberDto = AuthMemberDto.of(member); authMember = AuthMemberDto.of(member);
} }
@Override @Override
public Map<String, Object> getAttributes() { public Map<String, Object> getAttributes() {
// OAuth2 제공자로부터 받은 사용자 속성 데이터를 반환합니다. // OAuth2 제공자로부터 받은 사용자 속성 데이터를 반환합니다.
return Map.of( return Map.of(
"id", authMemberDto.getId(), "id", authMember.getId(),
"email", authMemberDto.getEmail(), "email", authMember.getEmail(),
"role", authMemberDto.getRole() "role", authMember.getRole()
); );
} }
@Override @Override
public Collection<? extends GrantedAuthority> getAuthorities() { public Collection<? extends GrantedAuthority> getAuthorities() {
// 사용자의 역할(RoleType) 권한으로 변환하여 반환합니다. // 사용자의 역할(RoleType) 권한으로 변환하여 반환합니다.
return List.of(new SimpleGrantedAuthority(authMemberDto.getRole())); return List.of(new SimpleGrantedAuthority(authMember.getRole()));
} }
@Override @Override
public String getName() { public String getName() {
// 사용자의 고유 식별자를 반환합니다. 여기서는 이메일을 사용합니다. // 사용자의 고유 식별자를 반환합니다. 여기서는 이메일을 사용합니다.
return authMemberDto.getEmail(); return authMember.getEmail();
}
public int getId(){
return authMember.getId();
} }
} }

View File

@ -1,4 +1,4 @@
package com.worlabel.domain.auth.dto; package com.worlabel.domain.auth.entity.dto;
import com.worlabel.domain.member.entity.Member; import com.worlabel.domain.member.entity.Member;
import lombok.Getter; import lombok.Getter;

View File

@ -0,0 +1,11 @@
package com.worlabel.domain.auth.entity.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class JwtToken {
private String accessToken;
private String refreshToken;
}

View File

@ -2,7 +2,7 @@ package com.worlabel.domain.auth.service;
import com.worlabel.domain.auth.attribute.OAuth2Attribute; import com.worlabel.domain.auth.attribute.OAuth2Attribute;
import com.worlabel.domain.auth.attribute.OAuth2AttributeFactory; import com.worlabel.domain.auth.attribute.OAuth2AttributeFactory;
import com.worlabel.domain.auth.entity.CustomOAuth2Member; import com.worlabel.domain.auth.entity.CustomOAuth2User;
import com.worlabel.domain.auth.entity.ProviderType; import com.worlabel.domain.auth.entity.ProviderType;
import com.worlabel.domain.member.entity.Member; import com.worlabel.domain.member.entity.Member;
import com.worlabel.domain.member.repository.MemberRepository; import com.worlabel.domain.member.repository.MemberRepository;
@ -13,6 +13,7 @@ import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/** /**
* OAuth2 사용자 서비스 클래스 * OAuth2 사용자 서비스 클래스
@ -21,27 +22,34 @@ import org.springframework.stereotype.Service;
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class CustomOAuth2MemberService extends DefaultOAuth2UserService { public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final MemberRepository memberRepository; private final MemberRepository memberRepository;
@Override @Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// OAuth2 인증을 통해 사용자 정보를 가져온다.
OAuth2User user = super.loadUser(userRequest); OAuth2User user = super.loadUser(userRequest);
log.debug("oAuth2User: {}", user.getAttributes());
// OAuth2 제공자 정보 가져오기
ProviderType provider = ProviderType.valueOf(userRequest.getClientRegistration().getRegistrationId().toUpperCase()); ProviderType provider = ProviderType.valueOf(userRequest.getClientRegistration().getRegistrationId().toUpperCase());
OAuth2Attribute attribute = OAuth2AttributeFactory.parseAttribute(provider, user.getAttributes());
log.debug("provider: {}, user: {}", provider, user);
Member member = memberRepository.findByProviderMemberId(attribute.getId()) // Provider 사용자 속성 파싱
.orElseGet(() -> { OAuth2Attribute attribute = OAuth2AttributeFactory.parseAttribute(provider, user.getAttributes());
Member newMember = Member.of(attribute.getId(), attribute.getEmail(), attribute.getName(), attribute.getProfileImage()); log.debug("OAuth2 -> provider: {}, user: {}", provider, user);
// 이메일 기반으로 기존 사용자를 찾는다.
Member findMember = memberRepository.findByEmail(attribute.getEmail())
.orElseGet(() -> createMember(attribute, provider));
log.debug("Loaded member : {}", findMember);
return new CustomOAuth2User(findMember);
}
@Transactional
protected Member createMember(OAuth2Attribute attribute, ProviderType provider) {
Member newMember = Member.create(attribute, provider);
memberRepository.save(newMember); memberRepository.save(newMember);
return newMember; return newMember;
});
log.debug("member : {}", member);
return new CustomOAuth2Member(member);
} }
} }

View File

@ -1,9 +1,122 @@
package com.worlabel.domain.auth.service; package com.worlabel.domain.auth.service;
import lombok.extern.slf4j.Slf4j; import com.worlabel.domain.auth.entity.CustomOAuth2User;
import com.worlabel.domain.auth.entity.dto.JwtToken;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@Slf4j import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Service @Service
public class JwtTokenService { public class JwtTokenService {
private final SecretKey secretKey;
private final Long tokenExpiration;
private final Long refreshTokenExpiration;
public JwtTokenService(
@Value("${spring.jwt.secret}") String key,
@Value("${auth.tokenExpiry}") long tokenExpiry,
@Value("${auth.refreshTokenExpiry}") long refreshExpiry
) {
secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
tokenExpiration = tokenExpiry;
refreshTokenExpiration = refreshExpiry;
}
public JwtToken generateToken(int memberId){
long now = System.currentTimeMillis();
Date accessTokenExpire = new Date(now + tokenExpiration);
Date refreshTokenExpire = new Date(now + refreshTokenExpiration);
String accessToken = Jwts.builder()
.claim("id",memberId)
.claim("type","access")
.expiration(accessTokenExpire)
.signWith(secretKey)
.compact();
String refreshToken = Jwts.builder()
.claim("id",memberId)
.claim("type","refresh")
.expiration(refreshTokenExpire)
.signWith(secretKey)
.compact();
return new JwtToken(accessToken, refreshToken);
}
public JwtToken generateTokenByRefreshToken(String refreshToken) throws Exception{
if(isTokenExpired(refreshToken) && isRefreshToken(refreshToken)){
return generateToken(Integer.parseInt(refreshToken));
}
throw new Exception("유효하지 않은 토큰입니다.");
}
public int parseId(String token) throws Exception {
return parseClaims(token).get("id", Integer.class);
}
// 토큰 만료 여부 확인
public boolean isTokenExpired(String token){
try{
Claims claims = parseClaims(token);
return claims.getExpiration().before(new Date());
}catch (ExpiredJwtException e) {
return true; // 만료된 토큰
} catch (Exception e) {
return false; // 다른 오류일 경우
}
}
public boolean isRefreshToken(String token) {
return isTokenType(token, "refresh");
}
public boolean isAccessToken(String token) {
return isTokenType(token, "access");
}
private boolean isTokenType(String token, String expectedType) {
try {
Claims claims = parseClaims(token);
String tokenType = claims.get("type", String.class);
return expectedType.equals(tokenType);
} catch (Exception e) {
return false;
}
}
public long getRefreshTokenExpiration(){
return refreshTokenExpiration;
}
private Claims parseClaims(String token) throws Exception {
String message;
try{
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
}catch (ExpiredJwtException e){
message = "유효기간이 만료된 토큰입니다.";
}catch (MalformedJwtException e){
message = "잘못된 형식의 토큰입니다.";
}catch (IllegalArgumentException e) {
message = "잘못된 인자입니다.";
}catch (Exception e){
message = "토큰 파싱 중 에러가 발생했습니다.";
}
throw new Exception(message);
}
} }

View File

@ -1,5 +1,6 @@
package com.worlabel.domain.member.entity; package com.worlabel.domain.member.entity;
import com.worlabel.domain.auth.attribute.OAuth2Attribute;
import com.worlabel.domain.auth.entity.ProviderType; import com.worlabel.domain.auth.entity.ProviderType;
import com.worlabel.domain.comment.entity.Comment; import com.worlabel.domain.comment.entity.Comment;
import com.worlabel.global.common.BaseEntity; import com.worlabel.global.common.BaseEntity;
@ -69,12 +70,13 @@ public class Member extends BaseEntity {
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY,cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(mappedBy = "member", fetch = FetchType.LAZY,cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> commentList = new ArrayList<>(); private List<Comment> commentList = new ArrayList<>();
public static Member of(String providerMemberId, String email, String nickname, String profileImage) { public static Member create(OAuth2Attribute attribute, ProviderType provider) {
Member member = new Member(); Member member = new Member();
member.providerMemberId = providerMemberId; member.providerMemberId = attribute.getId();
member.email = email; member.email = attribute.getEmail();
member.nickname = nickname; member.nickname = attribute.getName();
member.profileImage = profileImage; member.profileImage = attribute.getProfileImage();
member.provider = provider;
return member; return member;
} }
} }

View File

@ -6,7 +6,5 @@ import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional; import java.util.Optional;
public interface MemberRepository extends JpaRepository<Member, Integer> { public interface MemberRepository extends JpaRepository<Member, Integer> {
Optional<Member> findByProviderMemberId(String providerMemberId);
Optional<Member> findByEmail(String email); Optional<Member> findByEmail(String email);
} }

View File

@ -1,10 +1,10 @@
package com.worlabel.global.config; package com.worlabel.global.config;
import com.worlabel.domain.auth.service.CustomOAuth2MemberService; import com.worlabel.domain.auth.service.CustomOAuth2UserService;
import com.worlabel.domain.member.repository.MemberRepository; import com.worlabel.global.filter.JwtAuthenticationFilter;
import com.worlabel.global.filter.JWTFilter; import com.worlabel.global.handler.CustomAuthenticationDeniedHandler;
import com.worlabel.global.handler.CustomAuthenticationEntryPoint;
import com.worlabel.global.handler.OAuth2SuccessHandler; import com.worlabel.global.handler.OAuth2SuccessHandler;
import com.worlabel.global.util.JWTUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -18,73 +18,81 @@ import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Collections; import java.util.List;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@RequiredArgsConstructor @RequiredArgsConstructor
public class SecurityConfig { public class SecurityConfig {
private final CustomAuthenticationDeniedHandler authenticationDeniedHandler;
private final CustomOAuth2MemberService customOAuth2UserService; private final CustomAuthenticationEntryPoint authenticationEntryPoint;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomOAuth2UserService customOAuth2UserService;
private final OAuth2SuccessHandler oAuth2SuccessHandler; private final OAuth2SuccessHandler oAuth2SuccessHandler;
private final JWTUtil jwtUtil;
@Value("${frontend.url}") @Value("${frontend.url}")
private String frontend; private String frontend;
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, MemberRepository memberRepository) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// HTTP Basic 인증 방식 비활성화
http.
httpBasic((auth) -> auth.disable());
// CSRF 비활성화 // CSRF 비활성화
http http
.csrf((auth) -> auth.disable()); .csrf((auth) -> auth.disable());
// Form 로그인 방식 비활성화
// Form 로그인 방식 Disable
http http
.formLogin((auth) -> auth.disable()); .formLogin((auth) -> auth.disable());
// HTTP Basic 인증 방식 disable // 세션 설정 비활성화
http. http.sessionManagement((session)->session
httpBasic((auth) -> auth.disable()); .sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// OAuth2 // CORS 설정
http http
.oauth2Login(oauth2 -> oauth2 .cors(configurer -> configurer.configurationSource(corsConfigurationSource()));
.authorizationEndpoint(authorizationEndpoint ->
authorizationEndpoint.baseUri("/api/oauth2/authorization")) http
.redirectionEndpoint(redirectionEndpoint -> .exceptionHandling(configurer -> configurer
redirectionEndpoint.baseUri("/api/login/oauth2/code/*")) .authenticationEntryPoint(authenticationEntryPoint)
.userInfoEndpoint(userInfoEndpointConfig -> .accessDeniedHandler(authenticationDeniedHandler)
userInfoEndpointConfig.userService(customOAuth2UserService))
.successHandler(oAuth2SuccessHandler)
); );
// 경로별 인가 작업 // 경로별 인가 작업
http http
.authorizeHttpRequests((auth)->auth .authorizeHttpRequests(auth->auth
.requestMatchers("/favicon.ico").permitAll() // Allow access to favicon.ico .requestMatchers("/api/**").authenticated()
.requestMatchers("/").permitAll()
.anyRequest().authenticated()); .anyRequest().authenticated());
// 세션 설정: STATELESS // OAuth2
http.sessionManagement((session)->session http
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); .oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authorization -> authorization.baseUri("/api/login/oauth2/authorization"))
.redirectionEndpoint(redirection -> redirection .baseUri("/api/login/oauth2/code/*"))
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
.successHandler(oAuth2SuccessHandler)
);
// JWT 필터 추가 // JWT 필터 추가
http http
.addFilterAfter(new JWTFilter(jwtUtil, memberRepository), UsernamePasswordAuthenticationFilter.class); .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build(); return http.build();
} }
@Bean @Bean
public CorsConfigurationSource corsConfigurationSource() { public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration(); CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Collections.singletonList(frontend)); // 프론트엔드 URL 사용
configuration.setAllowedMethods(Collections.singletonList("*"));
configuration.setAllowCredentials(true); configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Collections.singletonList("*")); configuration.setAllowedOrigins(List.of(frontend)); // 프론트엔드 URL 사용
configuration.setAllowedMethods(List.of("GET","POST","PUT","PATCH","DELETE","OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setMaxAge(3600L); configuration.setMaxAge(3600L);
configuration.addExposedHeader("Authorization"); configuration.addExposedHeader("Authorization");
@ -93,6 +101,4 @@ public class SecurityConfig {
source.registerCorsConfiguration("/**", configuration); source.registerCorsConfiguration("/**", configuration);
return source; return source;
} }
} }

View File

@ -22,8 +22,9 @@ public enum ErrorCode {
REFRESH_TOKEN_EXPIRED(HttpStatus.BAD_REQUEST, 2003, "리프레시 토큰이 만료되었습니다."), REFRESH_TOKEN_EXPIRED(HttpStatus.BAD_REQUEST, 2003, "리프레시 토큰이 만료되었습니다."),
INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, 2004, "유효하지 않은 리프레시 토큰입니다."), INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, 2004, "유효하지 않은 리프레시 토큰입니다."),
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, 2005, "인증에 실패하였습니다."), UNAUTHORIZED(HttpStatus.UNAUTHORIZED, 2005, "인증에 실패하였습니다."),
Access_DENIED(HttpStatus.FORBIDDEN, 2006, "접근 권한이 없습니다."),
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, 2000, "올바르지 않는 인증 토큰입니다. 다시 확인 해주세요");
;
private final HttpStatus status; private final HttpStatus status;
private final int code; private final int code;

View File

@ -1,53 +0,0 @@
package com.worlabel.global.filter;
import com.worlabel.domain.auth.entity.CustomOAuth2Member;
import com.worlabel.domain.member.repository.MemberRepository;
import com.worlabel.global.util.JWTUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Arrays;
import java.util.Optional;
@Slf4j
@RequiredArgsConstructor
public class JWTFilter extends OncePerRequestFilter {
private final JWTUtil jwtUtil;
private final MemberRepository memberRepository;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Cookie[] cookies = request.getCookies();
String token = Arrays.stream(Optional.ofNullable(cookies).orElse(new Cookie[0]))
.filter(cookie -> "Authorization".equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
if(token == null || jwtUtil.isExpired(token)){
log.debug("토큰 X");
filterChain.doFilter(request, response);
return;
}
String username = jwtUtil.getUsername(token);
memberRepository.findByEmail(username).ifPresent(member -> {
CustomOAuth2Member customOAuth2Member = new CustomOAuth2Member(member);
Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2Member.getAuthMemberDto(), null, customOAuth2Member.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
});
filterChain.doFilter(request, response);
}
}

View File

@ -0,0 +1,63 @@
package com.worlabel.global.filter;
import com.worlabel.domain.auth.entity.CustomOAuth2User;
import com.worlabel.domain.member.repository.MemberRepository;
import com.worlabel.global.util.JWTUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Arrays;
import java.util.Optional;
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JWTUtil jwtUtil;
private final MemberRepository memberRepository;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = getToken(request);
try {
if (StringUtils.hasText(token) && !jwtUtil.isExpired(token)) {
String username = jwtUtil.getUsername(token);
memberRepository.findByEmail(username).ifPresent(member -> {
CustomOAuth2User customOAuth2User = new CustomOAuth2User(member);
Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2User.getAuthMember(), null, customOAuth2User.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
});
} else {
throw new JwtException("유효한 JWT 토큰이 없습니다.");
}
} catch (Exception e) {
log.debug("message: {}",e.getMessage());
SecurityContextHolder.clearContext();
request.setAttribute("error-message", e.getMessage());
}
filterChain.doFilter(request, response);
}
private static String getToken(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
return Arrays.stream(Optional.ofNullable(cookies).orElse(new Cookie[0]))
.filter(cookie -> "Authorization".equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
}
}

View File

@ -0,0 +1,29 @@
package com.worlabel.global.handler;
import com.worlabel.global.exception.CustomException;
import com.worlabel.global.exception.ErrorCode;
import com.worlabel.global.response.ErrorResponse;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j
@Component
public class CustomAuthenticationDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.debug("오류 : {}", request.getAttribute("error-message"));
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
CustomException exception = new CustomException(ErrorCode.Access_DENIED);
ErrorResponse errorResponse = new ErrorResponse(exception, request.getAttribute("error-message").toString());
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(errorResponse.toJson());
}
}

View File

@ -0,0 +1,32 @@
package com.worlabel.global.handler;
import com.worlabel.global.exception.CustomException;
import com.worlabel.global.exception.ErrorCode;
import com.worlabel.global.response.ErrorResponse;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.debug("인증 실패 오류 : {} ", request.getAttribute("error-message"));
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
CustomException exception = new CustomException(ErrorCode.INVALID_TOKEN);
if (request.getAttribute("error-message") == null) {
request.setAttribute("error-message", exception.getMessage());
}
ErrorResponse errorResponse = new ErrorResponse(exception, request.getAttribute("error-message").toString());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(errorResponse.toJson());
}
}

View File

@ -1,6 +1,8 @@
package com.worlabel.global.handler; package com.worlabel.global.handler;
import com.worlabel.domain.auth.entity.CustomOAuth2Member; import com.worlabel.domain.auth.entity.CustomOAuth2User;
import com.worlabel.domain.auth.entity.dto.JwtToken;
import com.worlabel.domain.auth.service.JwtTokenService;
import com.worlabel.global.util.JWTUtil; import com.worlabel.global.util.JWTUtil;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie; import jakarta.servlet.http.Cookie;
@ -10,55 +12,50 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.io.IOException; import java.io.IOException;
import java.util.stream.Collectors;
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
// TODO : 추후 도메인 혹은 배포시 설정
@Value("${frontend.url}") @Value("${frontend.url}")
private String frontEnd; private String frontEnd;
private final JWTUtil jwtUtil; private final JwtTokenService jwtTokenService;
@Override @Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// OAuth2User // OAuth2User
CustomOAuth2Member customOAuth2Member = (CustomOAuth2Member) authentication.getPrincipal(); CustomOAuth2User customOAuth2User = (CustomOAuth2User) authentication.getPrincipal();
log.debug("로그인 성공 : {}", customOAuth2Member); log.debug("로그인 성공 : {}", customOAuth2User);
String username = customOAuth2Member.getName();
String role = authentication.getAuthorities()
.stream()
.map(auth -> auth.getAuthority())
.toList()
.get(0);
// JWT 토큰 생성 // JWT 토큰 생성
String token = jwtUtil.createJwtToken(username, role, 60 * 60 * 1000L, "access"); JwtToken jwtToken = jwtTokenService.generateToken(customOAuth2User.getId());
// 쿠키에 JWT 토큰 추가
response.addCookie(createCookie("Authorization", token)); // 헤더에 액세스 토큰 추가
response.setHeader("Authorization", jwtToken.getAccessToken());
// 쿠키에 리프레시 토큰 추가
response.addCookie(createCookie(jwtToken.getRefreshToken()));
// 성공 리다이렉트할 URL 설정 // 성공 리다이렉트할 URL 설정
// String redirectUrl = frontEnd + "/"; // 적절한 리다이렉트 URL로 수정 String redirectUrl = frontEnd + "/"; // TODO: 적절한 리다이렉트 URL로 수정
// getRedirectStrategy().sendRedirect(request, response, redirectUrl); getRedirectStrategy().sendRedirect(request, response, redirectUrl);
super.onAuthenticationSuccess(request, response, authentication);
} }
private Cookie createCookie(String key, String value) { private Cookie createCookie(String value) {
Cookie cookie = new Cookie(key, value); Cookie cookie = new Cookie("refreshToken", value);
// TODO: 추후 변경 int refreshTokenExpiryInSeconds = (int) ((System.currentTimeMillis() + jwtTokenService.getRefreshTokenExpiration()) / 1000); // 리프레시 토큰 만료 시간과 일치
cookie.setMaxAge(60 * 60 * 60); // 개발 단계에서는 유효기간 길게 cookie.setMaxAge(refreshTokenExpiryInSeconds);
cookie.setPath("/"); // 쿠키 경로를 전체 경로로 설정 cookie.setPath("/");
cookie.setHttpOnly(true); // HttpOnly 설정, JavaScript 접근 불가 cookie.setHttpOnly(true);
// cookie.setSecure(true); // TODO: 배포시 HTTPS 환경에서 사용 // cookie.setSecure(true); // 배포 HTTPS에서 사용
return cookie; return cookie;
} }
} }

View File

@ -1,7 +1,6 @@
package com.worlabel.global.resolver; package com.worlabel.global.resolver;
import com.worlabel.domain.auth.dto.AuthMemberDto; import com.worlabel.domain.auth.entity.dto.AuthMemberDto;
import com.worlabel.domain.auth.entity.CustomOAuth2Member;
import com.worlabel.global.annotation.CurrentUser; import com.worlabel.global.annotation.CurrentUser;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
@ -24,9 +23,8 @@ public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolve
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(principal instanceof AuthMemberDto){ if(principal instanceof AuthMemberDto){
return principal; return ((AuthMemberDto) principal).getId();
} }
return null; return null;
} }
} }