From 6f1058ca64e973651dc3b5c75c648bd6353ed965 Mon Sep 17 00:00:00 2001 From: user040131 Date: Tue, 11 Nov 2025 20:36:58 +0900 Subject: [PATCH 1/4] =?UTF-8?q?6=EC=A3=BC=EC=B0=A8=20=EA=B3=BC=EC=A0=9C=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../blogapplication/config/TokenProvider.java | 37 ++++++++++ .../config/WebSecurityConfig.java | 31 +++++++- .../controller/AuthController.java | 3 - .../controller/KakaoLoginController.java | 8 ++ .../blogapplication/domain/RefreshToken.java | 23 +++++- .../leets/blogapplication/domain/User.java | 52 ++++++------- .../blogapplication/domain/UserSocial.java | 65 +++++++++++++++++ .../handler/KakaoLoginSuccessHandler.java | 55 ++++++++++++++ .../repository/RefreshTokenRepository.java | 4 + .../repository/UserSocialRepository.java | 11 +++ .../service/auth/TokenService.java | 27 ++++++- .../auth/kakao/KakaoOAuth2UserService.java | 73 +++++++++++++++++++ src/main/resources/application.properties | 18 +++++ src/main/resources/application.yml | 18 +++++ 15 files changed, 390 insertions(+), 36 deletions(-) create mode 100644 src/main/java/leets/blogapplication/controller/KakaoLoginController.java create mode 100644 src/main/java/leets/blogapplication/domain/UserSocial.java create mode 100644 src/main/java/leets/blogapplication/handler/KakaoLoginSuccessHandler.java create mode 100644 src/main/java/leets/blogapplication/repository/UserSocialRepository.java create mode 100644 src/main/java/leets/blogapplication/service/auth/kakao/KakaoOAuth2UserService.java diff --git a/build.gradle b/build.gradle index 6b5b8d6..256bf73 100644 --- a/build.gradle +++ b/build.gradle @@ -35,6 +35,7 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' } tasks.named('test') { diff --git a/src/main/java/leets/blogapplication/config/TokenProvider.java b/src/main/java/leets/blogapplication/config/TokenProvider.java index 314c8b0..47d376d 100644 --- a/src/main/java/leets/blogapplication/config/TokenProvider.java +++ b/src/main/java/leets/blogapplication/config/TokenProvider.java @@ -68,6 +68,24 @@ public String generateAccessToken(String email, Long userId, Duration ttl) { .compact(); } + public String generateAccessToken(Long userId, Duration ttl) { + Date now = new Date(); + Date exp = new Date(now.getTime() + ttl.toMillis()); + + Map claims = new HashMap<>(); + claims.put("id", userId); + claims.put("tokenType", "accessToken"); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setIssuer(props.getIssuer()) + .setIssuedAt(now) + .setExpiration(exp) + .addClaims(claims) + .signWith(hmacKey, SignatureAlgorithm.HS256) + .compact(); + } + public String generateRefreshToken(String email, Long userId, Duration ttl) { Date now = new Date(); Date exp = new Date(now.getTime() + ttl.toMillis()); @@ -87,6 +105,25 @@ public String generateRefreshToken(String email, Long userId, Duration ttl) { .compact(); } + public String generateRefreshToken(Long userId, Duration ttl) { + Date now = new Date(); + Date exp = new Date(now.getTime() + ttl.toMillis()); + + Map claims = new HashMap<>(); + claims.put("id", userId); + claims.put("tokenType", "refreshToken"); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setIssuer(props.getIssuer()) + .setIssuedAt(now) + .setExpiration(exp) + .addClaims(claims) + .signWith(hmacKey, SignatureAlgorithm.HS256) + .compact(); + } + + public String createNewAccessTokenFromRefresh(String refreshToken, Duration accessTtl) { Claims claims = parseClaims(refreshToken); String tokenType = claims.get("tokenType", String.class); diff --git a/src/main/java/leets/blogapplication/config/WebSecurityConfig.java b/src/main/java/leets/blogapplication/config/WebSecurityConfig.java index 17e29d1..818d57a 100644 --- a/src/main/java/leets/blogapplication/config/WebSecurityConfig.java +++ b/src/main/java/leets/blogapplication/config/WebSecurityConfig.java @@ -1,11 +1,15 @@ package leets.blogapplication.config; +import leets.blogapplication.handler.KakaoLoginSuccessHandler; +import leets.blogapplication.service.auth.kakao.KakaoOAuth2UserService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -13,13 +17,34 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration +@EnableWebSecurity public class WebSecurityConfig { //spring security의 정책, 필터 설정 private final TokenProvider tokenProvider; + private final KakaoOAuth2UserService kakaoOAuth2UserService; + private final KakaoLoginSuccessHandler kakaoLoginSuccessHandler; - public WebSecurityConfig(TokenProvider tokenProvider) { + public WebSecurityConfig(TokenProvider tokenProvider, KakaoOAuth2UserService kakaoOAuth2UserService, KakaoLoginSuccessHandler kakaoLoginSuccessHandler) { this.tokenProvider = tokenProvider; + this.kakaoOAuth2UserService = kakaoOAuth2UserService; + this.kakaoLoginSuccessHandler = kakaoLoginSuccessHandler; + } + + // 체인 1: OAuth2 로그인 전용(세션 필요) + @Bean @Order(1) + SecurityFilterChain oauth2Chain(HttpSecurity http) throws Exception { + http.securityMatcher("/oauth2/**", "/login/**") + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(a -> a.anyRequest().permitAll()) + .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) + // ❌ 여기엔 토큰필터 넣지 말 것 + .oauth2Login(o -> o + .userInfoEndpoint(u -> u.userService(kakaoOAuth2UserService)) + .successHandler(kakaoLoginSuccessHandler) // or .defaultSuccessUrl("/oauth/signed-in", true) + .failureUrl("/login?error") // 에러 확인용 + ); + return http.build(); } @Bean @@ -28,6 +53,7 @@ public TokenAuthenticationFilter tokenAuthenticationFilter() { } @Bean + @Order(2) public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .csrf(AbstractHttpConfigurer::disable) @@ -36,7 +62,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .httpBasic(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth - .requestMatchers("/auth/login", "/auth/signup").permitAll() + .requestMatchers("/auth/login", "/auth/signup", "/oauth2/**","/login/**","/error").permitAll() .requestMatchers("/auth/logout", "/comments/**", "/post/**", "/posts/**").authenticated() .anyRequest().authenticated() // 나머지 보호하려면 이렇게. 전부 공개면 permitAll ) @@ -45,7 +71,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // SecurityContextHolder에서 유저 정보 빼오기 위해서 필수 add 코드 .addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) - .build(); } diff --git a/src/main/java/leets/blogapplication/controller/AuthController.java b/src/main/java/leets/blogapplication/controller/AuthController.java index b1c9e00..19f5f25 100644 --- a/src/main/java/leets/blogapplication/controller/AuthController.java +++ b/src/main/java/leets/blogapplication/controller/AuthController.java @@ -10,13 +10,10 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.RestController; -import javax.naming.AuthenticationNotSupportedException; import java.time.Duration; @RestController diff --git a/src/main/java/leets/blogapplication/controller/KakaoLoginController.java b/src/main/java/leets/blogapplication/controller/KakaoLoginController.java new file mode 100644 index 0000000..f17bee8 --- /dev/null +++ b/src/main/java/leets/blogapplication/controller/KakaoLoginController.java @@ -0,0 +1,8 @@ +package leets.blogapplication.controller; + +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class KakaoLoginController { + +} diff --git a/src/main/java/leets/blogapplication/domain/RefreshToken.java b/src/main/java/leets/blogapplication/domain/RefreshToken.java index ab92b1b..fa3b92c 100644 --- a/src/main/java/leets/blogapplication/domain/RefreshToken.java +++ b/src/main/java/leets/blogapplication/domain/RefreshToken.java @@ -2,7 +2,6 @@ import jakarta.persistence.*; -import java.time.Duration; import java.time.LocalDateTime; @Entity @@ -31,6 +30,10 @@ public class RefreshToken { @JoinColumn(name = "user_id", nullable = false) private User user; + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_social_id", nullable = false) + private UserSocial userSocial; + public void setUser(User user) { this.user = user; } public void setRevoked(boolean revoked) { this.revoked = revoked; } @@ -42,13 +45,27 @@ public RefreshToken(User user, String token, LocalDateTime expiresAt, LocalDateT this.user = user; } + public RefreshToken(UserSocial userSocial, String token, LocalDateTime expiresAt, LocalDateTime issuedAt, boolean revoked) { + this.refreshToken = token; + this.expiresAt = expiresAt; + this.issuedAt = issuedAt; + this.revoked = revoked; + this.userSocial = userSocial; + } + public static RefreshToken createRefreshToken(String token, User user){ RefreshToken ref = new RefreshToken(user, token, LocalDateTime.now().plusDays(7), LocalDateTime.now(), false); return ref; } + public static RefreshToken createRefreshToken(String token, UserSocial userSocial){ + RefreshToken ref = new RefreshToken(userSocial, token, LocalDateTime.now(), LocalDateTime.now(), false); + return ref; + } + protected RefreshToken() {} - public Long getUserId() { return user.getId(); - } + public Long getUserId() { return user.getId(); } + public Long getSocialUserId() { return userSocial.getId(); } + public void setRefreshToken(String refreshToken) { this.refreshToken = refreshToken; } } diff --git a/src/main/java/leets/blogapplication/domain/User.java b/src/main/java/leets/blogapplication/domain/User.java index 875d1f4..781d71e 100644 --- a/src/main/java/leets/blogapplication/domain/User.java +++ b/src/main/java/leets/blogapplication/domain/User.java @@ -6,6 +6,7 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; @@ -35,18 +36,14 @@ public class User implements UserDetails { @Column(length=500) private String profileImage; - //회원가입 방식 및 비밀번호 - @Enumerated(EnumType.STRING) - @Column(nullable=false, length=10) - private Provider provider; // EMAIL or KAKAO @Column(nullable=false) private LocalDateTime createdAt; @Column(nullable=true) private LocalDateTime updatedAt; @Column - private String providerUserId; // KAKAO - @Column private String password; // EMAIL + @Column(name = "display_name", nullable = false) + private String displayName; @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private List refreshTokens = new ArrayList<>(); @@ -67,21 +64,13 @@ public void removeToken(RefreshToken ref) { ref.setRevoked(false); } - public static User createUserWithKakao(String email, String name, LocalDate birthdate, - String nickname, String intro, String profileImage, Provider provider, - String providerUserId) { - User user = new User(); - user.status = UserStatus.ACTIVE; - user.email = email; - user.name = name; - user.birthdate = birthdate; - user.nickname = nickname; - user.intro = intro; - user.profileImage = profileImage; - user.provider = Provider.KAKAO; - user.createdAt = LocalDateTime.now(); - user.providerUserId = providerUserId; - return user; + public static User newSocial(String email, String displayName) { + User u = new User(); + u.setEmail(email); + u.setPassword(null); + u.setDisplayName(displayName != null ? displayName : "KakaoUser"); + u.setCreatedAt(LocalDateTime.now()); + return u; } public static User createUserWithEmail(String email, String name, LocalDate birthdate, @@ -94,13 +83,12 @@ public static User createUserWithEmail(String email, String name, LocalDate birt user.nickname = nickname; user.intro = intro; user.profileImage = profileImage; - user.provider = Provider.EMAIL; user.createdAt = LocalDateTime.now(); user.password = password; return user; } - public void updateEmailUser(String nickname, String intro, String email, String password, String name, + public void updateUser(String nickname, String intro, String email, String password, String name, LocalDate birthdate) { this.nickname = nickname; this.intro = intro; @@ -111,9 +99,6 @@ public void updateEmailUser(String nickname, String intro, String email, String this.updatedAt = LocalDateTime.now(); } -// public void updateKakaoUser() {} - //카카오는 대체 뭘 변경한다는 건지 모르겟어서 일단 주석처리 했습니다 - protected User () {} // ====== 접근자 ====== @@ -133,5 +118,20 @@ public Collection getAuthorities() { public String getUsername() { return id.toString(); } public String getEmail() { return email; } + + // getters/setters + public void setId(Long id) { this.id = id; } + + public void setEmail(String email) { this.email = email; } + + public void setPassword(String password) { this.password = password; } + + public String getDisplayName() { return displayName; } + public void setDisplayName(String displayName) { this.displayName = displayName; } + + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + + public void setNickname(String nickname) { this.nickname = nickname; } } diff --git a/src/main/java/leets/blogapplication/domain/UserSocial.java b/src/main/java/leets/blogapplication/domain/UserSocial.java new file mode 100644 index 0000000..c3f1fc5 --- /dev/null +++ b/src/main/java/leets/blogapplication/domain/UserSocial.java @@ -0,0 +1,65 @@ +package leets.blogapplication.domain; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "user_social", + uniqueConstraints = @UniqueConstraint(columnNames = {"provider", "provider_user_id"})) +public class UserSocial { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String profile_nickname; + + @Column(name = "provider_user_id", nullable = false, length = 64) + private Long providerUserId; + + @Column(nullable = false, length = 20) + private String provider = "KAKAO"; + + @OneToOne + @JoinColumn(name = "user_id", unique = true) + private User user; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + public UserSocial() {} + + public static UserSocial link(Long userId, String profile_nickname, Long providerUserId) { + UserSocial us = new UserSocial(); + User u = new User(); + u.setId(userId); // 레퍼런스만 set + u.setNickname(profile_nickname); + us.setProviderUserId(providerUserId); + return us; + } + + // getters/setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getProfile_nickname() { return profile_nickname; } + public void setProfile_nickname(String profile_nickname) {} + + public Long getProviderUserId() { return providerUserId; } + public void setProviderUserId(Long providerUserId) { this.providerUserId = providerUserId; } + + public User getUser() { return user; + } + + public void setNickname(String nickname) { this.profile_nickname = nickname; } +} + diff --git a/src/main/java/leets/blogapplication/handler/KakaoLoginSuccessHandler.java b/src/main/java/leets/blogapplication/handler/KakaoLoginSuccessHandler.java new file mode 100644 index 0000000..944f2c4 --- /dev/null +++ b/src/main/java/leets/blogapplication/handler/KakaoLoginSuccessHandler.java @@ -0,0 +1,55 @@ +package leets.blogapplication.handler; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import leets.blogapplication.service.auth.RefreshTokenService; +import leets.blogapplication.service.auth.TokenService; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.time.Duration; + +@Component +public class KakaoLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final TokenService tokenService; + private final RefreshTokenService refreshService; + + public KakaoLoginSuccessHandler(TokenService tokenService, RefreshTokenService refreshService) { + this.tokenService = tokenService; + this.refreshService = refreshService; + } + + @Override + public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse res, Authentication auth) + throws IOException { + OAuth2User principal = (OAuth2User) auth.getPrincipal(); + Object raw = principal.getAttribute("id"); + long kakaoId = ((Number) raw).longValue(); // 실패 시 ClassCastException로 바로 발견 + + String refresh = tokenService.createNewRefreshToken(kakaoId); + String access = tokenService.createNewAccessTokenSocial(refresh); + + res.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + access); + res.setHeader("Access-Control-Expose-Headers", "Authorization"); + + ResponseCookie cookie = ResponseCookie.from("REFRESH_TOKEN", refresh) + .httpOnly(true) + .secure(true) // 운영 HTTPS 필수 + .sameSite("Lax") // 프론트/백 도메인 분리면 "None" + .path("/") + .maxAge(Duration.ofDays(30)) + .build(); + res.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); + + res.setStatus(HttpServletResponse.SC_OK); + res.setContentType("application/json;charset=UTF-8"); + res.getWriter().write("{\"accessToken\":\"" + access + "\"}"); + clearAuthenticationAttributes(req); + } +} diff --git a/src/main/java/leets/blogapplication/repository/RefreshTokenRepository.java b/src/main/java/leets/blogapplication/repository/RefreshTokenRepository.java index 739a164..2f23c79 100644 --- a/src/main/java/leets/blogapplication/repository/RefreshTokenRepository.java +++ b/src/main/java/leets/blogapplication/repository/RefreshTokenRepository.java @@ -2,10 +2,14 @@ import leets.blogapplication.domain.RefreshToken; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; public interface RefreshTokenRepository extends JpaRepository { Optional findByRefreshToken(String refreshToken); + + //추후에 revoked는 false이고, 만료되지 않은 토큰으로 찾아오도록 하는 쿼리 추가 Optional findByUser_Id(Long userId); } diff --git a/src/main/java/leets/blogapplication/repository/UserSocialRepository.java b/src/main/java/leets/blogapplication/repository/UserSocialRepository.java new file mode 100644 index 0000000..5d342f5 --- /dev/null +++ b/src/main/java/leets/blogapplication/repository/UserSocialRepository.java @@ -0,0 +1,11 @@ +package leets.blogapplication.repository; + +import leets.blogapplication.domain.UserSocial; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserSocialRepository extends JpaRepository { + Optional findByProviderAndProviderUserId(String provider, Long providerUserId); + Optional findByProviderUserId(Long providerUserId); +} diff --git a/src/main/java/leets/blogapplication/service/auth/TokenService.java b/src/main/java/leets/blogapplication/service/auth/TokenService.java index f81c7b6..050efc0 100644 --- a/src/main/java/leets/blogapplication/service/auth/TokenService.java +++ b/src/main/java/leets/blogapplication/service/auth/TokenService.java @@ -3,8 +3,10 @@ import leets.blogapplication.config.TokenProvider; import leets.blogapplication.domain.RefreshToken; import leets.blogapplication.domain.User; +import leets.blogapplication.domain.UserSocial; import leets.blogapplication.repository.RefreshTokenRepository; import leets.blogapplication.repository.UserRepository; +import leets.blogapplication.repository.UserSocialRepository; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; @@ -17,13 +19,15 @@ public class TokenService { private final RefreshTokenService refreshTokenService; private final UserRepository accountRepository; private final RefreshTokenRepository refreshTokenRepository; + private final UserSocialRepository userSocialRepository; public TokenService(TokenProvider tokenProvider, RefreshTokenService refreshTokenService, - UserRepository accountRepository, RefreshTokenRepository refreshTokenRepository) { + UserRepository accountRepository, RefreshTokenRepository refreshTokenRepository, UserSocialRepository userSocialRepository) { this.tokenProvider = tokenProvider; this.refreshTokenService = refreshTokenService; this.accountRepository = accountRepository; this.refreshTokenRepository = refreshTokenRepository; + this.userSocialRepository = userSocialRepository; } @@ -40,6 +44,19 @@ public String createNewAccessToken(String refreshToken) { //첫 argument로 들어가는 놈은 무조건 extends UserDetails를 한 놈만 됨 } + public String createNewAccessTokenSocial(String refreshToken) { + if(!tokenProvider.validToken(refreshToken)) { + throw new IllegalArgumentException("Invalid refresh token"); + } + + Long userId = refreshTokenService.findByRefreshToken(refreshToken).getSocialUserId(); + UserSocial user = userSocialRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("access Account not found")); + + return tokenProvider.generateAccessToken(userId, Duration.ofHours(2)); + //첫 argument로 들어가는 놈은 무조건 extends UserDetails를 한 놈만 됨 + } + public String createNewRefreshToken(String email) { User user = accountRepository.findByEmail(email) .orElseThrow(() -> new IllegalArgumentException("refresh Account not found")); @@ -48,6 +65,14 @@ public String createNewRefreshToken(String email) { return ref; } + public String createNewRefreshToken(Long userId) { + UserSocial user = userSocialRepository.findByProviderUserId(userId) + .orElseThrow(() -> new IllegalArgumentException("refresh Account not found")); + String ref = tokenProvider.generateRefreshToken(user.getId(), Duration.ofDays(1)); + refreshTokenRepository.save(RefreshToken.createRefreshToken(ref, user)); + return ref; + } + public User getUserFromAccessToken() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); String token = auth.getCredentials().toString(); diff --git a/src/main/java/leets/blogapplication/service/auth/kakao/KakaoOAuth2UserService.java b/src/main/java/leets/blogapplication/service/auth/kakao/KakaoOAuth2UserService.java new file mode 100644 index 0000000..fcdb57d --- /dev/null +++ b/src/main/java/leets/blogapplication/service/auth/kakao/KakaoOAuth2UserService.java @@ -0,0 +1,73 @@ +package leets.blogapplication.service.auth.kakao; + +import leets.blogapplication.domain.User; +import leets.blogapplication.domain.UserSocial; +import leets.blogapplication.repository.UserRepository; +import leets.blogapplication.repository.UserSocialRepository; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +@Service +public class KakaoOAuth2UserService implements OAuth2UserService { + private final UserSocialRepository socialRepo; + + public KakaoOAuth2UserService(UserSocialRepository socialRepo) { + this.socialRepo = socialRepo; + } + + private final OAuth2UserService delegate = new DefaultOAuth2UserService(); + + @Transactional + @Override + public OAuth2User loadUser(OAuth2UserRequest req) throws OAuth2AuthenticationException { + OAuth2User o = delegate.loadUser(req); // 카카오 userinfo 호출 + + Map attr = o.getAttributes(); + Long kakaoId = (attr.get("id") instanceof Number n) + ? n.longValue() + : Long.parseLong(String.valueOf(attr.get("id")).trim()); // providerUserId + String nickname; + Object acc = attr.get("kakao_account"); + if (acc instanceof Map) { + Object profile = ((Map) acc).get("profile"); + if (profile instanceof Map) nickname = (String) ((Map) profile).get("nickname"); + else { + nickname = null; + } + } else { + nickname = null; + } + + UserSocial social = socialRepo + .findByProviderAndProviderUserId("KAKAO", kakaoId) + .orElseGet(() -> socialRepo.save( + buildNewSocial(kakaoId, nickname))); + + // 소셜 id를 principal로 사용 + Collection auth = List.of(new SimpleGrantedAuthority("ROLE_USER")); + return new DefaultOAuth2User(auth, attr, "id") { + public Long getSocialId() { return social.getId(); } + }; + } + + private UserSocial buildNewSocial(Long subject, String nickname) { + UserSocial s = new UserSocial(); + s.setProviderUserId(subject); + s.setNickname(nickname); + return s; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d21de9a..d543497 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,19 @@ spring.application.name=BlogApplication + +#REST API KEY +kakao.login.api_key=67fa70c37870452229b26c75c181e4fe + +#client-secret +kakao.login.client_secret=${JWT_SECRET_KEY} + +# code +kakao.login.redirect_uri=http://localhost:8080/oauth/kakao/code +kakao.login.uri.code=/oauth/authorize +kakao.login.uri.base=https://kauth.kakao.com + +# get token +kakao.login.uri.token=/oauth/token + +# kakao login +kakao.api.uri.base=https://kapi.kakao.com +kakao.api.uri.user=/v2/user/me \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 15e6e38..ad2bf35 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,6 +2,24 @@ jwt: issuer: leets.blog secret: ${JWT_SECRET_KEY} spring: + security: + oauth2: + client: + registration: + kakao: + client-id: f2da5fb562d9d7032c7d8f189505e0c4 + client-secret: JuwvLMOMai27uqp91DjHZCAPtS0N99vH + client-authentication-method: client_secret_post + authorization-grant-type: authorization_code + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + scope: profile_nickname + client-name: Kakao + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id thymeleaf: cache: false datasource: From 002b250fc185569d5ed7372ca99b7fcb34b28139 Mon Sep 17 00:00:00 2001 From: user040131 Date: Thu, 13 Nov 2025 16:20:50 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../leets/blogapplication/config/WebSecurityConfig.java | 3 --- .../blogapplication/controller/KakaoLoginController.java | 8 -------- .../blogapplication/handler/KakaoLoginSuccessHandler.java | 4 +--- .../service/auth/kakao/KakaoOAuth2UserService.java | 4 ---- 4 files changed, 1 insertion(+), 18 deletions(-) delete mode 100644 src/main/java/leets/blogapplication/controller/KakaoLoginController.java diff --git a/src/main/java/leets/blogapplication/config/WebSecurityConfig.java b/src/main/java/leets/blogapplication/config/WebSecurityConfig.java index 818d57a..e98ddac 100644 --- a/src/main/java/leets/blogapplication/config/WebSecurityConfig.java +++ b/src/main/java/leets/blogapplication/config/WebSecurityConfig.java @@ -5,9 +5,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; diff --git a/src/main/java/leets/blogapplication/controller/KakaoLoginController.java b/src/main/java/leets/blogapplication/controller/KakaoLoginController.java deleted file mode 100644 index f17bee8..0000000 --- a/src/main/java/leets/blogapplication/controller/KakaoLoginController.java +++ /dev/null @@ -1,8 +0,0 @@ -package leets.blogapplication.controller; - -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class KakaoLoginController { - -} diff --git a/src/main/java/leets/blogapplication/handler/KakaoLoginSuccessHandler.java b/src/main/java/leets/blogapplication/handler/KakaoLoginSuccessHandler.java index 944f2c4..5152c95 100644 --- a/src/main/java/leets/blogapplication/handler/KakaoLoginSuccessHandler.java +++ b/src/main/java/leets/blogapplication/handler/KakaoLoginSuccessHandler.java @@ -18,11 +18,9 @@ public class KakaoLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final TokenService tokenService; - private final RefreshTokenService refreshService; - public KakaoLoginSuccessHandler(TokenService tokenService, RefreshTokenService refreshService) { + public KakaoLoginSuccessHandler(TokenService tokenService) { this.tokenService = tokenService; - this.refreshService = refreshService; } @Override diff --git a/src/main/java/leets/blogapplication/service/auth/kakao/KakaoOAuth2UserService.java b/src/main/java/leets/blogapplication/service/auth/kakao/KakaoOAuth2UserService.java index fcdb57d..e9429e7 100644 --- a/src/main/java/leets/blogapplication/service/auth/kakao/KakaoOAuth2UserService.java +++ b/src/main/java/leets/blogapplication/service/auth/kakao/KakaoOAuth2UserService.java @@ -1,8 +1,6 @@ package leets.blogapplication.service.auth.kakao; -import leets.blogapplication.domain.User; import leets.blogapplication.domain.UserSocial; -import leets.blogapplication.repository.UserRepository; import leets.blogapplication.repository.UserSocialRepository; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -12,12 +10,10 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Map; From c8902ec9925a8504d9ec89c8404af080c48e2761 Mon Sep 17 00:00:00 2001 From: user040131 Date: Tue, 18 Nov 2025 22:43:23 +0900 Subject: [PATCH 3/4] =?UTF-8?q?7=EC=A3=BC=EC=B0=A8=20=EA=B3=BC=EC=A0=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20push?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../blogapplication/repository/RefreshTokenRepository.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/leets/blogapplication/repository/RefreshTokenRepository.java b/src/main/java/leets/blogapplication/repository/RefreshTokenRepository.java index 2f23c79..6cc3fad 100644 --- a/src/main/java/leets/blogapplication/repository/RefreshTokenRepository.java +++ b/src/main/java/leets/blogapplication/repository/RefreshTokenRepository.java @@ -2,8 +2,6 @@ import leets.blogapplication.domain.RefreshToken; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import java.util.Optional; From 71dc871002b136ce4977e8e46a2e71ba180a3f3d Mon Sep 17 00:00:00 2001 From: user040131 Date: Mon, 1 Dec 2025 02:07:18 +0900 Subject: [PATCH 4/4] =?UTF-8?q?8=EC=A3=BC=EC=B0=A8=20=EA=B3=BC=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + src/main/resources/application.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c2065bc..0d1258f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ bin/ out/ !**/src/main/**/out/ !**/src/test/**/out/ +src/main/resources/application.yml ### NetBeans ### /nbproject/private/ diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ad2bf35..8ab7b3c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -23,7 +23,7 @@ spring: thymeleaf: cache: false datasource: - url: jdbc:mysql://localhost:3306/blogapp?serverTimezone=Asia/Seoul + url: jdbc:mysql://db:3306/appdb?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 username: root password: 1234 jpa: