-
Notifications
You must be signed in to change notification settings - Fork 4
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
base: main
Are you sure you want to change the base?
2주차 다운 #10
Changes from all commits
642778c
7b7e7bc
0fdda69
c06f126
5a5d980
1361488
e3c8124
1658d32
ee1166d
6bb70b7
e539571
2db300f
43d3ab6
9d7daa0
ac6ad8a
5f014a0
6d7eb27
76b91f2
4df947c
664ddc6
1463897
a943d80
4241c23
9657c9b
6d66ed9
bd2dc67
cd4c616
16a2b3e
3c0d089
0daa5af
6a7a6e4
4f0c875
b105861
fc1ed07
7646cde
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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/ | ||
|
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 테스트 |
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(); | ||
} | ||
|
||
} |
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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 위에 WHITELIST 정의하셨는데 url 목록을 이걸로 대체할 수 있을꺼 같아요 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} |
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; | ||
} |
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. throw error 하나로 추상화하는거 어떤가요? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filter는 configurage가 아닌데 여기 폴더에 있네요
저번주에 저가 지적받은 부분이라 남겨둡ㄴ디ㅏ
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
죄송합니다