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] 카카오 로그인 기능 추가 #50

Merged
merged 2 commits into from
Oct 21, 2024
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
15 changes: 11 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,20 @@ repositories {
}

dependencies {
//PostGreSQL driver add
//runtimeOnly 'org.postgresql:postgresql'
// oauth2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'io.jsonwebtoken:jjwt:0.9.1' // 자바 JWT 라이브러리
implementation 'javax.xml.bind:jaxb-api:2.3.1' // XML 문서와 Java 객체 간 매핑 자동화
testAnnotationProcessor('org.projectlombok:lombok')
// JsonObject
implementation 'com.googlecode.json-simple:json-simple:1.1'
// JWT
implementation 'io.jsonwebtoken:jjwt:0.9.1'
// XML 문서와 Java 객체 간 매핑 자동화
implementation 'javax.xml.bind:jaxb-api:2.3.1'
//swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
// Jackson
Expand All @@ -57,14 +64,14 @@ dependencies {

// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.springframework:spring-webflux'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testAnnotationProcessor('org.projectlombok:lombok')
}

tasks.named('test') {
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/wanted/ribbon/global/response/ResponseCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package wanted.ribbon.global.response;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
* ResoibseCode
* - 도메인 별로 나누어 관리
* - [동사_목적어_SUCCESS] 형태로 생성
* */

@Getter
@AllArgsConstructor
public enum ResponseCode {
// User
LOGIN_SUCCESS(200, "U001", "로그인에 성공하였습니다."),
GET_USERPROFILE_SUCCESS(200, "U002", "회원 프로필을 조회하였습니다."),
EDIT_PROFILE_SUCCESS(200, "U003", "회원 프로필을 수정하였습니다."),
LOGIN_FAIL(200, "U004", "로그인에 실패하였습니다."),
SAVE_PROFILE_SUCCESS(200, "U005", "회원 프로필을 저장하였습니다."),
DELETE_SUCCESS(200, "U006", "회원 탈퇴에 성공하였습니다."),
DELETE_FAIL(200, "U007", "회원 탈퇴에 실패했습니다."),
CHECK_TOKEN_SUCCESS(200, "U008", "유효한 토큰입니다."),
CHECK_TOKEN_FAIL(200, "U009", "만료된 토큰입니다.")
;

private final int status;
private final String code;
private final String message;
}
33 changes: 33 additions & 0 deletions src/main/java/wanted/ribbon/global/response/ResponseDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package wanted.ribbon.global.response;

import lombok.Data;
import lombok.Getter;

@Getter
@Data
public class ResponseDto {
private int status;

private String code;

private String message;

private Object data;

public ResponseDto(ResponseCode responseCode, Object data) {
this.status = responseCode.getStatus();
this.code = responseCode.getCode();
this.message = responseCode.getMessage();
this.data = data;
}

// 전송할 데이터가 있는 경우
public static ResponseDto of(ResponseCode responseCode, Object data){
return new ResponseDto(responseCode, data);
}

// 전송할 데이터가 없는 경우
public static ResponseDto of(ResponseCode responseCode){
return new ResponseDto(responseCode, "");
}
}
22 changes: 19 additions & 3 deletions src/main/java/wanted/ribbon/user/config/WebSecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import wanted.ribbon.user.service.KakaoOAuth2UserService;
import wanted.ribbon.user.service.UserDetailService;

@RequiredArgsConstructor
@Configuration
public class WebSecurityConfig {
private final UserDetailService userService;
private final TokenProvider tokenProvider;
private final KakaoOAuth2UserService kakaoOAuth2UserService;

// 특정 HTTP 요청에 대한 웹 기반 보안 구성
@Bean
Expand All @@ -25,20 +28,33 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.httpBasic(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.authorizeRequests(auth -> auth
.requestMatchers("/api/users/login", "/api/users/signup").permitAll()
.requestMatchers("/api/users/login", "/api/users/signup", "/api/oauth/**", "/exception/**", "/main").permitAll() // 일반 회원가입, 로그인 및 oauth 설정
.requestMatchers("/api/datapipes/**").permitAll() // 데이터파이프라인 모든 권한 허용
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll() //swagger
.anyRequest().authenticated())
.formLogin(AbstractHttpConfigurer::disable)
.addFilterBefore(new TokenAuthenticationFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class);
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) // 필요한 경우 세션 생성
.oauth2Login(oauth2 -> oauth2
.loginPage("/api/oauth/kakao/login")
.userInfoEndpoint(userInfo -> userInfo
.userService(kakaoOAuth2UserService)))
.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); // 필터를 메서드로 추가

return http.build();
}

// TokenAuthenticationFilter를 @Bean으로 등록
@Bean
public TokenAuthenticationFilter tokenAuthenticationFilter() {
return new TokenAuthenticationFilter(tokenProvider);
}

// 인증 관리자 관련 설정
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder, UserDetailService userDetailService) throws Exception {
AuthenticationManagerBuilder auth = http.getSharedObject(AuthenticationManagerBuilder.class);
auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
auth.userDetailsService(userDetailService).passwordEncoder(bCryptPasswordEncoder);
return auth.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ public ResponseEntity<SignUpResponse> signUp(@Validated @RequestBody SignUpUserR
}

@PostMapping("/login")
public ResponseEntity<UserLoginResponseDto> login(@RequestBody UserLoginRequestDto requestDto) {
public ResponseEntity<LoginResponseDto> login(@RequestBody LoginRequestDto requestDto) {
// 사용자 검증 로직 추가
UserLoginResponseDto responseDto = userService.login(requestDto);
LoginResponseDto responseDto = userService.login(requestDto);
return ResponseEntity.ok(responseDto);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package wanted.ribbon.user.controller;

import lombok.RequiredArgsConstructor;
import org.json.simple.parser.ParseException;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import wanted.ribbon.global.response.ResponseCode;
import wanted.ribbon.global.response.ResponseDto;
import wanted.ribbon.user.domain.SocialType;
import wanted.ribbon.user.domain.User;
import wanted.ribbon.user.dto.LoginRequestDto;
import wanted.ribbon.user.service.UserOauthService;

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

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/oauth")
public class UserOauthController {
private final UserOauthService userAuthService;

@GetMapping("/kakao/login")
public ResponseEntity<ResponseDto> kakaoLogin(@RequestParam("code") String code) throws ParseException {
// 액세스 토큰 받아오기
String accessToken = userAuthService.getKakaoAccessToken(code);
System.out.println("access_token : " + accessToken);

// 액세스 토큰을 사용해 카카오 유저 정보 받아오기
Map<String, Object> userInfo = userAuthService.getKakaoUserInfo(accessToken);

if(userInfo != null && userInfo.get("id") != null){
String id = userInfo.get("id").toString();
System.out.println("카카오 로그인 유저 ID: " + id);
LoginRequestDto userLoginDto = userAuthService.loginUser(id, SocialType.KAKAO);

if(userLoginDto != null){
// 유저 등록 또는 로그인 처리
User user = userAuthService.registedUser(id, SocialType.KAKAO);
System.out.println("사용자 저장 완료: " + user.getId());

// 인증 정보를 설정하고 세션에 저장
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userLoginDto.getId(), null, new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);

// 로그인 성공 후 리다이렉션이나 응답 반환
return ResponseEntity.ok(ResponseDto.of(ResponseCode.LOGIN_SUCCESS, user));

}
}
return ResponseEntity.badRequest().body(ResponseDto.of(ResponseCode.LOGIN_FAIL));
}
}
3 changes: 2 additions & 1 deletion src/main/java/wanted/ribbon/user/domain/RefreshToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public class RefreshToken {
@Column(name = "token_id", nullable = false)
private Long tokenId;

@ManyToOne(fetch = FetchType.LAZY)
@ManyToOne(fetch =
FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/wanted/ribbon/user/domain/SocialType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package wanted.ribbon.user.domain;

public enum SocialType {
KAKAO;
}
12 changes: 11 additions & 1 deletion src/main/java/wanted/ribbon/user/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
Expand Down Expand Up @@ -39,14 +40,23 @@ public class User implements UserDetails {
@Column(name = "recommend")
private boolean recommend;

@Enumerated(EnumType.STRING)
@Column(name="social_type", length = 10)
private SocialType socialType;

@Column(name="reg_time")
private LocalDateTime regTime;

@Builder
public User(UUID userId, String id, String password, double lat, double lon, boolean recommend) {
public User(UUID userId, String id, String password, double lat, double lon, boolean recommend, SocialType socialType) {
this.userId = userId;
this.id = id;
this.password = password;
this.lat = lat;
this.lon = lon;
this.recommend = recommend;
this.socialType = socialType;
this.regTime = LocalDateTime.now();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import wanted.ribbon.user.domain.SocialType;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class UserLoginRequestDto {
public class LoginRequestDto {
private String id;
private String password;
private SocialType socialType;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class UserLoginResponseDto {
public class LoginResponseDto {
private UUID userId;
private String accessToken;
private String refreshToken;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class SignUpUserRequest {
@Size(max = 50)
private String id;

@NotBlank
// @NotBlank
@Size(max = 200)
private String password;
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package wanted.ribbon.user.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import wanted.ribbon.user.domain.SocialType;
import wanted.ribbon.user.domain.User;

import java.util.Optional;
import java.util.UUID;

public interface UserRepository extends JpaRepository<User, UUID> {
// id로 사용자 정보를 가져옴
Optional<User> findById(String id);
Optional<User> findByIdAndSocialType(String id, SocialType socialType);
Optional<Object> findById(String id);
}
49 changes: 49 additions & 0 deletions src/main/java/wanted/ribbon/user/service/CustomUserDetail.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package wanted.ribbon.user.service;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

public class CustomUserDetail implements UserDetails {
private final String username;

public CustomUserDetail(String username) {
this.username = username;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null; // 사용자 권한 처리
}

@Override
public String getPassword() {
return null; // 패스워드가 없으므로 null 반환
}

@Override
public String getUsername() {
return username;
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}
Loading