[Spring] Spring Boot + Spring Security OAuth2 Client + Kakao Login - 1
0. 의존성 추가
먼저 프로젝트에 사용할 라이브러리들의 의존성을 추가한다
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'mysql:mysql-connector-java:8.0.21'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
compileOnly 'org.projectlombok:lombok:1.18.22'
annotationProcessor 'org.projectlombok:lombok:1.18.12'
}
1. SecurityConfig.java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final OAuthService oAuthService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/index", "/login/**").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login() // OAuth2 로그인 설정 시작
.defaultSuccessUrl("/home")
.userInfoEndpoint() // OAuth2 로그인 성공 이후 사용자 정보를 가져올 때 설정을 저장
.userService(oAuthService); // OAuth2 로그인 성공 시, 후작업을 진행할 UserService 인터페이스 구현체 등록
}
}
- EnableWebSecurity
- spring security 설정을 활성화시켜주는 annotation
- antMatchers("/").permitAll()
- "/" 경로의 모든 요청을 허용한다
- anyRequest().authenticated()
- 모든 요청은 인증이되어야한다
- oauth2Login()
- oauth2 로그인을 할 때, 처리하는 메소드
- defaultSuccessUrl("/home")
- oauth2 인증 성공 시 특정 경로로 이동
- userInfoEndpoinrt().userService(oAuthService)
- 로그인이 성공하면, 해당 유저 정보를 들고 oAuthService에서 후처리를 진행한다
2. application-oauth.yml
spring:
security:
oauth2:
client:
registration:
kakao:
client-id: kakao에서 발급받은 앱 REST API 키
client-secret: kakao에서 발급받은 토큰 발급 시 보안을 강화하기 위해 추가 확인하는 코드
redirect-uri: "http://localhost:8080/login/oauth2/code/kakao"
client-authentication-method: POST
authorization-grant-type: authorization_code
scope: account_email
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
위의 application-oauth.yml 파일은 client-id, client-secret과 같이 공유되어서는 안되는 설정값이 있기 때문에
.gitignore 에 추가하고, application.yml에 아래와 같이 include 해주어야 한다.
spring:
profiles:
include: oauth
application-oauth.yml의 설정값들은 org.springframework.security.oauth2.client.registration 객체의 값으로 매핑된다
public final class ClientRegistration implements Serializable {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private String registrationId;
private String clientId;
private String clientSecret;
private ClientAuthenticationMethod clientAuthenticationMethod;
private AuthorizationGrantType authorizationGrantType;
private String redirectUri;
private Set<String> scopes = Collections.emptySet();
private String clientName;
private ProviderDetails providerDetails = new ProviderDetails();
public class ProviderDetails implements Serializable {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private String authorizationUri;
private String tokenUri;
private UserInfoEndpoint userInfoEndpoint = new UserInfoEndpoint();
private String jwkSetUri;
private String issuerUri;
private Map<String, Object> configurationMetadata = Collections.emptyMap();
}
3. OAuthSevice.java
@Slf4j
@Service
@RequiredArgsConstructor
public class OAuthService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final HttpSession httpSession;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
String serviceName = userRequest.getClientRegistration()
.getRegistrationId();
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName();
Map<String, Object> attributes = oAuth2User.getAttributes();
String email;
if ("kakao".equals(serviceName)) {
Map<String, Object> profile = (Map<String, Object>) attributes.get("kakao_account");
email = (String) profile.get("email");
} else {
throw new OAuth2AuthenticationException("허용되지 않는 인증입니다.");
}
User user;
Optional<User> optionalUser = userRepository.findByEmail(email);
String accessToken = userRequest.getAccessToken().getTokenValue();
if (optionalUser.isPresent()) {
user = optionalUser.get();
} else {
user = new User();
user.setEmail(email);
user.setRole(Role.ROLE_USER);
userRepository.save(user);
}
httpSession.setAttribute("user", new SessionUser(user));
httpSession.setAttribute("access_token", accessToken);
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(user.getRole().toString()))
, oAuth2User.getAttributes()
, userNameAttributeName
);
}
}
먼저 userRequest 인자를 통해 로그인 성공한 유저 정보를 받아온다
DB에 존재하지 않는 유저일 경우 저장하고, 세션에 유저정보와 발급받은 access token을 저장한다
4. KakaoAPI.java
로그아웃의 경우 세션초기화와 함께 카카오 계정 로그아웃도 진행해야되기 때문에 따로 객체를 구현했다
아래는 로그인 성공 후 이동한 home.html 코드이다
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
<h1>홈 화면</h1>
<div class="authenticated">
로그인 계정 : <span id ="user" th:text="${session.user.email}"></span>
<div>
<a href="/user/logout" class="btn" role="button">Logout</a>
</div>
</div>
</body>
</html>
아래는 /user/logout 경로로 이동 시 처리하는 UserController.java 코드이다
@Slf4j
@Controller
public class UserController {
private KakaoAPI kakaoapi;
public UserController() {
kakaoapi = new KakaoAPI();
}
@RequestMapping("/user/logout")
public String logout(HttpServletRequest request) {
HttpSession session = request.getSession();
String accessToken = (String)session.getAttribute("access_token");
if (accessToken != null && !"".equals(accessToken)) {
kakaoapi.logout(accessToken);
session.removeAttribute("access_token");
session.removeAttribute("user");
log.info("logout success");
}
return "redirect:/";
}
}
세션 값을 가져와 access token이 존재한다면 세션을 초기화하고 카카오 로그아웃을 진행한다.
아래는 카카오 로그아웃 기능을 구현한 KakaoAPI.java 코드이다
@Slf4j
public class KakaoAPI {
private final String AUTHORIZATION = "Authorization";
private final String BEARER_PREFIX = "Bearer ";
private final String logoutURL = "https://kapi.kakao.com/v1/user/logout";
public void logout(String accessToken) {
try {
URL url = new URL(logoutURL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty(AUTHORIZATION, BEARER_PREFIX + accessToken);
conn.disconnect();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Spring Security OAuth2 Client 라이브러리 사용 시 access token 을 어떻게 재발급 받는지에 대한 레퍼런스를 찾을 수 없어 많이 헤멨었는데, 차근차근 공식문서를 읽어보니 내부적으로 자동 갱신되어진다는 언급을 발견했다..!
reference : https://velog.io/@dbtlwns/Spring-CSRF
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api
https://docs.spring.io/spring-security/site/docs/5.3.2.RELEASE/reference/html5/#oauth2