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

2주차 다운 #10

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
642778c
docs: Todo 제작
momnpa333 Apr 2, 2024
7b7e7bc
feat: user 엔티티 제작
momnpa333 Apr 2, 2024
0fdda69
feat: 기프티콘 엔티티, 유저 엔티티 제작
momnpa333 Apr 3, 2024
c06f126
feat: 레포지토리 제작
momnpa333 Apr 3, 2024
5a5d980
feat: 유저 엔티티 생성 메서드 및 빌더 패턴 추가
momnpa333 Apr 3, 2024
1361488
KNU-HAEDAL#1 feat: 기프티콘 생성메서드, 비즈니스 로직, 빌더패턴 제작
momnpa333 Apr 4, 2024
e3c8124
KNU-HAEDAL#1 feat: 유저, 유저기프티콘 비즈니스 로직 추가
momnpa333 Apr 4, 2024
1658d32
KNU-HAEDAL#1 feat: 기프티콘 빌더 추가
momnpa333 Apr 4, 2024
ee1166d
KNU-HAEDAL#1 feat: 기프티콘 및 유저 기프티콘 서비스 제작
momnpa333 Apr 4, 2024
6bb70b7
KNU-HAEDAL#1 feat:기프티콘 컨트롤러 제작
momnpa333 Apr 4, 2024
e539571
KNU-HAEDAL#1 feat: exception handler제작 및 GifticonUpdateValidator 제작
momnpa333 Apr 4, 2024
2db300f
KNU-HAEDAL#1 feat: ErrorResponse 제작
momnpa333 Apr 4, 2024
43d3ab6
KNU-HAEDAL#1 fix: 기프티콘 수정 api 수정 및 dto 제작
momnpa333 Apr 4, 2024
9d7daa0
KNU-HAEDAL#1 feat: response dto 생성 및 그에 맞게 컨트롤러 수정
momnpa333 Apr 5, 2024
ac6ad8a
KNU-HAEDAL#1 feat: NotFoundGifticonException 추가
momnpa333 Apr 5, 2024
5f014a0
KNU-HAEDAL#1 docs: gitignore .yml 추가
momnpa333 Apr 5, 2024
6d7eb27
KNU-HAEDAL#1 refactor:notfoundexception 분리
momnpa333 Apr 5, 2024
76b91f2
KNU-HAEDAL#1 feat: 스웨거 기능 추가
momnpa333 Apr 5, 2024
4df947c
KNU-HAEDAL#1 feat: 회원가입을 위한 폴더 구조 제작
momnpa333 Apr 5, 2024
664ddc6
KNU-HAEDAL#1 delete: 공부의 흔적들.. 다 쓸모없는 짓이었음..
momnpa333 Apr 5, 2024
1463897
KNU-HAEDAL#1 feat: exception 처리
momnpa333 Apr 5, 2024
a943d80
KNU-HAEDAL#1 feat: jwt 및 필터체인 제작
momnpa333 Apr 5, 2024
4241c23
KNU-HAEDAL#1 feat: OAuthController 및 서비스 제작
momnpa333 Apr 5, 2024
9657c9b
KNU-HAEDAL#1 feat: 회원가입 기능 추가
momnpa333 Apr 5, 2024
6d66ed9
KNU-HAEDAL#1 docs: 리드미 업데이트
momnpa333 Apr 6, 2024
bd2dc67
KNU-HAEDAL#1 feat: 로그인 관련 exception 처리
momnpa333 Apr 14, 2024
cd4c616
KNU-HAEDAL#1 fatch: 노트북 바꿔서 yml 파일 및 build.gradle 및 변경사항 커밋
momnpa333 Apr 20, 2024
16a2b3e
KNU-HAEDAL#1 feat: docs 및 권한 관련 config 수정
momnpa333 Apr 25, 2024
3c0d089
KNU-HAEDAL#2 feat: Category 추가
momnpa333 May 10, 2024
0daa5af
KNU-HAEDAL#2 feat: FriendController 제작
momnpa333 May 10, 2024
6a7a6e4
KNU-HAEDAL#2 feat: FriendRequestJpaRepository 페이지 메서드 생성
momnpa333 May 10, 2024
4f0c875
KNU-HAEDAL#2 feat: friend관련 기능 추가
momnpa333 May 10, 2024
b105861
KNU-HAEDAL#2 feat: friend 삭제 관련 기능 추가
momnpa333 May 11, 2024
fc1ed07
KNU-HAEDAL#2 feat: friendService 관련 기능 관련 테스트
momnpa333 May 11, 2024
7646cde
KNU-HAEDAL#2 feat: 2차 마무리
momnpa333 May 11, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
HELP.md
.gradle
.yml
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 0. 기초 세팅
### 0.1. 폴더 구조 생성
- entity 폴더
- repository 폴더
- service 폴더
- controller 폴더
### 0.2. h2 db 연결
# 1. 기프티콘 구현
- [x] 엔티티 구현
- [x] 레포지토리 구현
- [x] 서비스 구현
- [x] 컨트롤러 구현
- [ ] 테스트 코드 작성
- [x] API 문서 작성
- [ ] API 테스트
- [x] 예외 처리
- [ ] 예외 처리 테스트
# 2. 회원가입 및 로그인 구현
- [x] 클라이언트- 인가코드 받기
- [x] 클라이언트- 인가 코드를 서버에 넘기기
- [x] 서버- 카카오 엑세스 토큰 받기
- [x] 서버- 카카오 리소스 서버에서 유저 정보 받기
- [x] 서버- 유저 정보를 DB에 저장하기
- [x] 서버- 로그인 구현
- [x] 서버- 회원가입 구현
- [ ] 테스트 코드 작성
- [ ] API 문서 작성
- [ ] API 테스트
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

