Skip to content

Commit

Permalink
Feat: KakaoOAuth 기능 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
1223v committed Nov 13, 2023
1 parent 0da16eb commit 6a7d9ab
Show file tree
Hide file tree
Showing 23 changed files with 1,164 additions and 28 deletions.
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

0 comments on commit 6a7d9ab

Please sign in to comment.