Skip to content

Commit

Permalink
Merge pull request #49 from Kakaotech-18-Ecommerce/SCRUM-69-SocicalLo…
Browse files Browse the repository at this point in the history
…gout

[Feat] Scrum 69 socical logout
  • Loading branch information
yeopyeop-82 authored Aug 29, 2024
2 parents 4619e4a + d0e63a7 commit e120e5d
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 28 deletions.
30 changes: 18 additions & 12 deletions src/main/java/com/kakaoteck/golagola/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
package com.kakaoteck.golagola.config;

import com.kakaoteck.golagola.security.filter.JwtAuthenticationFilter;
import com.kakaoteck.golagola.security.handler.signout.CustomSignOutProcessHandler;
import com.kakaoteck.golagola.security.jwt.JWTFilter;
import com.kakaoteck.golagola.security.jwt.JWTUtil;
import com.kakaoteck.golagola.security.oauth2.CustomSuccessHandler;
import com.kakaoteck.golagola.security.handler.signin.CustomSuccessHandler;
import com.kakaoteck.golagola.service.CustomOAuth2UserService;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
Expand All @@ -36,9 +37,7 @@ public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
private final CustomSuccessHandler customSuccessHandler;
private final JWTUtil jwtUtil;
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
private final LogoutHandler logoutHandler;
private final CustomSignOutProcessHandler customSignOutProcessHandler;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
Expand Down Expand Up @@ -68,11 +67,21 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {

// OAuth2 로그인 설정
http.oauth2Login(oauth2 -> oauth2.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
.successHandler(customSuccessHandler));
.successHandler(customSuccessHandler)
// .failureHandler(oAuth2LoginFailureHandler) # 실패핸들러 추가하기
);

// 로그아웃 설정
http.logout(logout -> logout.logoutUrl("/api/v1/auth/logout")
//.addLogoutHandler(logoutHandler) // 지미꺼
.addLogoutHandler(customSignOutProcessHandler) // 코이꺼
.deleteCookies("JSESSIONID", "Authorization", "RefreshToken")
);

// JWT 필터 설정
// http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterAfter(new JWTFilter(jwtUtil), OAuth2LoginAuthenticationFilter.class);
http.addFilterBefore(new JWTFilter(jwtUtil), LogoutFilter.class); // 로그아웃 필터전에 jwt필터실행
http.addFilterBefore(new JWTFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
// http.addFilterAfter(new JWTFilter(jwtUtil), OAuth2LoginAuthenticationFilter.class);

// 경로별 인가 작업
http.authorizeHttpRequests(auth -> auth
Expand All @@ -82,14 +91,11 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
// 세션 설정: STATELESS
http.sessionManagement(session -> session.sessionCreationPolicy(STATELESS));

// 로그아웃 설정
http.logout(logout -> logout.logoutUrl("/api/v1/auth/logout").addLogoutHandler(logoutHandler));

return http.build();
}

private static final String[] WHITE_LIST_URL = {
"/api/v1/auth/**",
// "/api/v1/auth/**",
"/v2/api-docs",
"/v3/api-docs",
"/v3/api-docs/**",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
package com.kakaoteck.golagola.domain.auth.Repository;

import com.kakaoteck.golagola.domain.auth.entity.UserEntity;
import io.lettuce.core.dynamic.annotation.Param;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;

import java.util.Optional;

public interface UserRepository extends JpaRepository<UserEntity, Long> {
UserEntity findByUsername(String username); // username을 전달하여 해당하는 엔티티 가져오기(JPA)
// UserEntity findByUsername(String username); // username을 전달하여 해당하는 엔티티 가져오기(JPA)

Optional<UserEntity> findByUsername(String username); // 차이가 뭔지 공부하기

@Modifying
@Query("UPDATE UserEntity u SET u.refreshToken = :refreshToken, u.loginStatus = :loginStatus WHERE u.username = :username")
void updateRefreshTokenAndLoginStatus(@Param("userName") String username,
@Param("refreshToken") String refreshToken,
@Param("loginStatus") boolean loginStatus);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,8 @@ public String getUsername() {
return userDTO.getUsername();
}

public Long getId() { // id 값을 반환하는 메서드 추가
return userDTO.getId();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
@Setter
public class UserDTO {

private Long id; // 엔티티의 id 추가
private String role;
private String name;
private String username;
private String email; // 엔티티의 email 추가
private String refreshToken; // 엔티티의 refreshToken 추가
private boolean loginStatus; // 엔티티의 loginStatus 추가
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public class UserEntity {
private String email;
private String role;

// 추가
private String refreshToken; // JWT 리프레시 토큰 발급
private boolean loginStatus; // 로그인 상태처리



}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.kakaoteck.golagola.security.oauth2;
package com.kakaoteck.golagola.security.handler.signin;

import com.kakaoteck.golagola.domain.auth.Repository.UserRepository;
import com.kakaoteck.golagola.domain.auth.dto.CustomOAuth2User;
import com.kakaoteck.golagola.domain.auth.entity.UserEntity;
import com.kakaoteck.golagola.security.jwt.JWTUtil;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
Expand All @@ -14,31 +16,50 @@
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.Optional;

@Component
public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

private final JWTUtil jwtUtil;
public CustomSuccessHandler(JWTUtil jwtUtil) {
private final UserRepository userRepository;

// AutoWired로 대체 가능한가요?
public CustomSuccessHandler(JWTUtil jwtUtil, UserRepository userRepository) {
this.jwtUtil = jwtUtil;
this.userRepository = userRepository;
}

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

//OAuth2User
CustomOAuth2User customUserDetails = (CustomOAuth2User) authentication.getPrincipal();

String username = customUserDetails.getUsername();

Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority auth = iterator.next();
String role = auth.getAuthority();

// JWT 생성
String token = jwtUtil.createJwt(username, role, 60*60*60L);

// Refresh Token 생성 및 저장
String refreshToken = jwtUtil.createJwt(username, role, 7*24*60*60L); // 예: 7일간 유효한 리프레시 토큰

// UserEntity 업데이트
Optional<UserEntity> userEntityOptional = userRepository.findByUsername(username);
if (userEntityOptional.isPresent()) {
UserEntity userEntity = userEntityOptional.get();
userEntity.setRefreshToken(refreshToken);
userEntity.setLoginStatus(true); // 로그인 상태를 true로 설정
userRepository.save(userEntity); // 업데이트된 정보를 저장
}

// 쿠키 설정
response.addCookie(createCookie("Authorization", token)); // 쿠키를 넣어준다.
response.addCookie(createCookie("RefreshToken", refreshToken)); // 리프레시 토큰도 쿠키로 추가
response.sendRedirect("http://localhost:8080/"); // 프론트쪽으로 특정 uri로 리다이렉트
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.kakaoteck.golagola.security.handler.signout;


import com.kakaoteck.golagola.domain.auth.Repository.UserRepository;
import com.kakaoteck.golagola.domain.auth.dto.CustomOAuth2User;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
@Slf4j
public class CustomSignOutProcessHandler implements LogoutHandler {
private final UserRepository userRepository;

@Override
@Transactional
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
if (authentication == null) {
return;
}

// 1. 테리코드
// CustomUserDetails userPrincipal = (CustomUserDetails) authentication.getPrincipal();
// userRepository.updateRefreshTokenAndLoginStatus(userPrincipal.getId(), null, false);

// 2. 용우코드
CustomOAuth2User userPrincipal = (CustomOAuth2User) authentication.getPrincipal(); // CustomOAuth2User 사용
System.out.println("로그아웃 정보 확인"+ userPrincipal + userPrincipal.getUsername());
userRepository.updateRefreshTokenAndLoginStatus(userPrincipal.getUsername(), null, false); // UserEntity에서 해당 유저를 찾아서 리프레시 토큰과 로그인 상태를 업데이트

}


}







20 changes: 17 additions & 3 deletions src/main/java/com/kakaoteck/golagola/security/jwt/JWTFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ public JWTFilter(JWTUtil jwtUtil) {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

// jwt 기간 만료시, 무한 재로그인 방지 로직
String requestUri = request.getRequestURI();
if (requestUri.matches("^\\/login(?:\\/.*)?$")) {

filterChain.doFilter(request, response);
return;
}
if (requestUri.matches("^\\/oauth2(?:\\/.*)?$")) {

filterChain.doFilter(request, response);
return;
}

// cookie들을 불러온 뒤 Authorization Key에 담긴 쿠키를 찾음
String authorization = null;
Cookie[] cookies = request.getCookies();
Expand All @@ -38,7 +51,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
}
}
}

// Authorization 헤더 검증
if (authorization == null) {
System.out.println("token null");
Expand All @@ -59,6 +71,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
// 토큰에서 username과 role 획득
String username = jwtUtil.getUsername(token);
String role = jwtUtil.getRole(token);
System.out.println("jwtfilter jwt확인: " + username + role);

// userDTO를 생성하여 값 set
UserDTO userDTO = new UserDTO();
Expand All @@ -68,12 +81,13 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
// UserDetails에 회원 정보 객체 담기
CustomOAuth2User customOAuth2User = new CustomOAuth2User(userDTO);

// 스프링 시큐리티 인증 토큰 생성
// 스프링 시큐리티 인증 토큰 생성, 스프링 시큐리티에서 세션을 생성해가지고 토큰을 등록하고 있음.
Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2User, null, customOAuth2User.getAuthorities());

// 세션에 사용자 등록
SecurityContextHolder.getContext().setAuthentication(authToken);

filterChain.doFilter(request, response);
filterChain.doFilter(request, response); // jwtfilter작업을 다 했기 때문에 다음 필터에게 작업을 넘긴다는 doFilter작업을 진행해주시면 됩니다.
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.Optional;

// DefaultOAuth2UserService: OAuth2에서 기본적으로 유저를 저장하는 메서드를 가지고 있다.
// super로 상속받아서 사용한다.
// OAuth2UserRequest: 리소스 서버에서 제공되는 유저정보
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

private final UserRepository userRepository;

public CustomOAuth2UserService(UserRepository userRepository) {
Expand All @@ -41,15 +42,18 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic

//리소스 서버에서 발급 받은 정보로 사용자를 특정할 아이디값을 만듬
String username = oAuth2Response.getProvider() + " " + oAuth2Response.getProviderId();
UserEntity existData = userRepository.findByUsername(username);
Optional<UserEntity> optionalUserEntity = userRepository.findByUsername(username);

if (existData == null) {
// 1. 새로운 유저라면
if (optionalUserEntity.isEmpty()) {

UserEntity userEntity = new UserEntity();
userEntity.setUsername(username);
userEntity.setEmail(oAuth2Response.getEmail());
userEntity.setName(oAuth2Response.getName());
userEntity.setRole("ROLE_USER");
userEntity.setUsername(username); // ex) kakao 3664463254
userEntity.setEmail(oAuth2Response.getEmail()); // ex) [email protected]
userEntity.setName(oAuth2Response.getName()); // ex) 이용우
userEntity.setRole("ROLE_USER"); // ex) ROLE_USER

// 리프레시 토큰 넣기

userRepository.save(userEntity);

Expand All @@ -59,7 +63,11 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic
userDTO.setRole("ROLE_USER");

return new CustomOAuth2User(userDTO);
} else {
}

// 2. 기존 유러라면
else {
UserEntity existData = optionalUserEntity.get();
existData.setEmail(oAuth2Response.getEmail());
existData.setName(oAuth2Response.getName());

Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ spring.application.name=golagola
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/golagola?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=8253
spring.datasource.password=00000000

spring.jpa.hibernate.ddl-auto=create
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
Expand Down

0 comments on commit e120e5d

Please sign in to comment.