//security
implementation 'org.springframework.boot:spring-boot-starter-security'

//test
testImplementation 'org.springframework.security:spring-security-test'

}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package team.haedal.gifticionfunding.auth.api;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import team.haedal.gifticionfunding.auth.dto.TokenDto;
import team.haedal.gifticionfunding.auth.service.OAuthService;
import team.haedal.gifticionfunding.auth.service.SecurityService;
import team.haedal.gifticionfunding.dto.user.request.UserCreate;
import team.haedal.gifticionfunding.entity.user.User;
import team.haedal.gifticionfunding.service.user.UserService;

import java.util.Optional;

@RestController
@RequiredArgsConstructor
@Slf4j
public class OAuthController {
private final OAuthService oAuthService;
private final SecurityService securityService;
private final UserService userService;

@GetMapping("/oauth2")
public ResponseEntity<Void> socialLogin(@RequestParam("code") String code){
UserCreate userInfo=oAuthService.getUserInfo(code);
log.info("userInfo: {}",userInfo);
//null일때 exception 처리
Optional<User> user= Optional.ofNullable(userService.SignIn(userInfo).orElseThrow(() ->
new IllegalArgumentException("로그인 실패")));
log.info("user: {}",user.get().getId());
TokenDto tokenDto=securityService.generateTokenDto(user.get().getId());
HttpHeaders headers=securityService.setTokenHeaders(tokenDto);
log.info("로그인 성공");

return ResponseEntity
.status(HttpStatus.OK)
.headers(headers)
.build();
}

}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filter는 configurage가 아닌데 여기 폴더에 있네요
저번주에 저가 지적받은 부분이라 남겨둡ㄴ디ㅏ

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

죄송합니다

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package team.haedal.gifticionfunding.auth.config;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import team.haedal.gifticionfunding.auth.jwt.JwtProvider;

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

@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private final JwtProvider jwtProvider;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
log.info("dofilterinternal 실행");

if (request.getRequestURI().startsWith("/oauth2") || request.getRequestURI().startsWith("/refresh") ||request.getRequestURI().startsWith("/swagger-ui")||request.getRequestURI().startsWith("/api-docs")||request.getRequestURI().startsWith("/v3")) {
log.info("다음필터 실행");

filterChain.doFilter(request, response);
return;
}

//authorization header에서 access token을 추출
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
String accessToken = jwtProvider.parseAccessToken(authHeader);

