์นด์นด์ค ๊ฐ๋ฐ์ ์ฌ์ดํธ์์ ์ดํ๋ฆฌ์ผ์ด์ ์ ํ๋ ๋ฑ๋กํจ
์นด์นด์ค ๋ก๊ทธ์ธ ์ค์ ์ ํ์ฑํํ๊ณ , Redirect URI ๋ถ๋ถ์ http://localhost:8080/login/oauth2/code/kakao
์ฃผ์๋ฅผ ์
๋ ฅ
๋์ ํญ๋ชฉ ์ค์ ์์ profile_nickname, profile_image ํ๋๋ฅผ ํ์ ๋์๋ก ์ค์
๋ณด์ ์ค์ ์์ Client Secret์ ํ์ฑํํ๊ณ , ์ฝ๋๋ฅผ ์์ฑ
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
spring:
security:
oauth2:
client:
registration:
kakao:
client-name: kakao
client-id: e77125b43bb728c04d2f3a40bc2db4a0
client-secret: xyqgyjDW66KF0FAYxjGxZlJGVAg3Jwtz
scope: profile_nickname, profile_image
redirect-uri: "http://localhost:8080/login/oauth2/code/{registrationId}"
authorization-grant-type: authorization_code
client-authentication-method: client_secret_post
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
public class OAuth2AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
//ํ๋ ์๋ต
public OAuth2AuthenticationSuccessHandler(Jwt jwt, UserService userService) {
this.jwt = jwt;
this.userService = userService;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
if (authentication instanceof OAuth2AuthenticationToken) {
OAuth2AuthenticationToken oauth2Token = (OAuth2AuthenticationToken) authentication;
OAuth2User principal = oauth2Token.getPrincipal();
String registrationId = oauth2Token.getAuthorizedClientRegistrationId();
User user = processUserOAuth2UserJoin(principal, registrationId);
String loginSuccessJson = generateLoginSuccessJson(user);
response.setContentType("application/json;charset=UTF-8");
response.setContentLength(loginSuccessJson.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(loginSuccessJson);
} else {
super.onAuthenticationSuccess(request, response, authentication);
}
}
//private ๋ฉ์๋ ์๋ต
}
@Service
public class UserService {
// ... ์๋ต ...
@Transactional
public User join(OAuth2User oauth2User, String provider) {
checkArgument(oauth2User != null, "oauth2User must be provided.");
checkArgument(isNotEmpty(provider), "authorizedClientRegistrationId must be provided.");
String providerId = oauth2User.getName();
return findByProviderAndProviderId(provider, providerId)
.map(user -> {
log.warn("Already exists: {} for (provider: {}, providerId: {})", user, provider, providerId);
return user;
})
.orElseGet(() -> {
Map<String, Object> attributes = oauth2User.getAttributes();
@SuppressWarnings("unchecked")
Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
checkArgument(properties != null, "OAuth2User properties is empty");
String nickname = (String) properties.get("nickname");
String profileImage = (String) properties.get("profile_image");
Group group = groupRepository.findByName("USER_GROUP")
.orElseThrow(() -> new IllegalStateException("Could not found group for USER_GROUP"));
return userRepository.save(
new User(nickname, provider, providerId, profileImage, group)
);
});
}
}
@Bean
public OAuth2AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler(Jwt jwt) {
return new OAuth2AuthenticationSuccessHandler(jwt, userService);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, Jwt jwt) throws Exception {
return http
.oauth2Login(auth -> auth
.successHandler(oauth2AuthenticationSuccessHandler(jwt)))
.addFilterAfter(jwtAuthenticationFilter(jwt), SecurityContextHolderFilter.class)
.build();
}
๋ก๊ทธ์ธ ํ์ด์ง ์์ฑ ํํฐ
์นด์นด์ค ์ธ์ฆ์๋ฒ๋ก ์ฌ์ฉ์๋ฅผ ๋ฆฌ๋ค์ด๋ ํธ ์ํด
Authorization Response๋ฅผ ์์ ํ๊ณ , Token Request๋ฅผ ์ธ์ฆ ์๋ฒ๋ก ์์ฒญ
authorizationRequestRepository ์ธํฐํ์ด์ค ๊ธฐ๋ณธ ๊ตฌํ์ฒด๊ฐ HttpSessionOAuth2AuthorizationRequestRepository ํด๋์ค๋ก Session์ ์ฌ์ฉํ์ง๋ง, API ์๋ฒ๋ Session์ ์ฌ์ฉํ์ง ์๊ธฐ ๋๋ฌธ์ HttpCookieOAuth2AuthorizationRequestRepository ๊ตฌํ์ ์ถ๊ฐํ์ฌ, Session ๋์ Cookie์ ์ฌ์ฉํ๋๋กํจ
public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
private static final String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "OAUTH2_AUTHORIZATION_REQUEST";
private final String cookieName;
private final int cookieExpireSeconds;
public HttpCookieOAuth2AuthorizationRequestRepository() {
this(OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, 180);
}
public HttpCookieOAuth2AuthorizationRequestRepository(String cookieName, int cookieExpireSeconds) {
this.cookieName = cookieName;
this.cookieExpireSeconds = cookieExpireSeconds;
}
@Override
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
return getCookie(request)
.map(this::getOAuth2AuthorizationRequest)
.orElse(null);
}
@Override
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
if (authorizationRequest == null) {
getCookie(request).ifPresent(cookie -> clear(cookie, response));
} else {
String value = Base64.getUrlEncoder().encodeToString(SerializationUtils.serialize(authorizationRequest));
Cookie cookie = new Cookie(cookieName, value);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(cookieExpireSeconds);
response.addCookie(cookie);
}
}
@Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) {
return getCookie(request)
.map(cookie -> {
OAuth2AuthorizationRequest oauth2Request = getOAuth2AuthorizationRequest(cookie);
clear(cookie, response);
return oauth2Request;
})
.orElse(null);
}
private Optional<jakarta.servlet.http.Cookie> getCookie(HttpServletRequest request) {
return ofNullable(WebUtils.getCookie(request, cookieName));
}
private void clear(Cookie cookie, HttpServletResponse response) {
cookie.setValue("");
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
}
private OAuth2AuthorizationRequest getOAuth2AuthorizationRequest(Cookie cookie) {
return SerializationUtils.deserialize(
Base64.getUrlDecoder().decode(cookie.getValue())
);
}
}
OAuth2LoginAuthenticationFilter ๊ตฌํ์ ๋ง์ง๋ง ๋ถ๋ถ์์ OAuth2AuthorizedClient (OAuth2.0์ธ์ฆ ์๋ฃ๋ ์ฌ์ฉ์ ์ ๋ณด) ์ ์ฅ
- authorizedClientRepository ๊ธฐ๋ณธ ๊ตฌํ์ฒด๊ฐ AuthenticatedPrincipalOAuth2AuthorizedClientRepository ํด๋์ค์ด๋ฉฐ ๋ด๋ถ์ ์ผ๋ก InMemoryOAuth2AuthorizedClientService ํด๋์ค๋ฅผ ์ฌ์ฉํด OAuth2AuthorizedClient ๊ฐ์ฒด๋ฅผ ์ ์ฅํจ
- ๋ฐ๋ผ์, OAuth2.0 ์ผ๋ก ์ธ์ฆ๋๋ ํด๋ผ์ด์ธํธ๊ฐ ๋ง์์ง๋ฉด OOME ๋ฐ์ ๊ฐ๋ฅ์ฑ์ด ์์
- ๋ํ ์ธ์ฆ๋ ์ฌ์ฉ์ ์ ๋ณด๊ฐ ํน์ ์๋ฒ ๋ฉ๋ชจ๋ฆฌ์๋ง ์ ์ฅ๋๊ณ ์๊ธฐ ๋๋ฌธ์ ํน์ ์๋ฒ ์ฅ์ ๋ฐ์ ์ ์ฌ์ด๋ ์ดํํธ๊ฐ ๋ฐ์ํ ์ ์์
- ๋คํํ InMemoryOAuth2AuthorizedClientService ํด๋์ค๋ OAuth2AuthorizedClientService ์ธํฐํ์ด์ค ๊ตฌํ์ฒด์ด๋ฉฐ, InMemoryOAuth2AuthorizedClientService ๋ฅผ ๋์ฒดํ ์ ์๋ JdbcOAuth2AuthorizedClientService ํด๋์ค๊ฐ ์์
@Bean
public AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository() {
return new HttpCookieOAuth2AuthorizationRequestRepository();
}
@Bean
public OAuth2AuthorizedClientService authorizedClientService(JdbcOperations jdbcOperations, ClientRegistrationRepository clientRegistrationRepository) {
return new JdbcOAuth2AuthorizedClientService(jdbcOperations, clientRegistrationRepository);
}
@Bean
public OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) {
return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, Jwt jwt, OAuth2AuthorizedClientService authorizedClientService) throws Exception {
return http
//์๋ต
.oauth2Login(auth -> auth
.successHandler(oauth2AuthenticationSuccessHandler(jwt))
.authorizationEndpoint(authorizationEndpointConfig -> authorizationEndpointConfig.authorizationRequestRepository(authorizationRequestRepository()))
.authorizedClientRepository(oAuth2AuthorizedClientRepository(authorizedClientService)))
.addFilterAfter(jwtAuthenticationFilter(jwt), SecurityContextHolderFilter.class)
.build();
}