Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feat: KakaoOAuth 기능 추가 #9

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 22 additions & 21 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,6 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

//Querydsl 추가
implementation 'com.querydsl:querydsl-core:5.0.0'
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'

annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

// mysql driver
runtimeOnly 'com.mysql:mysql-connector-j'

Expand All @@ -45,9 +37,18 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

// jwt
implementation 'com.auth0:java-jwt:4.2.1'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// //Querydsl 추가
// implementation 'com.querydsl:querydsl-core:5.0.0'
// implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
//
// annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
// annotationProcessor "jakarta.annotation:jakarta.annotation-api"
// annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

checkstyle {
Expand All @@ -62,16 +63,16 @@ tasks.named('test') {
}


def querydslSrcDir = 'src/main/generated'

sourceSets {
main.java.srcDirs += "$projectDir/build/generated"
}

compileJava {
options.compilerArgs << '-Aquerydsl.generatedAnnotationClass=javax.annotation.Generated'
}

clean {
delete file(querydslSrcDir)
}
//def querydslSrcDir = 'src/main/generated'
//
//sourceSets {
// main.java.srcDirs += "$projectDir/build/generated"
//}
//
//compileJava {
// options.compilerArgs << '-Aquerydsl.generatedAnnotationClass=javax.annotation.Generated'
//}
//
//clean {
// delete file(querydslSrcDir)
//}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.readyvery.readyverydemo.domain;

import java.time.LocalDateTime;

import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import lombok.Setter;

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
@Setter
public class BaseTimeEntity {

@CreatedDate
@Column(name = "created_at")
private LocalDateTime createdAt;

@LastModifiedDate
@Column(name = "last_modified_at")
private LocalDateTime lastModifiedAt;
}
14 changes: 14 additions & 0 deletions src/main/java/com/readyvery/readyverydemo/domain/Role.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.readyvery.readyverydemo.domain;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum Role {

GUEST("ROLE_GUEST"), USER("ROLE_USER");

private final String key;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.readyvery.readyverydemo.domain;

public enum SocialType {
KAKAO, NAVER, GOOGLE
}

54 changes: 53 additions & 1 deletion src/main/java/com/readyvery/readyverydemo/domain/UserInfo.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,56 @@
package com.readyvery.readyverydemo.domain;

public class UserInfo {
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Builder
@Table(name = "USERS")
@AllArgsConstructor
@Slf4j
public class UserInfo extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;

@Column(nullable = false)
private String email; // 이메일

@Column(nullable = false)
private String nickName; // 닉네임


private String imageUrl; // 프로필 이미지
private int age; // 나이

@Enumerated(EnumType.STRING)
private Role role;

@Column(nullable = false)
@Enumerated(EnumType.STRING)
private SocialType socialType; // KAKAO, NAVER, GOOGLE

@Column(nullable = false)
private String socialId; // 로그인한 소셜 타입의 식별자 값 (일반 로그인인 경우 null)

private String refreshToken; // 리프레시 토큰

public void updateRefresh(String updateRefreshToken) {
this.refreshToken = updateRefreshToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.readyvery.readyverydemo.domain.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

import com.readyvery.readyverydemo.domain.SocialType;
import com.readyvery.readyverydemo.domain.UserInfo;

public interface UserRepository extends JpaRepository<UserInfo, Long> {
Optional<UserInfo> findByEmail(String email);

Optional<UserInfo> findByRefreshToken(String refreshToken);

/**
* 소셜 타입과 소셜의 식별값으로 회원 찾는 메소드
* 정보 제공을 동의한 순간 DB에 저장해야하지만, 아직 추가 정보(사는 도시, 나이 등)를 입력받지 않았으므로
* 유저 객체는 DB에 있지만, 추가 정보가 빠진 상태이다.
* 따라서 추가 정보를 입력받아 회원 가입을 진행할 때 소셜 타입, 식별자로 해당 회원을 찾기 위한 메소드
*/
Optional<UserInfo> findBySocialTypeAndSocialId(SocialType socialType, String socialId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,117 @@

import static org.springframework.security.config.Customizer.*;

import java.util.Arrays;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
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.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.readyvery.readyverydemo.domain.repository.UserRepository;
import com.readyvery.readyverydemo.security.exception.CustomAuthenticationEntryPoint;
import com.readyvery.readyverydemo.security.jwt.filter.JwtAuthenticationProcessingFilter;
import com.readyvery.readyverydemo.security.jwt.service.JwtService;
import com.readyvery.readyverydemo.security.oauth2.handler.OAuth2LoginFailureHandler;
import com.readyvery.readyverydemo.security.oauth2.handler.OAuth2LoginSuccessHandler;
import com.readyvery.readyverydemo.security.oauth2.service.CustomOAuth2UserService;

import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class SpringSecurityConfig {

private final JwtService jwtService;
private final UserRepository userRepository;
private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;
private final OAuth2LoginFailureHandler oAuth2LoginFailureHandler;
private final CustomOAuth2UserService customOAuth2UserService;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authz) -> authz
//.requestMatchers("/api/v1/user").authenticated() // 해당 요청은 인증이 필요함
.anyRequest().permitAll() // 위를 제외한 나머지는 모두 허용
http
// [PART 1]
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
//.formLogin(withDefaults()) // 기본 로그인 페이지 사용
.httpBasic(withDefaults());
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)

// [PART 2]
//== URL별 권한 관리 옵션 ==//
.authorizeHttpRequests((authz) -> authz
.requestMatchers(
"/jwt-test",
"/oauth2/**",
"/login"
).permitAll() // 해당 요청은 인증이 필요함
.anyRequest().authenticated() // 위를 제외한 나머지는 모두 허용
)
// [PART 3]
//== 소셜 로그인 설정 ==//
.oauth2Login(oauth2 -> oauth2
.successHandler(oAuth2LoginSuccessHandler) // 동의하고 계속하기를 눌렀을 때 Handler 설정
.failureHandler(oAuth2LoginFailureHandler) // 소셜 로그인 실패 시 핸들러 설정
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService)))

// Custom Exception Handling
.exceptionHandling(exceptionHandling ->
exceptionHandling.authenticationEntryPoint(customAuthenticationEntryPoint(new ObjectMapper()))
);

// [PART4]
// 원래 스프링 시큐리티 필터 순서가 LogoutFilter 이후에 로그인 필터 동작
// 따라서, LogoutFilter 이후에 우리가 만든 필터 동작하도록 설정
// 순서 : LogoutFilter -> JwtAuthenticationProcessingFilter -> CustomJsonUsernamePasswordAuthenticationFilter
http.addFilterBefore(jwtAuthenticationProcessingFilter(), LogoutFilter.class);

return http.build();
}

@Bean
public CustomAuthenticationEntryPoint customAuthenticationEntryPoint(ObjectMapper objectMapper) {
return new CustomAuthenticationEntryPoint(objectMapper);
}

@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
} // 패스워드 인코더

@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("POST", "PATCH", "GET", "DELETE"));

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
} // CORS 설정

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
} // 정적 리소스 보안 필터 해제

@Bean
public JwtAuthenticationProcessingFilter jwtAuthenticationProcessingFilter() {
JwtAuthenticationProcessingFilter jwtAuthenticationFilter = new JwtAuthenticationProcessingFilter(jwtService,
userRepository);
return jwtAuthenticationFilter;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.readyvery.readyverydemo.security.exception;

import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.readyvery.readyverydemo.security.exception.dto.ErrorResponse;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;


public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

private final ObjectMapper objectMapper;

@Autowired
public CustomAuthenticationEntryPoint(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {

response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");

ErrorResponse errorResponse = new ErrorResponse("Access Denied", request.getRequestURI());
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.readyvery.readyverydemo.security.exception.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class ErrorResponse {
private String message;
private String path;

}
Loading