Skip to content

Commit

Permalink
Feat: 카카오 로그인, 회원가입 기능 추가
Browse files Browse the repository at this point in the history
- 권한 필요한 API 접속 시 카카오 로그인 페이지 반환
  - 로그인 성공 시 가입이 안되어있으면 가입, 되어있으면 권한 부여
  - 권한은 JWT 발급 후 쿠키에 추가, 매 요청마다 JwtFilter를 거치며 쿠키의 Jwt 확

Related to: KakaoTech-BootCamp-Team-2#1
  • Loading branch information
Taejin1221 committed Aug 30, 2024
1 parent 0f1e1eb commit 51acd08
Show file tree
Hide file tree
Showing 21 changed files with 626 additions and 10 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,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
8 changes: 7 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ repositories {

dependencies {
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'
Expand All @@ -33,7 +34,12 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testRuntimeOnly 'com.h2database:h2'

// 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') {
Expand Down
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();
}
13 changes: 12 additions & 1 deletion src/main/java/kaboo/kaboo_auth/domain/entity/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

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;
Expand All @@ -22,6 +25,9 @@ public class Member {
@Setter
private String username;

@Setter
private String email;

@Setter
private String nickname;

Expand All @@ -31,10 +37,15 @@ public class Member {
@Setter
private String info;

@Enumerated(EnumType.STRING)
private UserRole role;

@Builder
public Member(String username, String nickname, String password) {
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package kaboo.kaboo_auth.domain.handler;

import java.io.IOException;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import kaboo.kaboo_auth.domain.jwt.JwtTokenProvider;
import kaboo.kaboo_auth.domain.jwt.entity.JwtAccessToken;
import kaboo.kaboo_auth.domain.jwt.entity.JwtRefreshToken;
import kaboo.kaboo_auth.domain.jwt.repository.JwtAccessTokenRepository;
import kaboo.kaboo_auth.domain.jwt.repository.JwtRefreshTokenRepository;
import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

private final JwtAccessTokenRepository jwtAccessTokenRepository;
private final JwtRefreshTokenRepository jwtRefreshTokenRepository;
private final JwtTokenProvider jwtTokenProvider;
private final int accessTokenValidTime = 10 * 60; // 유효기간 : 10분
private final int refreshTokenValidTime = 10 * 24 * 60 * 60; // 유효기간 : 10일

private Cookie createCookie(String key, String value, int maxAge) {
Cookie cookie = new Cookie(key, value);
cookie.setMaxAge(maxAge);
cookie.setPath("/");
cookie.setHttpOnly(true);

return cookie;
}

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws
IOException {
String username = authentication.getName();
String accessToken = jwtTokenProvider.createAccessToken(username);
String refreshToken = jwtTokenProvider.createRefreshToken(username);

jwtAccessTokenRepository.save(new JwtAccessToken(username, accessToken));
jwtRefreshTokenRepository.save(new JwtRefreshToken(username, refreshToken));

response.addCookie(createCookie("Username", username, refreshTokenValidTime));
response.addCookie(createCookie("Authorization", accessToken, accessTokenValidTime));
response.addCookie(createCookie("RefreshToken", refreshToken, refreshTokenValidTime));
response.sendRedirect("/");
}
}
Loading

0 comments on commit 51acd08

Please sign in to comment.