Merge branch 'be/feat/16-jwttoken' into 'be/develop'
Feat: JWT 토큰 추가 - S11P21S002-16 See merge request s11-s-project/S11P21S002!14
This commit is contained in:
commit
8d8de6e396
@ -46,7 +46,7 @@ dependencies {
|
|||||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||||
|
|
||||||
// GJson
|
// GJson
|
||||||
implementation 'com.google.code.gson:gson:2.7'
|
implementation 'com.google.code.gson:gson:2.8.9'
|
||||||
|
|
||||||
// OAuth
|
// OAuth
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
|
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
|
||||||
@ -56,6 +56,9 @@ dependencies {
|
|||||||
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
|
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
|
||||||
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
|
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
|
||||||
|
|
||||||
|
// Redis
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||||
|
|
||||||
//Swagger
|
//Swagger
|
||||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
|
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
|
||||||
|
|
||||||
|
@ -19,4 +19,5 @@ public abstract class OAuth2Attribute {
|
|||||||
public abstract String getEmail();
|
public abstract String getEmail();
|
||||||
|
|
||||||
public abstract String getProfileImage();
|
public abstract String getProfileImage();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,85 @@
|
|||||||
package com.worlabel.domain.auth.controller;
|
package com.worlabel.domain.auth.controller;
|
||||||
|
|
||||||
import com.worlabel.domain.auth.dto.AuthMemberDto;
|
import com.worlabel.domain.auth.entity.dto.JwtToken;
|
||||||
|
import com.worlabel.domain.auth.repository.AuthCacheRepository;
|
||||||
|
import com.worlabel.domain.auth.service.AuthService;
|
||||||
|
import com.worlabel.domain.auth.service.JwtTokenService;
|
||||||
import com.worlabel.global.annotation.CurrentUser;
|
import com.worlabel.global.annotation.CurrentUser;
|
||||||
|
import com.worlabel.global.exception.CustomException;
|
||||||
|
import com.worlabel.global.exception.ErrorCode;
|
||||||
import com.worlabel.global.response.SuccessResponse;
|
import com.worlabel.global.response.SuccessResponse;
|
||||||
|
import jakarta.servlet.http.Cookie;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@RequestMapping("/api/auth")
|
@RequestMapping("/api/auth")
|
||||||
public class AuthController {
|
public class AuthController {
|
||||||
|
|
||||||
// TODO: 리이슈 처리
|
private final AuthService authService;
|
||||||
|
private final AuthCacheRepository authCacheRepository;
|
||||||
|
private final JwtTokenService jwtTokenService;
|
||||||
|
|
||||||
|
@Value("${auth.refreshTokenExpiry}")
|
||||||
|
long refreshExpiry;
|
||||||
|
|
||||||
|
// TODO: 리이슈 처리, 액세스 어떻게 받았는지 물어보기
|
||||||
|
@PostMapping("/reissue")
|
||||||
|
public SuccessResponse<String> reissue(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response
|
||||||
|
) {
|
||||||
|
String refresh = parseRefreshCookie(request);
|
||||||
|
log.info("reissue :{}", refresh);
|
||||||
|
try {
|
||||||
|
JwtToken newToken = authService.reissue(refresh);
|
||||||
|
log.debug("새로운 토큰 발급 성공");
|
||||||
|
int id = jwtTokenService.parseId(newToken.getAccessToken());
|
||||||
|
log.debug("{}",id);
|
||||||
|
response.addCookie(createCookie(newToken.getRefreshToken()));
|
||||||
|
authCacheRepository.save(id, newToken.getRefreshToken(), refreshExpiry);
|
||||||
|
return SuccessResponse.of(newToken.getAccessToken());
|
||||||
|
} catch (CustomException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new CustomException(ErrorCode.INVALID_TOKEN, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cookie createCookie(String value) {
|
||||||
|
Cookie cookie = new Cookie("refreshToken", value);
|
||||||
|
cookie.setMaxAge((int) (refreshExpiry / 1000));
|
||||||
|
cookie.setPath("/");
|
||||||
|
cookie.setHttpOnly(true);
|
||||||
|
// cookie.setSecure(true); // 배포 시 HTTPS에서 사용
|
||||||
|
return cookie;
|
||||||
|
}
|
||||||
|
|
||||||
@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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String parseRefreshCookie(HttpServletRequest request) {
|
||||||
|
Cookie[] cookies = request.getCookies();
|
||||||
|
if(cookies != null) {
|
||||||
|
return Arrays.stream(cookies)
|
||||||
|
.filter(cookie -> "refreshToken".equals(cookie.getName()))
|
||||||
|
.findFirst()
|
||||||
|
.map(Cookie::getValue)
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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;
|
@ -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;
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package com.worlabel.domain.auth.entity.dto;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class LoginMember {
|
||||||
|
private int id;
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
public static LoginMember of(int id, String email) {
|
||||||
|
LoginMember member = new LoginMember();
|
||||||
|
member.id = id;
|
||||||
|
member.email = email;
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package com.worlabel.domain.auth.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());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package com.worlabel.domain.auth.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());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package com.worlabel.domain.auth.handler;
|
||||||
|
|
||||||
|
import com.worlabel.domain.auth.entity.CustomOAuth2User;
|
||||||
|
import com.worlabel.domain.auth.entity.dto.JwtToken;
|
||||||
|
import com.worlabel.domain.auth.repository.AuthCacheRepository;
|
||||||
|
import com.worlabel.domain.auth.service.JwtTokenService;
|
||||||
|
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.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
|
||||||
|
|
||||||
|
@Value("${frontend.url}") private String frontEnd;
|
||||||
|
@Value("${auth.refreshTokenExpiry}") long refreshExpiry;
|
||||||
|
private final AuthCacheRepository authCacheRepository;
|
||||||
|
private final JwtTokenService jwtTokenService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
|
||||||
|
// OAuth2User
|
||||||
|
CustomOAuth2User customOAuth2User = (CustomOAuth2User) authentication.getPrincipal();
|
||||||
|
log.debug("로그인 성공 : {}", customOAuth2User);
|
||||||
|
|
||||||
|
// JWT 토큰 생성
|
||||||
|
JwtToken jwtToken = jwtTokenService.generateTokenByOAuth2User(customOAuth2User);
|
||||||
|
|
||||||
|
// 쿼리 파라미터로 액세스 토큰 전달
|
||||||
|
String redirectUrl = UriComponentsBuilder.fromUriString(frontEnd + "/redirect/oauth2")
|
||||||
|
.queryParam("accessToken", jwtToken.getAccessToken())
|
||||||
|
.toUriString();
|
||||||
|
|
||||||
|
// 쿠키에 리프레시 토큰 추가
|
||||||
|
response.addCookie(createCookie(jwtToken.getRefreshToken()));
|
||||||
|
authCacheRepository.save(customOAuth2User.getId(), jwtToken.getRefreshToken(), refreshExpiry);
|
||||||
|
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, redirectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cookie createCookie(String value) {
|
||||||
|
Cookie cookie = new Cookie("refreshToken", value);
|
||||||
|
cookie.setMaxAge((int) (refreshExpiry / 1000));
|
||||||
|
cookie.setPath("/");
|
||||||
|
cookie.setHttpOnly(true);
|
||||||
|
// cookie.setSecure(true); // 배포 시 HTTPS에서 사용
|
||||||
|
return cookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package com.worlabel.domain.auth.repository;
|
||||||
|
|
||||||
|
import com.worlabel.global.cache.CacheKey;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Repository
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AuthCacheRepository {
|
||||||
|
|
||||||
|
private final RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
public void save(int memberId, String token, Long expiredTime) {
|
||||||
|
redisTemplate.opsForValue().set(CacheKey.authenticationKey(memberId), String.valueOf(token), expiredTime, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String find(int memberId) {
|
||||||
|
return (String) redisTemplate.opsForValue().get(CacheKey.authenticationKey(memberId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(int memberId){
|
||||||
|
redisTemplate.delete(CacheKey.authenticationKey(memberId));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package com.worlabel.domain.auth.service;
|
||||||
|
|
||||||
|
import com.worlabel.domain.auth.entity.dto.JwtToken;
|
||||||
|
import com.worlabel.domain.auth.repository.AuthCacheRepository;
|
||||||
|
import com.worlabel.global.exception.CustomException;
|
||||||
|
import com.worlabel.global.exception.ErrorCode;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AuthService {
|
||||||
|
private final JwtTokenService jwtTokenService;
|
||||||
|
private final AuthCacheRepository authCacheRepository;
|
||||||
|
|
||||||
|
public JwtToken reissue(String refreshToken) throws Exception {
|
||||||
|
int id = jwtTokenService.parseId(refreshToken);
|
||||||
|
Object redisRefreshToken = authCacheRepository.find(id);
|
||||||
|
log.debug("{} == {} ",redisRefreshToken,refreshToken);
|
||||||
|
if(!Objects.equals(refreshToken, redisRefreshToken)){
|
||||||
|
throw new CustomException(ErrorCode.USER_ALREADY_SIGN_OUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jwtTokenService.generateTokenByRefreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,9 +1,154 @@
|
|||||||
package com.worlabel.domain.auth.service;
|
package com.worlabel.domain.auth.service;
|
||||||
|
|
||||||
|
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 lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@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 generateTokenByOAuth2User(CustomOAuth2User user) {
|
||||||
|
List<String> authorities = user.getAuthorities().stream()
|
||||||
|
.map(GrantedAuthority::getAuthority)
|
||||||
|
.toList();
|
||||||
|
return generateToken(user.getName(), user.getId(), authorities);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JwtToken generateTokenByRefreshToken(String refreshToken) throws Exception{
|
||||||
|
if (!isTokenExpired(refreshToken) && isRefreshToken(refreshToken)) {
|
||||||
|
Claims claims = parseClaims(refreshToken);
|
||||||
|
String username = claims.getSubject();
|
||||||
|
int memberId = claims.get("id", Integer.class);
|
||||||
|
List<String> authorities = parseAuthorities(refreshToken).stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
|
||||||
|
return generateToken(username, memberId, authorities);
|
||||||
|
}
|
||||||
|
throw new Exception("유효하지 않은 토큰입니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private JwtToken generateToken(String username, int memberId, List<String> authorities){
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
Date accessTokenExpire = new Date(now + tokenExpiration);
|
||||||
|
Date refreshTokenExpire = new Date(now + refreshTokenExpiration);
|
||||||
|
|
||||||
|
String accessToken = Jwts.builder()
|
||||||
|
.subject(username)
|
||||||
|
.claim("type", "access")
|
||||||
|
.claim("id", memberId)
|
||||||
|
.claim("authorities", authorities) // 권한 정보 추가
|
||||||
|
.expiration(accessTokenExpire)
|
||||||
|
.signWith(secretKey)
|
||||||
|
.compact();
|
||||||
|
|
||||||
|
String refreshToken = Jwts.builder()
|
||||||
|
.subject(username)
|
||||||
|
.claim("type","refresh")
|
||||||
|
.claim("id", memberId)
|
||||||
|
.claim("authorities", authorities) // 권한 정보 추가
|
||||||
|
.expiration(refreshTokenExpire)
|
||||||
|
.signWith(secretKey)
|
||||||
|
.compact();
|
||||||
|
|
||||||
|
log.debug("액세스 발급: {}",accessToken);
|
||||||
|
return new JwtToken(accessToken, refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String parseUsername(String token) throws Exception {
|
||||||
|
return parseClaims(token).getSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int parseId(String token) throws Exception {
|
||||||
|
return parseClaims(token).get("id", Integer.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SimpleGrantedAuthority> parseAuthorities(String token) throws Exception {
|
||||||
|
Claims claims = parseClaims(token);
|
||||||
|
List<String> authorities = claims.get("authorities", List.class); // JWT에서 권한 정보 추출
|
||||||
|
return authorities.stream()
|
||||||
|
.map(SimpleGrantedAuthority::new)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 토큰 만료 여부 확인
|
||||||
|
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);
|
||||||
|
log.debug("claims : {}",claims);
|
||||||
|
String tokenType = claims.get("type", String.class);
|
||||||
|
return expectedType.equals(tokenType);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
5
backend/src/main/java/com/worlabel/global/cache/CacheKey.java
vendored
Normal file
5
backend/src/main/java/com/worlabel/global/cache/CacheKey.java
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package com.worlabel.global.cache;
|
||||||
|
|
||||||
|
public class CacheKey {
|
||||||
|
public static String authenticationKey(int memberId) { return "authentication:" + memberId;}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package com.worlabel.global.config;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
|
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||||
|
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class RedisConfig {
|
||||||
|
|
||||||
|
@Value("${spring.data.redis.host}")
|
||||||
|
private String host;
|
||||||
|
|
||||||
|
@Value("${spring.data.redis.port}")
|
||||||
|
private int port;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RedisConnectionFactory redisConnectionFactory() {
|
||||||
|
return new LettuceConnectionFactory(host, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedisTemplate 정의 및 시리얼라이저 설정
|
||||||
|
@Bean
|
||||||
|
public RedisTemplate<String, Object> redisTemplate() {
|
||||||
|
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
|
||||||
|
redisTemplate.setConnectionFactory(redisConnectionFactory());
|
||||||
|
|
||||||
|
// 일반적인 key:value 경우 시리얼라이저
|
||||||
|
redisTemplate.setKeySerializer(new StringRedisSerializer());
|
||||||
|
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
|
||||||
|
|
||||||
|
// Hash 사용시 시리얼라이저
|
||||||
|
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
|
||||||
|
redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
|
||||||
|
|
||||||
|
// Default
|
||||||
|
redisTemplate.setDefaultSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
|
||||||
|
|
||||||
|
return redisTemplate;
|
||||||
|
}
|
||||||
|
}
|
@ -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.domain.auth.handler.CustomAuthenticationDeniedHandler;
|
||||||
import com.worlabel.global.handler.OAuth2SuccessHandler;
|
import com.worlabel.domain.auth.handler.CustomAuthenticationEntryPoint;
|
||||||
import com.worlabel.global.util.JWTUtil;
|
import com.worlabel.domain.auth.handler.OAuth2SuccessHandler;
|
||||||
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,9 @@ public class SecurityConfig {
|
|||||||
source.registerCorsConfiguration("/**", configuration);
|
source.registerCorsConfiguration("/**", configuration);
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ5czAxMDcxODUxNjUxQGdtYWlsLmNvbSIsInR5cGUiOiJhY2Nlc3MiLCJhdXRob3JpdGllcyI6W3siYXV0aG9yaXR5IjoiVVNFUiJ9XSwiZXhwIjoxNzI0ODU1NzQ2fQ.tIo9e40nY1KjhBwYcw0BG18Q9qeTYAoXefezYM9YQiY
|
||||||
|
|
||||||
|
*/
|
@ -22,6 +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, 2007, "올바르지 않는 인증 토큰입니다. 다시 확인 해주세요"),
|
||||||
|
USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED, 2008,"이미 로그아웃한 사용자입니다."),
|
||||||
|
|
||||||
// Workspace - 3000
|
// Workspace - 3000
|
||||||
NOT_AUTHOR(HttpStatus.FORBIDDEN, 3001, "작성자가 아닙니다. 이 작업을 수행할 권한이 없습니다."),
|
NOT_AUTHOR(HttpStatus.FORBIDDEN, 3001, "작성자가 아닙니다. 이 작업을 수행할 권한이 없습니다."),
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,61 @@
|
|||||||
|
package com.worlabel.global.filter;
|
||||||
|
|
||||||
|
import com.worlabel.domain.auth.entity.dto.LoginMember;
|
||||||
|
import com.worlabel.domain.auth.service.JwtTokenService;
|
||||||
|
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;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
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.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
public final String AUTHORIZATION_HEADER = "Authorization";
|
||||||
|
private final JwtTokenService jwtTokenService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
String token = resolveToken(request);
|
||||||
|
log.debug("token {}", token);
|
||||||
|
try {
|
||||||
|
if (StringUtils.hasText(token) && !jwtTokenService.isTokenExpired(token) && jwtTokenService.isAccessToken(token)) {
|
||||||
|
String name = jwtTokenService.parseUsername(token);
|
||||||
|
int id = jwtTokenService.parseId(token);
|
||||||
|
|
||||||
|
List<SimpleGrantedAuthority> authorities = jwtTokenService.parseAuthorities(token);
|
||||||
|
Authentication authToken = new UsernamePasswordAuthenticationToken(LoginMember.of(id,name), null, authorities);
|
||||||
|
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 String resolveToken(HttpServletRequest request) {
|
||||||
|
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
|
||||||
|
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
|
||||||
|
return bearerToken.substring(7);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -1,64 +0,0 @@
|
|||||||
package com.worlabel.global.handler;
|
|
||||||
|
|
||||||
import com.worlabel.domain.auth.entity.CustomOAuth2Member;
|
|
||||||
import com.worlabel.global.util.JWTUtil;
|
|
||||||
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.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
|
|
||||||
|
|
||||||
// TODO : 추후 도메인 혹은 배포시 설정
|
|
||||||
@Value("${frontend.url}")
|
|
||||||
private String frontEnd;
|
|
||||||
|
|
||||||
private final JWTUtil jwtUtil;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
|
|
||||||
|
|
||||||
// OAuth2User
|
|
||||||
CustomOAuth2Member customOAuth2Member = (CustomOAuth2Member) authentication.getPrincipal();
|
|
||||||
log.debug("로그인 성공 : {}", customOAuth2Member);
|
|
||||||
|
|
||||||
String username = customOAuth2Member.getName();
|
|
||||||
String role = authentication.getAuthorities()
|
|
||||||
.stream()
|
|
||||||
.map(auth -> auth.getAuthority())
|
|
||||||
.toList()
|
|
||||||
.get(0);
|
|
||||||
|
|
||||||
// JWT 토큰 생성
|
|
||||||
String token = jwtUtil.createJwtToken(username, role, 60 * 60 * 1000L, "access");
|
|
||||||
// 쿠키에 JWT 토큰 추가
|
|
||||||
response.addCookie(createCookie("Authorization", token));
|
|
||||||
|
|
||||||
// 성공 시 리다이렉트할 URL 설정
|
|
||||||
// String redirectUrl = frontEnd + "/"; // 적절한 리다이렉트 URL로 수정
|
|
||||||
// getRedirectStrategy().sendRedirect(request, response, redirectUrl);
|
|
||||||
super.onAuthenticationSuccess(request, response, authentication);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Cookie createCookie(String key, String value) {
|
|
||||||
Cookie cookie = new Cookie(key, value);
|
|
||||||
// TODO: 추후 변경
|
|
||||||
cookie.setMaxAge(60 * 60 * 60); // 개발 단계에서는 유효기간 길게
|
|
||||||
cookie.setPath("/"); // 쿠키 경로를 전체 경로로 설정
|
|
||||||
cookie.setHttpOnly(true); // HttpOnly 설정, JavaScript 접근 불가
|
|
||||||
// cookie.setSecure(true); // TODO: 배포시 HTTPS 환경에서 사용
|
|
||||||
return cookie;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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.LoginMember;
|
||||||
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;
|
||||||
@ -23,10 +22,9 @@ public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolve
|
|||||||
@Override
|
@Override
|
||||||
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 LoginMember){
|
||||||
return principal;
|
return ((LoginMember) principal).getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
package com.worlabel.global.util;
|
|
||||||
|
|
||||||
import io.jsonwebtoken.Jwts;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class JWTUtil {
|
|
||||||
|
|
||||||
private final SecretKey secretKey;
|
|
||||||
|
|
||||||
public JWTUtil(@Value("${spring.jwt.secret}") String key) {
|
|
||||||
secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUsername(String token) {
|
|
||||||
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getRole(String token) {
|
|
||||||
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean isExpired(String token) {
|
|
||||||
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AccessToken 생성
|
|
||||||
* @param username 사용자 아이디(이메일)
|
|
||||||
* @param role 사용자 권한
|
|
||||||
* @param expiredMs 토큰 만료시간
|
|
||||||
* @return AccessToken
|
|
||||||
*/
|
|
||||||
public String createJwtToken(String username, String role, Long expiredMs, String type) {
|
|
||||||
return Jwts.builder()
|
|
||||||
.claim("username", username)
|
|
||||||
.claim("role", role)
|
|
||||||
.claim("type", type)
|
|
||||||
.expiration(new Date(System.currentTimeMillis() + expiredMs))
|
|
||||||
.signWith(secretKey)
|
|
||||||
.compact();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user