Skip to content

Commit

Permalink
Feat: 소셜로그인 구현 (#16)
Browse files Browse the repository at this point in the history
* Feat(#10): RestTemplate 이용한 카카오 계정 정보 받아오기 로직 작성

* Feat(#10): 카카오 계정 이메일 추출

* Feat(#10): 네이버 카카오 인가코드 이용해 사용자 정보 추출

* Feat(#10): jwt 인증 구현

* Feat(#10): Redis 이용한 리프레쉬 토큰 구현

* Chore(#10): docker-compose.yml에 redis 추가

* Feat(#10): swagger 내용 작성

* Feat(#10): 토큰 갱신 api 구현

* Refactor(#10): 코드 리팩토링

* Fix(#10): inner class protected 삭제

* Fix(#10): gitignore 오타 수정
  • Loading branch information
nohy6630 authored and versatile0010 committed Oct 15, 2023
1 parent eac59ee commit 1b3af09
Show file tree
Hide file tree
Showing 28 changed files with 678 additions and 6 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ application.properties

### VS Code ###
.vscode/

**/src/main/generated/
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ dependencies {

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'

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

implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'com.sun.xml.bind:jaxb-impl:4.0.1'
implementation 'com.sun.xml.bind:jaxb-core:4.0.1'
implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
}

tasks.named('test') {
Expand Down
8 changes: 7 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,10 @@ services:
container_name: numberone
restart: always
ports:
- 8080:8080
- 8080:8080

redis:
hostname: redis
image: redis:latest
ports:
- 6379:6379
14 changes: 14 additions & 0 deletions src/main/java/com/numberone/backend/LoginTestController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.numberone.backend;

import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LoginTestController {
@RequestMapping("/logintest")
public String test(Authentication authentication){
return "Hello "+authentication.getName();
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/numberone/backend/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.numberone.backend.config;

import com.numberone.backend.properties.RedisProperties;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@RequiredArgsConstructor
public class RedisConfig {
private final RedisProperties redisProperties;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
}

@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/numberone/backend/config/RestTemplateConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.numberone.backend.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();

}
}
44 changes: 40 additions & 4 deletions src/main/java/com/numberone/backend/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,26 +1,60 @@
package com.numberone.backend.config;

import com.numberone.backend.config.auth.JwtFilter;
import com.numberone.backend.properties.JwtProperties;
import lombok.RequiredArgsConstructor;
import com.numberone.backend.config.auth.JwtFilter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.Customizer;
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.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;

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

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
http
.httpBasic(httpBasic -> httpBasic.disable())
.csrf(csrf -> csrf.disable())
.cors(Customizer.withDefaults())
.authorizeHttpRequests((auth) -> auth
.anyRequest().authenticated() // 모든 요청에 대한 인증 처리하도록 설정
)
.httpBasic(HttpBasicConfigurer::disable);
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(exception -> exception
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler()));
;
return http.build();
}

Expand All @@ -32,7 +66,9 @@ public WebSecurityCustomizer webSecurityCustomizer() {
"/error",
"/swagger-ui/**",
"/swagger-resources/**",
"/v3/api-docs/**")
.requestMatchers("/**"); // 인증 처리 하지 않을 케이스
"/v3/api-docs/**",
"/token/**",
"/shelter/**");
//.requestMatchers("/**"); // 인증 처리 하지 않을 케이스
}
}
54 changes: 54 additions & 0 deletions src/main/java/com/numberone/backend/config/auth/JwtFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.numberone.backend.config.auth;

import com.numberone.backend.domain.member.entity.Member;
import com.numberone.backend.domain.member.service.MemberService;
import com.numberone.backend.domain.token.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Collections;

@RequiredArgsConstructor
@Component
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final MemberService memberService;

//토큰이 유효하지 않다면 setAuthentication이 진행되지 않아 UsernamePasswordAuthenticationFilter에서 인증이 되지 않음
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}

String token = authorizationHeader.split(" ")[1];
if (jwtUtil.isExpired(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Token has expired");
filterChain.doFilter(request, response);
return;
}

String email = jwtUtil.getEmail(token);
Member member = memberService.findByEmail(email);

UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
member.getEmail(), null, Collections.emptyList());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.numberone.backend.domain.member.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String email;

@Builder
public Member(String email) {
this.email = email;
}

public static Member of(String email){
return Member.builder()
.email(email)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.numberone.backend.domain.member.repository;

import com.numberone.backend.domain.member.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);
boolean existsByEmail(String email);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.numberone.backend.domain.member.service;

import com.numberone.backend.domain.member.entity.Member;
import com.numberone.backend.domain.member.repository.MemberRepository;
import com.numberone.backend.exception.notfound.NotFoundMemberException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
@Transactional(readOnly = true)
public class MemberService {
private final MemberRepository memberRepository;

public Member findByEmail(String email) {
return memberRepository.findByEmail(email)
.orElseThrow(NotFoundMemberException::new);
}

public void create(String email) {
memberRepository.save(Member.of(email));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.numberone.backend.domain.token.controller;

import com.numberone.backend.domain.token.dto.request.TokenRequest;
import com.numberone.backend.domain.token.dto.response.TokenResponse;
import com.numberone.backend.domain.token.service.TokenService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "token", description = "토큰 관련 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/token")
public class TokenController {
private final TokenService tokenService;

@Operation(summary = "카카오 토큰을 이용하여 서버 JWT 토큰 발급받기", description =
"""
카카오 토큰을 body 에 담아서 post 요청 해주세요.
앞으로 서버 요청 시에 사용할 수 있는 JWT 토큰이 발급됩니다.
이후 서버에 API 요청시 이 JWT 토큰을 같이 담아서 요청해야 정상적으로 API가 호출 됩니다.
""")
@PostMapping("/kakao")
public TokenResponse loginKakao(@RequestBody TokenRequest tokenRequest) {
return tokenService.loginKakao(tokenRequest);
}

@Operation(summary = "네이버 토큰을 이용하여 서버 JWT 토큰 발급받기", description =
"""
네이버 토큰을 body 에 담아서 post 요청 해주세요.
앞으로 서버 요청 시에 사용할 수 있는 JWT 토큰이 발급됩니다.
이후 서버에 API 요청시 이 JWT 토큰을 같이 담아서 요청해야 정상적으로 API가 호출 됩니다.
""")
@PostMapping("/naver")
public TokenResponse loginNaver(@RequestBody TokenRequest tokenRequest) {
return tokenService.loginNaver(tokenRequest);
}

@Operation(summary = "만료된 JWT 토큰 갱신하기", description =
"""
만료된 JWT 토큰을 body 에 담아서 post 요청 해주세요.
새로 사용할 수 있는 JWT 토큰이 발급됩니다.
""")
@PostMapping("/refresh")
public TokenResponse refresh(@RequestBody TokenRequest tokenRequest) {
return tokenService.refresh(tokenRequest);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.numberone.backend.domain.token.dto.request;

import lombok.*;

@ToString
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TokenRequest {
private String token;
}
Loading

0 comments on commit 1b3af09

Please sign in to comment.