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 Security Oauth2.0 Pull Request #8

Merged
merged 19 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'

// oauth
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}

tasks.named('bootBuildImage') {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.api.TaveShot.domain.Member;
package com.api.TaveShot.domain.Member.domain;

import com.api.TaveShot.domain.base.BaseEntity;
import jakarta.persistence.Entity;
Expand All @@ -8,8 +8,10 @@
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
Expand All @@ -19,4 +21,10 @@ public class Member extends BaseEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private Long gitId;
private String gitLoginId;
private String gitEmail;
private String gitName;
private String profileImageUrl;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.api.TaveShot.domain.Member.dto.response;

import lombok.Builder;

@Builder
public record AuthResponse(Long memberId, String gitLoginId, String mail, String gitProfileImageUrl) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.api.TaveShot.domain.Member.repository;

import com.api.TaveShot.domain.Member.domain.Member;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByGitId(Long gitId);
}
14 changes: 11 additions & 3 deletions src/main/java/com/api/TaveShot/domain/base/BaseEntityConfig.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
package com.api.TaveShot.domain.base;

import java.util.Optional;
import java.util.UUID;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

@EnableJpaAuditing
@Configuration
public class BaseEntityConfig {

@Bean
public AuditorAware<String> auditorProvider() {
// TODO 지금은 UUID이지만, Security 설정 후 spring Securitycontextholder 에서 값 꺼내기
return () -> Optional.of(UUID.randomUUID().toString());
return () -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return Optional.of("Anonymous");
}

String name = authentication.getName();
return Optional.of(name);
};
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/api/TaveShot/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.api.TaveShot.global.config;

import com.api.TaveShot.global.jwt.JwtAuthenticationFilter;
import com.api.TaveShot.global.oauth2.CustomOAuth2UserService;
import com.api.TaveShot.global.oauth2.CustomOAuthSuccessHandler;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
Expand All @@ -10,13 +13,18 @@
import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final CustomOAuth2UserService customOAuth2UserService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomOAuthSuccessHandler customOAuthSuccessHandler;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
Expand All @@ -40,9 +48,21 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
, "/swagger-ui/**"
, "/api-docs/swagger-config"
, "/members/login"
,"/oauth/**"
,"/favicon.ico"
,"/login/**"
, "/**"
).permitAll()
.anyRequest().permitAll());
http
.oauth2Login()
.authorizationEndpoint().baseUri("/login/oauth2/code/github")
.and()
.userInfoEndpoint()
.userService(customOAuth2UserService)
.and()
.successHandler(customOAuthSuccessHandler);
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/com/api/TaveShot/global/constant/OauthConstant.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.api.TaveShot.global.constant;

public final class OauthConstant {

private OauthConstant() {
throw new IllegalArgumentException("인스턴스화 불가");
}

public static final String ID_PATTERN = "id";
public static final String PROFILE_IMAGE_URL_PATTERN = "avatar_url";
public static final String LOGIN_PATTERN = "login";
// ToDo 추후 Github에서 제공하는 Name 을 사용할지 결정
public static final String NAME_PATTERN = "name";
public static final String EMAIL_PATTERN = "email";
public static final long ACCESS_TOKEN_VALID_TIME = 15 * 60 * 1000L;
public static final String REDIRECT_URL = "http://localhost:5173";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.api.TaveShot.global.jwt;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtProvider jwtProvider;

@Override
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response,
final FilterChain filterChain) throws ServletException, IOException {

String requestURI = request.getRequestURI();
if (isPublicUri(requestURI)) {
// Public uri 일 경우 검증 안함
filterChain.doFilter(request, response);
return;
}

String authorizationHeader = request.getHeader("Authorization");

if (authorizationHeader != null && isBearer(authorizationHeader)) {
// "Bearer " 이후의 문자열을 추출
String jwtToken = authorizationHeader.substring(7);

// token 단순 유효성 검증
jwtProvider.isValidToken(jwtToken);

// token을 활용하여 유저 정보 검증
jwtProvider.getAuthenticationFromToken(jwtToken);
}

filterChain.doFilter(request, response);

}

private boolean isBearer(final String authorizationHeader) {
return authorizationHeader.startsWith("Bearer ");
}

private boolean isPublicUri(final String requestURI) {
return
requestURI.startsWith("/swagger-ui") ||
requestURI.startsWith("/favicon.ico") ||
requestURI.startsWith("/oauth/**") ||
requestURI.startsWith("/login/**");
}
}
101 changes: 101 additions & 0 deletions src/main/java/com/api/TaveShot/global/jwt/JwtProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.api.TaveShot.global.jwt;

import static com.api.TaveShot.global.constant.OauthConstant.ACCESS_TOKEN_VALID_TIME;

import com.api.TaveShot.domain.Member.repository.MemberRepository;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import javax.crypto.SecretKey;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
@Slf4j
@RequiredArgsConstructor
public class JwtProvider {

private final MemberRepository memberRepository;

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

public String generateJwtToken(final String id) {
Claims claims = createClaims(id);
Date now = new Date();
long expiredDate = calculateExpirationDate(now);
SecretKey secretKey = generateKey();

return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(expiredDate))
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}

// JWT claims 생성
private Claims createClaims(final String id) {
return Jwts.claims().setSubject(id);
}

// JWT 만료 시간 계산
private long calculateExpirationDate(final Date now) {
return now.getTime() + ACCESS_TOKEN_VALID_TIME;
}

// Key 생성
private SecretKey generateKey() {
return Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
}

// 토큰의 유효성 검사
public void isValidToken(final String jwtToken) {
try {
SecretKey key = generateKey();
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(jwtToken);

} catch (ExpiredJwtException e) { // 어세스 토큰 만료
throw new IllegalArgumentException("Access Token expired");
} catch (Exception e) {
throw new IllegalArgumentException("User Not Authorized");
}
}

// jwtToken 으로 Authentication 에 사용자 등록
public void getAuthenticationFromToken(final String jwtToken) {

log.info("-------------- getAuthenticationFromToken jwt token: " + jwtToken);
getGitLoginId(jwtToken);

}

// token 으로부터 유저 정보 확인
private void getGitLoginId(final String jwtToken) {
Long userId = Long.valueOf(getUserIdFromToken(jwtToken));
memberRepository.findById(userId).orElseThrow(() -> new RuntimeException("token 으로 Member를 찾을 수 없음"));
}

// 토큰에서 유저 아이디 얻기
public String getUserIdFromToken(final String jwtToken) {
SecretKey key = generateKey();

Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(jwtToken)
.getBody();

log.info("-------------- JwtProvider.getUserIdFromAccessToken: " + claims.getSubject());
return claims.getSubject();
}
}
Loading