Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ef47a21
Merge pull request #12 from JECT-Study/develop
tlarbals824 Mar 15, 2026
7200690
Merge pull request #18 from JECT-Study/develop
Junhyukkkk Mar 18, 2026
27af347
Merge pull request #20 from JECT-Study/develop
Junhyukkkk Mar 18, 2026
929b3f6
Merge pull request #23 from JECT-Study/develop
Junhyukkkk Mar 19, 2026
a60b4d4
Merge pull request #25 from JECT-Study/develop
Junhyukkkk Mar 19, 2026
f4d4c6e
Merge pull request #27 from JECT-Study/develop
tlarbals824 Mar 22, 2026
f11496e
Merge pull request #29 from JECT-Study/develop
Junhyukkkk Mar 22, 2026
871345f
jwt 의존성 추가, google api 키 추가
KII1ua Mar 29, 2026
d8be6ed
Token클래스 , TokenType 열거형 추가, 토큰 생성 util 추가
KII1ua Mar 30, 2026
0fdc04f
Google 소셜로그인 DTO 생성
KII1ua Mar 30, 2026
cbfa8e6
Google 소셜로그인 OAuth 서비스, 소셜로그인 핸들러 추가
KII1ua Mar 30, 2026
0178a54
SecurityConfig 수정, yml파일 수정
KII1ua Mar 30, 2026
38c94cd
google-yml파일 추적 삭제
KII1ua Mar 30, 2026
862f380
로직 수정
KII1ua Apr 12, 2026
74ecdb6
기존 queryParameter에서 httponly cookie로직 변경
KII1ua Apr 13, 2026
ba0dc59
cookie filter 추가, AuthController 추가 -> accesstoken이 만료됬을 경우 재발급, 그 외 …
KII1ua Apr 13, 2026
851b83e
JwtAuthFilter config 추가 -> 서버에서 accesstoken으로 사용자 식별
KII1ua Apr 14, 2026
915ce4b
refresh token 재발급 정상 실행 확인
KII1ua Apr 14, 2026
eb73913
Refactor token handling: introduce `CookieType` constant, convert `To…
gyumin-sim-rapportlabs Apr 17, 2026
083c299
Add Flyway support: configure dependencies, H2 database, and migratio…
gyumin-sim-rapportlabs Apr 17, 2026
56dfde6
Refactor OAuth2 login: use `CookieUtil.CookieType` for cookie creation
gyumin-sim-rapportlabs Apr 17, 2026
800dfd8
Refactor AuthController: use `CookieUtil.CookieType` constants for im…
gyumin-sim-rapportlabs Apr 17, 2026
e9677d6
Remove unused `@Setter` annotation from `User` entity class
gyumin-sim-rapportlabs Apr 17, 2026
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@ swagger.json
generated-api-client/
node_modules/
package-lock.json

### api key ###
src/main/resources/application-google.yml
src/main/resources/application-dev.yml
7 changes: 7 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,16 @@ dependencies {
implementation(Dependencies.SpringBoot.VALIDATION)
implementation(Dependencies.SpringBoot.WEB)
implementation(Dependencies.SpringBoot.ACTUATOR)
implementation(Dependencies.SpringBoot.OAUTH2_CLIENT)

// Jwt
implementation(Dependencies.Jwt.API)
runtimeOnly(Dependencies.Jwt.IMPL)
runtimeOnly(Dependencies.Jwt.JACKSON)

// Database
runtimeOnly(Dependencies.Database.H2)
implementation(Dependencies.Database.FLYWAY)

// Swagger / OpenAPI
implementation(Dependencies.Swagger.SPRINGDOC)
Expand Down
8 changes: 8 additions & 0 deletions buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ object Dependencies {
const val VALIDATION = "org.springframework.boot:spring-boot-starter-validation"
const val WEB = "org.springframework.boot:spring-boot-starter-web"
const val TEST = "org.springframework.boot:spring-boot-starter-test"
const val OAUTH2_CLIENT = "org.springframework.boot:spring-boot-starter-oauth2-client"
}

object Jwt {
const val API = "io.jsonwebtoken:jjwt-api:0.12.6"
const val IMPL = "io.jsonwebtoken:jjwt-impl:0.12.6"
const val JACKSON = "io.jsonwebtoken:jjwt-jackson:0.12.6"
}

object SpringSecurity {
Expand All @@ -23,6 +30,7 @@ object Dependencies {

object Database {
const val H2 = "com.h2database:h2"
const val FLYWAY = "org.flywaydb:flyway-core"
}

object Test {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/ject/vs/VsServerApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@SpringBootApplication
@ConfigurationPropertiesScan
public class VsServerApplication {

public static void main(String[] args) {
Expand Down
53 changes: 53 additions & 0 deletions src/main/java/com/ject/vs/config/JwtAuthFilter.java
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);
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/ject/vs/config/JwtProperties.java
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) {

}
74 changes: 74 additions & 0 deletions src/main/java/com/ject/vs/config/OAuth2LoginSuccessHandler.java
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 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

소셜 로그인이 동작하는지 테스트해보셨을까요?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

넵 테스트 했을때 토큰값도 정상적으로 쿼리에 잘 들어가는것까지 확인했습니다.


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";
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

혹시 해당 변수는 어디에서 사용되는걸까요?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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);
}
}
9 changes: 7 additions & 2 deletions src/main/java/com/ject/vs/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
public SecurityFilterChain securityFilterChain(HttpSecurity http, OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
Expand All @@ -19,10 +19,15 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
"/swagger-ui/**",
"/swagger-ui.html",
"/api/**",
"/actuator/health"
"/actuator/health",
"/",
"/error",
"/auth/reissue"
).permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.successHandler(oAuth2LoginSuccessHandler))
.build();
}
}
49 changes: 49 additions & 0 deletions src/main/java/com/ject/vs/controller/AuthController.java
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();
}
}
2 changes: 2 additions & 0 deletions src/main/java/com/ject/vs/controller/HelloController.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.ject.vs.controller;

import com.ject.vs.dto.HelloResponse;
import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

Expand Down
43 changes: 43 additions & 0 deletions src/main/java/com/ject/vs/domain/Token.java
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;
}
}
6 changes: 6 additions & 0 deletions src/main/java/com/ject/vs/domain/TokenType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.ject.vs.domain;

public enum TokenType {
ACCESS,
REFRESH
}
15 changes: 15 additions & 0 deletions src/main/java/com/ject/vs/domain/User.java
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
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Setter는 선언하지 않는게 좋아보여요! 만일 값이 수정되어야한다면 목적이 들어나는 함수를 정의하도록 하는게 좋아보여요

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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;
// 아직 유저에 대한 정보 확정 아님
}
13 changes: 13 additions & 0 deletions src/main/java/com/ject/vs/dto/LoginTokenResponse.java
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 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Record를 활용할 수 있어보여요!

private Long userId;
private String accessToken;
private String refreshToken;
}

Loading