Skip to content

Commit

Permalink
Merge pull request #19 from Kusitms-29th-Meetup-TeamE/feat/14/oauth
Browse files Browse the repository at this point in the history
Feat: 카카오 oauth 로그인 구현
  • Loading branch information
Jeongho427 authored May 7, 2024
2 parents d53302d + f42871c commit c61b830
Show file tree
Hide file tree
Showing 19 changed files with 581 additions and 4 deletions.
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'

//jwt
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'


//secrets manager
implementation 'io.awspring.cloud:spring-cloud-starter-aws-secrets-manager-config:2.4.4'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
package com.meetup.teame.backend.domain.auth.config;

import com.meetup.teame.backend.domain.auth.jwt.JwtFilter;
import lombok.RequiredArgsConstructor;
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.web.SecurityFilterChain;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.web.cors.CorsConfiguration;

import java.util.List;

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtFilter jwtFilter;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(c -> c.disable())
.authorizeHttpRequests(c -> c
.anyRequest().permitAll())
.requestMatchers(
"/api/sign-up",
"/api/login",
"/users/main",
"/swagger-ui/**",
"/swagger-resources/**",
"/v3/api-docs/**").permitAll()
.anyRequest().authenticated())
.addFilterBefore(jwtFilter, ExceptionTranslationFilter.class)
.cors(c -> c.configurationSource(request -> {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000", "https://api.yeongjin.site", "https://ddoba.vercel.app"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.meetup.teame.backend.domain.auth.jwt;

import com.meetup.teame.backend.global.exception.CustomException;
import com.meetup.teame.backend.global.exception.ExceptionContent;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
private static final String AUTHORIZATION = "Authorization";
private static final String BEARER = "Bearer ";

private final JwtProvider jwtProvider;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String accessToken = getAccessTokenFromHttpServletRequest(request);
jwtProvider.validateAccessToken(accessToken);
final Long userId = jwtProvider.getSubject(accessToken);
setAuthentication(request, userId);
filterChain.doFilter(request, response);
}

private String getAccessTokenFromHttpServletRequest(HttpServletRequest request) {
String accessToken = request.getHeader(AUTHORIZATION);
if (StringUtils.hasText(accessToken) && accessToken.startsWith(BEARER)) {
return accessToken.substring(BEARER.length());
}
throw new CustomException(ExceptionContent.INVALID_TOKEN);
}

private void setAuthentication(HttpServletRequest request, Long userId) {
UserAuthentication authentication = new UserAuthentication(userId, null, null);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.meetup.teame.backend.domain.auth.jwt;

import com.meetup.teame.backend.global.exception.CustomException;
import com.meetup.teame.backend.global.exception.ExceptionContent;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Base64;
import java.util.Date;

@Getter
@Component
public class JwtProvider {

@Value("${jwt.secret}")
private String secretKey;

public void validateAccessToken(String accessToken) {
try {
getJwtParser().parseClaimsJws(accessToken);
} catch (ExpiredJwtException e) {
throw new CustomException(ExceptionContent.EXPIRED_TOKEN);
} catch (Exception e) {
throw new CustomException(ExceptionContent.INVALID_TOKEN);
}
}

public Long getSubject(String token) {
return Long.valueOf(getJwtParser().parseClaimsJws(token)
.getBody()
.getSubject());
}

public String generateToken(Long userId, long expireTime) {
final Date now = new Date();
final Date expiration = new Date(now.getTime() + expireTime);
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setSubject(String.valueOf(userId))
.setIssuedAt(now)
.setExpiration(expiration)
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}

private JwtParser getJwtParser() {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build();
}

private Key getSigningKey() {
String encoded = Base64.getEncoder().encodeToString(secretKey.getBytes());
return Keys.hmacShaKeyFor(encoded.getBytes());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.meetup.teame.backend.domain.auth.jwt;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class UserAuthentication extends UsernamePasswordAuthenticationToken {
public UserAuthentication(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.meetup.teame.backend.domain.auth.jwt.dto;

import com.meetup.teame.backend.domain.user.entity.User;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {

private final User user;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {

Collection<GrantedAuthority> collection = new ArrayList<>();

collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getEmail();
}
});
return collection;
}

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

@Override
public String getPassword() {
return null;
}

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

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

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

@Override
public boolean isEnabled() {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.meetup.teame.backend.domain.auth.jwt.dto;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class TokenInfo {
private String accessToken;
private String refreshToken;

public static TokenInfo of(String accessToken, String refreshToken) {
return new TokenInfo(accessToken, refreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.meetup.teame.backend.domain.auth.jwt.service;

import com.meetup.teame.backend.domain.auth.jwt.dto.CustomUserDetails;
import com.meetup.teame.backend.domain.user.entity.User;
import com.meetup.teame.backend.domain.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

//DB에서 조회
User userData = userRepository.findByName(username);

if (userData != null) {

//UserDetails에 담아서 return하면 AutneticationManager가 검증 함
return new CustomUserDetails(userData);
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.meetup.teame.backend.domain.auth.oauth.controller;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.meetup.teame.backend.domain.auth.oauth.dto.CreateOauthUserRequest;
import com.meetup.teame.backend.domain.auth.oauth.dto.CreateUserRequest;
import com.meetup.teame.backend.domain.auth.oauth.service.KakaoService;
import com.meetup.teame.backend.domain.user.entity.User;
import com.meetup.teame.backend.domain.user.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;


@RequiredArgsConstructor
@RestController
@RequestMapping("/api")
@Tag(name = "oauth", description = "로그인 관련 api")
public class KakaoController {

private final UserService userService;
private final KakaoService kakaoService;

@Operation(summary = "카카오 로그인", description = """
카카오 소셜 로그인을 진행합니다.
이미 등록된 사용자면 "login"이 출력되고
등록되지 않은 사용자면 "/api/sign-up"을 요청해서 거주지 정보를 추가로 받아줘야 합니다.
""")
@GetMapping("/login/kakao")
public ResponseEntity<Object> kakaoLogin(@RequestParam String code) throws JsonProcessingException {
String kakaoAccessToken = kakaoService.getKakaoAccessToken(code); //인가코드로 카카오 엑세스 토큰 받아오기
CreateOauthUserRequest request = kakaoService.getKakaoInfo(kakaoAccessToken); //엑세스 토큰으로 카카오 사용자 정보 받아오기
boolean checkExist = kakaoService.userExists(request.getEmail());
if(checkExist) { //이미 가입된 회원
/*Optional<User> userOptional*/User user = userService.findByEmail(request.getEmail());
//User user = userOptional.get();
HttpHeaders headers = kakaoService.getLoginHeader(user);

return ResponseEntity.ok().headers(headers).body("login");
//로그인 처리하기
} else { //신규 회원
return ResponseEntity.ok(request);
}
}

@Operation(summary = "사용자 등록", description = """
사용자 정보 등록을 진행합니다.
거주지까지 입력을 마친 정보로 DB에 사용자 정보를 등록합니다.
Jwt 토큰을 헤더에 넣어서 "OK" 메세지와 함께 반환합니다.
""")
@PostMapping("/sign-up")
public ResponseEntity<Object> signup(@RequestBody CreateUserRequest request) { //이미 있는 회원인지 확인해야됨
User user = userService.createUser(request);
Long userId = userService.save(user);
HttpHeaders headers = kakaoService.getLoginHeader(userService.findById(userId));
return ResponseEntity.ok().headers(headers).body("OK");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.meetup.teame.backend.domain.auth.oauth.dto;

import lombok.*;

@AllArgsConstructor
@NoArgsConstructor
@Getter
public class CreateOauthUserRequest {
private String name; // 필수 동의
private String email; // 선택 동의
private String gender; // 필수 동의
private String birthYear; // 필수 동의
private String profileImage; // 선택 동의
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.meetup.teame.backend.domain.auth.oauth.dto;

import com.meetup.teame.backend.domain.user.entity.Gender;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;

@AllArgsConstructor
@NoArgsConstructor
@Getter
public class CreateUserRequest {

private String name;

private String email;

private String imageUrl;

private String gender;

private String birthyear;

private String location;
}
Loading

0 comments on commit c61b830

Please sign in to comment.