Skip to content

Commit

Permalink
✨ 카카오 로그인/회원가입 구현 (#3)
Browse files Browse the repository at this point in the history
* Style: codeStyle 추가

* Feat: Member, Profile Entity, MemberRepository 추가

+ MemberRepositoryTest 추가

Related to: #1

* Feat: 카카오 로그인, 회원가입 기능 추가

- 권한 필요한 API 접속 시 카카오 로그인 페이지 반환
  - 로그인 성공 시 가입이 안되어있으면 가입, 되어있으면 권한 부여
  - 권한은 JWT 발급 후 쿠키에 추가, 매 요청마다 JwtFilter를 거치며 쿠키의 Jwt 확

Related to: #1

* Style: 코드 포맷, 비밀번호 노출 등 수정

Related to: #1
  • Loading branch information
Taejin1221 authored Aug 30, 2024
1 parent 57b7065 commit 90052aa
Show file tree
Hide file tree
Showing 27 changed files with 811 additions and 23 deletions.
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ fabric.properties

.idea/*

!.idea/codeStyles
!.idea/runConfigurations

### Java ###
Expand Down Expand Up @@ -175,4 +174,7 @@ gradle-app.setting
# Java heap dump
*.hprof

# End of https://www.toptal.com/developers/gitignore/api/java,gradle,macos,intellij+all
# End of https://www.toptal.com/developers/gitignore/api/java,gradle,macos,intellij+allauth.yml
oauth.yml
database.yml
jwt.yml
5 changes: 5 additions & 0 deletions .idea/codeStyles/codeStyleConfig.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 27 additions & 20 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,40 +1,47 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.6'
id 'java'
id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.6'
}

group = 'kaboo'
version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// JWT Token Dependency
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

tasks.named('test') {
useJUnitPlatform()
useJUnitPlatform()
}
14 changes: 14 additions & 0 deletions src/main/java/kaboo/kaboo_auth/config/PasswordEncoderConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package kaboo.kaboo_auth.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
48 changes: 48 additions & 0 deletions src/main/java/kaboo/kaboo_auth/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package kaboo.kaboo_auth.config;

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.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import kaboo.kaboo_auth.domain.handler.LoginSuccessHandler;
import kaboo.kaboo_auth.domain.jwt.filter.JwtFilter;
import kaboo.kaboo_auth.service.CustomOAuth2Service;
import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtFilter jwtFilter;
private final CustomOAuth2Service customOAuth2Service;
private final LoginSuccessHandler loginSuccessHandler;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/").permitAll()
.anyRequest().authenticated()) // 그 외 요청은 인증 필요
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin));

http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

http.oauth2Login(auth -> auth
.userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig.userService(customOAuth2Service))
.successHandler(loginSuccessHandler));

return http.build();
}
}
24 changes: 24 additions & 0 deletions src/main/java/kaboo/kaboo_auth/controller/MainController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package kaboo.kaboo_auth.controller;

import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MainController {

@GetMapping("/")
public String mainAPI() {
return "누구나 접근가능한 API입니다.";
}

@GetMapping("/test")
public String authAPI() {
return "권한 Test API 입니다.";
}

@GetMapping("/auth/hello")
public String helloAuth(Authentication authentication) {
return "인가 받은 사용자 " + authentication.getName() + " 님 환영합니다.";
}
}
45 changes: 45 additions & 0 deletions src/main/java/kaboo/kaboo_auth/domain/CustomUserDetails.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package kaboo.kaboo_auth.domain;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;

import kaboo.kaboo_auth.domain.entity.Member;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class CustomUserDetails implements OAuth2User, UserDetails {
private final Member member;

@Override
public String getUsername() {
return member.getUsername();
}

@Override
public String getName() {
return member.getNickname();
}

@Override
public String getPassword() {
return member.getPassword();
}

@Override
public Map<String, Object> getAttributes() {
return null;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add((GrantedAuthority)() -> member.getRole().toString());

return collection;
}
}
15 changes: 15 additions & 0 deletions src/main/java/kaboo/kaboo_auth/domain/UserRole.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package kaboo.kaboo_auth.domain;

import lombok.Getter;

@Getter
public enum UserRole {
ROLE_ADMIN("ROLE_ADMIN"),
ROLE_USER("ROLE_USER");

private final String role;

UserRole(String role) {
this.role = role;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package kaboo.kaboo_auth.domain.dto.response;

import java.util.Map;

/** Kakao Response 형태
* {
* id: int,
* connected_at: Date
* properties: {
* nickname: String
* profile_image: URL
* thumbnail_image: URL
* },
* kakao_account: {
* profile_nickname_needs_agreement: boolean,
* profile_image_needs_agreement: boolean
* profile: {
* nickname: String
* thumbnail_image_url: URL
* profile_image_url: URL
* is_default_image: boolean
* is_default_nickname: boolean
* },
* has_email=true,
* email_needs_agreement=false,
* is_email_valid=true,
* is_email_verified=true,
* [email protected]
* }
*
* }
*/

public class KakaoResponse implements OAuth2Response {
private final Map<String, Object> attribute;

public KakaoResponse(Map<String, Object> attribute) {
this.attribute = attribute;
}

@Override
public String getProvider() {
return "kakao";
}

@Override
public String getProviderId() {
return attribute.get("id").toString();
}

@Override
public String getEmail() {
Map<String, Object> kakaoAccount = (Map<String, Object>)attribute.get("kakao_account");

// "kakao_account" 맵에서 "email" 키의 값을 가져옴
if (kakaoAccount != null && kakaoAccount.containsKey("email")) {
return kakaoAccount.get("email").toString();
}
return null; // email이 없을 경우
}

@Override
public String getNickname() {
Map<String, Object> properties = (Map<String, Object>)attribute.get("properties");

// "properties" 맵에서 "nickname" 키의 값을 가져옴
if (properties != null && properties.containsKey("nickname")) {
return properties.get("nickname").toString();
}
return null; // nickname이 없을 경우
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package kaboo.kaboo_auth.domain.dto.response;

public interface OAuth2Response {
String getProvider();

String getProviderId();

String getEmail();

String getNickname();
}
41 changes: 41 additions & 0 deletions src/main/java/kaboo/kaboo_auth/domain/entity/Member.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package kaboo.kaboo_auth.domain.entity;

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 kaboo.kaboo_auth.domain.UserRole;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;

private String username;
private String email;
private String nickname;
private String password;
private String info;

@Enumerated(EnumType.STRING)
private UserRole role;

@Builder
public Member(String username, String email, String nickname, String password, UserRole role) {
this.username = username;
this.email = email;
this.nickname = nickname;
this.password = password;
this.role = role;
}
}
Loading

0 comments on commit 90052aa

Please sign in to comment.