-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/ 소셜 로그인 구현, Filter 내용 추가 #35
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
Changes from all commits
ef47a21
7200690
27af347
929b3f6
a60b4d4
f4d4c6e
f11496e
871345f
d8be6ed
0fdc04f
cbfa8e6
0178a54
38c94cd
862f380
74ecdb6
ba0dc59
851b83e
915ce4b
eb73913
083c299
56dfde6
800dfd8
e9677d6
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 |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package com.ject.vs.config; | ||
|
|
||
| import com.ject.vs.util.CookieUtil; | ||
| import com.ject.vs.util.JwtProvider; | ||
| import jakarta.servlet.FilterChain; | ||
| import jakarta.servlet.ServletException; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import lombok.NonNull; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
| import org.springframework.security.core.authority.AuthorityUtils; | ||
| 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; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class JwtAuthFilter extends OncePerRequestFilter { | ||
| private final JwtProvider jwtProvider; | ||
| private final CookieUtil cookieUtil; | ||
|
|
||
| @Override | ||
| protected void doFilterInternal(@NonNull HttpServletRequest request, | ||
| @NonNull HttpServletResponse response, | ||
| @NonNull FilterChain filterChain) throws ServletException, IOException { | ||
|
|
||
| String accessToken = cookieUtil.getCookieValue(request, CookieUtil.CookieType.ACCESS_TOKEN); | ||
|
|
||
| var tokenInfo = jwtProvider.parseToken(accessToken); | ||
|
|
||
| if (accessToken != null | ||
| && tokenInfo.isAccessToken() | ||
| && SecurityContextHolder.getContext().getAuthentication() == null) { | ||
|
|
||
|
|
||
| UsernamePasswordAuthenticationToken authentication = | ||
| new UsernamePasswordAuthenticationToken( | ||
| tokenInfo.userId(), | ||
| null, | ||
| AuthorityUtils.NO_AUTHORITIES | ||
| ); | ||
|
|
||
| authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); | ||
| SecurityContextHolder.getContext().setAuthentication(authentication); | ||
| } | ||
|
|
||
| filterChain.doFilter(request, response); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.ject.vs.config; | ||
|
|
||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||
|
|
||
| @ConfigurationProperties(prefix = "app.jwt") | ||
| public record JwtProperties ( | ||
| String secret, | ||
| long accessTokenExpirationSeconds, | ||
| long refreshTokenExpirationSeconds) { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| package com.ject.vs.config; | ||
|
|
||
| import com.ject.vs.dto.LoginTokenResponse; | ||
| import com.ject.vs.dto.OAuthAttributes; | ||
| import com.ject.vs.service.AuthService; | ||
| import com.ject.vs.util.CookieUtil; | ||
| import jakarta.servlet.ServletException; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.http.HttpHeaders; | ||
| import org.springframework.http.ResponseCookie; | ||
| import org.springframework.security.core.Authentication; | ||
| import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; | ||
| import org.springframework.security.oauth2.core.user.OAuth2User; | ||
| import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { | ||
|
|
||
| private final AuthService authService; | ||
|
|
||
| @Value("${app.oauth2.redirect-success-url}") | ||
| private String redirectSuccessUrl; | ||
|
|
||
| @Value("${app.cookie.secure:false}") // 운영 상황에서는 true로 변경 https 사용할 경우 | ||
| private boolean secureCookie; | ||
|
|
||
| @Override | ||
| public void onAuthenticationSuccess(HttpServletRequest request, | ||
| HttpServletResponse response, | ||
| Authentication authentication) throws IOException, ServletException { | ||
|
|
||
| OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication; | ||
| OAuth2User oauth2User = (OAuth2User) authentication.getPrincipal(); | ||
|
|
||
| String registrationId = oauthToken.getAuthorizedClientRegistrationId(); | ||
| String userNameAttributeName = "sub"; | ||
|
Collaborator
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. 혹시 해당 변수는 어디에서 사용되는걸까요?
Collaborator
Author
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. 구글 소셜로그인에서는 유저마다 구분하는 키값이 sub 값으로 구분해서 해당 변수를 사용해서 키값으로 지정할 생각이였습니다. |
||
|
|
||
| OAuthAttributes attributes = OAuthAttributes.of( | ||
| registrationId, | ||
| userNameAttributeName, | ||
| oauth2User.getAttributes() | ||
| ); | ||
|
|
||
| LoginTokenResponse tokenResponse = authService.socialLogin(attributes.getSub()); | ||
|
|
||
| ResponseCookie accessTokenCookie = ResponseCookie.from(CookieUtil.CookieType.ACCESS_TOKEN, tokenResponse.getAccessToken()) | ||
| .httpOnly(true) | ||
| .secure(secureCookie) | ||
| .path("/") | ||
| .sameSite("Lax") | ||
| .maxAge(60 * 30) | ||
| .build(); | ||
|
|
||
| ResponseCookie refreshTokenCookie = ResponseCookie.from(CookieUtil.CookieType.REFRESH_TOKEN, tokenResponse.getRefreshToken()) | ||
| .httpOnly(true) | ||
| .secure(secureCookie) | ||
| .path("/") | ||
| .sameSite("Lax") | ||
| .maxAge(60 * 60 * 24 * 14) | ||
| .build(); | ||
|
|
||
| response.addHeader(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()); | ||
| response.addHeader(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()); | ||
|
|
||
| getRedirectStrategy().sendRedirect(request, response, redirectSuccessUrl); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package com.ject.vs.controller; | ||
|
|
||
| import com.ject.vs.dto.TokenInfo; | ||
| import com.ject.vs.service.AuthService; | ||
| import com.ject.vs.util.CookieUtil; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.http.HttpHeaders; | ||
| import org.springframework.http.ResponseCookie; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| public class AuthController { | ||
| private final AuthService authService; | ||
| private final CookieUtil cookieUtil; | ||
|
|
||
| @Value("${app.cookie.secure:true}") | ||
| private boolean secureCookie; | ||
|
|
||
| @PostMapping("/auth/reissue") | ||
| public ResponseEntity<Void> reissue(HttpServletRequest request, HttpServletResponse response) { | ||
| String refreshToken = cookieUtil.getCookieValue( | ||
| request, | ||
| CookieUtil.CookieType.REFRESH_TOKEN | ||
| ); | ||
|
|
||
| TokenInfo newAccessTokenInfo = authService.reissueAccessToken(refreshToken); | ||
|
|
||
| ResponseCookie accessTokenCookie = ResponseCookie.from( | ||
| CookieUtil.CookieType.ACCESS_TOKEN, | ||
| newAccessTokenInfo.tokenValue() | ||
| ) | ||
| .httpOnly(true) | ||
| .secure(secureCookie) | ||
| .path("/") | ||
| .sameSite("None") | ||
| .maxAge(60 * 30) | ||
| .build(); | ||
|
|
||
| response.addHeader(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()); | ||
|
|
||
| return ResponseEntity.ok().build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| package com.ject.vs.domain; | ||
|
|
||
| import jakarta.persistence.*; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @NoArgsConstructor | ||
| @Table(name = "Token", indexes = @Index(name = "idx_token_value", columnList = "tokenValue")) | ||
| public class Token { | ||
| @Id @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| private User user; | ||
|
|
||
| @Column(length = 1024) | ||
| private String tokenValue; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| private TokenType tokenType; | ||
|
|
||
| private LocalDateTime expiresAt; | ||
|
|
||
| private boolean revoked; | ||
|
|
||
| @Builder | ||
| public Token(User user, String tokenValue, TokenType tokenType, LocalDateTime expiresAt, boolean revoked) { | ||
| this.user = user; | ||
| this.tokenValue = tokenValue; | ||
| this.tokenType = tokenType; | ||
| this.expiresAt = expiresAt; | ||
| this.revoked = revoked; | ||
| } | ||
|
|
||
| public void revoke() { | ||
| this.revoked = true; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package com.ject.vs.domain; | ||
|
|
||
| public enum TokenType { | ||
| ACCESS, | ||
| REFRESH | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package com.ject.vs.domain; | ||
|
|
||
| import jakarta.persistence.*; | ||
| import lombok.Getter; | ||
| import lombok.Setter; | ||
|
|
||
| @Entity | ||
| @Getter | ||
|
Collaborator
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. Setter는 선언하지 않는게 좋아보여요! 만일 값이 수정되어야한다면 목적이 들어나는 함수를 정의하도록 하는게 좋아보여요
Collaborator
Author
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. 넵 수정하겠습니다. |
||
| @Table(name = "users") | ||
| public class User { | ||
| @Id @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
| private String sub; | ||
| // 아직 유저에 대한 정보 확정 아님 | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package com.ject.vs.dto; | ||
|
|
||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| @Builder | ||
| public class LoginTokenResponse { | ||
|
Collaborator
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. Record를 활용할 수 있어보여요! |
||
| private Long userId; | ||
| private String accessToken; | ||
| private String refreshToken; | ||
| } | ||
|
|
||
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.
소셜 로그인이 동작하는지 테스트해보셨을까요?
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.
넵 테스트 했을때 토큰값도 정상적으로 쿼리에 잘 들어가는것까지 확인했습니다.