if (StringUtils.hasText(accessToken) && jwtProvider.validateToken(accessToken)) {
// 일단 USER 권한만 부여
List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
Authentication authentication = new UsernamePasswordAuthenticationToken(jwtProvider.getUserIdFromToken(accessToken), null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
throw new IllegalArgumentException("예상치 못한 토큰 오류");
}

log.info("다음필터 실행");
// 다음 Filter를 실행하기 위한 코드. 마지막 필터라면 필터 실행 후 리소스를 반환한다.
filterChain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package team.haedal.gifticionfunding.auth.config;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import team.haedal.gifticionfunding.global.error.auth.CustomAccessDeniedHandler;
import team.haedal.gifticionfunding.global.error.auth.CustomAuthenticationEntryPoint;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;

import java.util.Arrays;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@Slf4j
public class SecurityConfig {
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
private final JwtAuthorizationFilter jwtAuthorizationFilter;

private static final String[] WHITE_LIST = {
"/auth2",
"/api-docs/**",
"/login/**",
};
private static final String[] AUTHENTICATION_LIST = {
"/api/gifticon"
};


@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.info("필터체인 실행");
http
.csrf(AbstractHttpConfigurer::disable)
.cors(c -> c.configurationSource(corsConfigSource()))
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authReq -> authReq
.requestMatchers(HttpMethod.OPTIONS).permitAll()
.requestMatchers("/", "/login", "/oauth2/**", "/refresh","/swagger-ui/**","/api-docs","/v3/api-docs/**").permitAll()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위에 WHITELIST 정의하셨는데 url 목록을 이걸로 대체할 수 있을꺼 같아요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.anyRequest().authenticated())
.exceptionHandling(e -> e
.authenticationEntryPoint(customAuthenticationEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler))
.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}
@Bean
public CorsConfigurationSource corsConfigSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("*")); // 모든 요청 허용
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); // 모든 HTTP 메서드 허용
configuration.setAllowedHeaders(Arrays.asList("*")); // 모든 헤더 허용
configuration.setExposedHeaders(Arrays.asList("Authorization", "Set-cookie"));
configuration.setAllowCredentials(true); // 쿠키와 같은 자격 증명을 허용

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);

return source;
}
}
15 changes: 15 additions & 0 deletions src/main/java/team/haedal/gifticionfunding/auth/dto/TokenDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package team.haedal.gifticionfunding.auth.dto;

import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@Builder
@RequiredArgsConstructor
public class TokenDto {
private final String grantType;
private final String accessToken;
private final String refreshToken;
private final Long accessTokenExpiresIn;
}
111 changes: 111 additions & 0 deletions src/main/java/team/haedal/gifticionfunding/auth/jwt/JwtProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package team.haedal.gifticionfunding.auth.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import team.haedal.gifticionfunding.auth.dto.TokenDto;
import team.haedal.gifticionfunding.global.error.auth.InvalidTokenException;

@Component
public class JwtProvider {

private final Key key;
private static final String GRANT_TYPE = "Bearer";
private static final long ACCESS_TOKEN_EXPIRES_IN = 1000L * 60 * 30; //access 30분
private static final long REFRESH_TOKEN_EXPIRES_IN = 1000L * 60 * 60 * 24 * 14; //refresh 14일

public JwtProvider(@Value("${jwt.secret}") String secretKey) {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
}

public TokenDto generateTokenDto(Long userId) {

long now = (new Date()).getTime();

String accessToken = generateAccessToken(now, userId);
String refreshToken = generateRefreshToken(now);

return TokenDto.builder()
.grantType(GRANT_TYPE)
.accessToken(accessToken)
.accessTokenExpiresIn(new Date(now + ACCESS_TOKEN_EXPIRES_IN).getTime())
.refreshToken(refreshToken)
.build();
}

public String generateAccessToken(long now, Long userId) {
String accessToken = Jwts.builder()
.setSubject(String.valueOf(userId))
.setExpiration(new Date(now + ACCESS_TOKEN_EXPIRES_IN))
.signWith(key, SignatureAlgorithm.HS512)
.compact();

return accessToken;
}

public String generateRefreshToken(long now) {
String refreshToken = Jwts.builder()
.setExpiration(new Date(now + REFRESH_TOKEN_EXPIRES_IN))
.signWith(key, SignatureAlgorithm.HS512)
.compact();

return refreshToken;
}

public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException | ExpiredJwtException |
UnsupportedJwtException |
IllegalArgumentException e) {
throw e;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw error 하나로 추상화하는거 어떤가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
}

public String parseAccessToken(String authHeader) {
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring("Bearer ".length());
return token;
} else {
throw new InvalidTokenException("AccessToken이 없거나 Bearer type이 아닙니다.");
}
}

public long getUserIdFromToken(String accessToken) {
String userId = parseClaims(accessToken).getSubject();

return Long.parseLong(userId);
}

protected Claims parseClaims(String accessToken) {
try {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(accessToken)
.getBody();
} catch (ExpiredJwtException e) {
throw e;
}
}


public String generateAccessTokenForTest() {
return Jwts.builder()
.setSubject("1")
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRES_IN))
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}
}
Loading