저번 시간에는 직접 컨트롤러에서 요청을 구현하여서 OAuth2 인증을 처리해봤습니다. 이번 시간에는 OAuth2-client 라이브러리를 이용해서, 소셜 로그인 API를 구현해보도록 하겠습니다.
개발 환경
- IntelliJ IDEA
- Spring Boot 2.4.4
- Java 11
- Spring JPA
- Maven 3.6.3
Maven 의존성 추가
spring-boot-starter-oauth2-client라는 라이브러리는 구글,페이스북 같은 로그인을 통한 인증과 권한 처리를 쉽게 할 수 있게 해준다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
spring security 의존성을 추가하면, 스프링 부트는 그 스프링 시큐리티 자동설정을 적용을 해줘서, 모든 요청에 대해서 인증을 필요로 하게 됩니다. (일부 url을 허용하려면, WebSecurityConfigurerAdaptre를 구현하여 설정을 해주면 됩니다. 이 부분은 아래에서 자세하게 살펴보겠습니다.)
application-oauth.properties 작성 + .gitignore 등록하기
application.properties 와 같은 위치에 새로운 파일을 작성합니다.
application-oauth.properties라는 파일을 생성하고, 이전에 발급받은 클라이언트 ID와 비밀번호를 넣어줍니다.
application-oauth.properties
대표적으로 spring security에서 제공하는 oauth2-client 라이브러리에는 유명한 google, facebook, twitter 등 웹사이트에 대한 provider들은 제공을 해주지만, 우리나라에서만 한정적으로 사용하는 네이버나, 카카오 같은 서비스에 대한 정보들을 제공해주지 못한다. 그래서 우리가 직접 등록을 해줄 것입니다!
# GOOGLE
spring.security.oauth2.client.registration.google.client-id = [클라이언트 id]
spring.security.oauth2.client.registration.google.client-secret = [클라이언트 pw]
spring.security.oauth2.client.registration.google.scope = profile, email
# 구글이나 페이스북은 안적어도 되는데, 네이버나 카카오는 적어줘야함(기본 제공 provider가 아니기 때문에)
spring.security.oauth2.client.registration.naver.client-id = [클라이언트 id]
spring.security.oauth2.client.registration.naver.client-secret= [클라이언트 pw]
spring.security.oauth2.client.registration.naver.client-name=Naver
spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.naver.redirect-uri=http://localhost:8080/login/oauth2/code/naver
# Naver Provider 등록!
spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize
spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token
spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me
spring.security.oauth2.client.provider.naver.user-name-attribute=response # 네이버가 회원정보를 json으로 넘겨주는데, response라는 키값으로 리턴해준다.
# KAKAO
spring.security.oauth2.client.registration.kakao.client-id = [클라이언트 id]
spring.security.oauth2.client.registration.kakao.client-secret = [클라이언트 pw]
spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8080/login/oauth2/code/kakao
spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.kakao.scope=profile,account_email
spring.security.oauth2.client.registration.kakao.client-name=kakao
spring.security.oauth2.client.registration.kakao.client-authentication-method=POST
## kAKAO Provider 등록!
spring.security.oauth2.client.provider.kakao.authorization-uri= https://kauth.kakao.com/oauth/authorize
spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token
spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me
spring.security.oauth2.client.provider.kakao.user-name-attribute=id # 카카오가 회원정보를 json으로 넘겨주는데, id라는 키값으로 리턴해준다.
application.properties 등록
파일명을 application-XXX.properties 로 지으면, XXX 부분을 아래처럼 사용할 수 있다.
# application-oauth.properties
spring.profiles.includes = oauth
.gitignore
발급 받은 클라이언트 id, pw가 들어있기 때문에 .gitignore에 추가해줍니다.
소셜 로그인 사용자 정보를 담을 별도의 User 클래스를 생성하였습니다.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String email;
private String picture;
private String role = "ROLE_USER";
public User(String name, String email, String picture) {
this.name = name;
this.email = email;
this.picture = picture;
}
public User update(String name, String picture) {
this.name = name;
this.picture = picture;
return this;
}
// .. getter, setter 생략
}
UserRepository도 생성해줍니다.
email을 이용해서 찾을 수 있도록 findByEmail 도 만들어줍니다.
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
스프링 시큐리티 설정
@Configuration
@EnableWebSecurity // 해당 애노테이션을 붙인 필터(현재 클래스)를 스프링 필터체인에 등록.
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 커스텀한 OAuth2UserService DI.
@Autowired
private CustomOAuth2UserService customOAuth2UserService;
// encoder를 빈으로 등록.
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
// WebSecurity에 필터를 거는 게 훨씬 빠름. HttpSecrity에 필터를 걸면, 이미 스프링 시큐리티 내부에 들어온 상태기 때문에..
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().mvcMatchers("/members/**","/image/**"); // /image/** 있는 모든 파일들은 시큐리티 적용을 무시한다.
web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations()); // 정적인 리소스들에 대해서 시큐리티 적용 무시.
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest() // 모든 요청에 대해서 허용하라.
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/") // 로그아웃에 대해서 성공하면 "/"로 이동
.and()
.oauth2Login()
.defaultSuccessUrl("/login-success")
.userInfoEndpoint()
.userService(customOAuth2UserService); // oauth2 로그인에 성공하면, 유저 데이터를 가지고 우리가 생성한
// customOAuth2UserService에서 처리를 하겠다. 그리고 "/login-success"로 이동하라.
}
}
- @EnableWebSecurity
- @Configuration 애노테이션을 붙인 클래스에 붙이고, WebSecurityConfigurerAdapter를 상속받으면, Spring Security 설정 클래스가 됩니다.
- BCryptPasswordEncoder는 Spring Security에서 제공하는 암호화 객체입니다.
- configure(WebSecurity web)
- websecurity는 FilterChainProxy를 생성하는 필터입니다.
- web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
- /css/** , /js/** 같은 정적 리소스 파일들은 Spring Security가 무시할 수 있도록, 통과시켜주는 것이 좋습니다.
- configure(HttpSecurity http)
- httpSecurity를 통해 HTTP 요청에 대한 웹 기반 보안을 구성할 수 있습니다.
- autorizeRequests()
- .antMatchers("/**").permitAll().hasRole("ADMIN"); 이렇게 해주면, "/**" 로 들어온 요청에 대해서 권한이 ADMIN일 때, 허용을 해주겠다는 뜻입니다.
- .anyRequest().permitAll() -> 모든 사용자에 대해서 허용하겠다.
- .anyRequest().authenticated() -> 모든 사용자에 대해서 인증 요청하겠다.
- formlogin()
- HttpSession을 이용한, form 로그인을 처리하는 메소드입니다.
- "/login"에 접근하면, Spring security가 제공하는 로그인을 이용할 수 있습니다.
- .loginPage("/customLogin")
- 커스텀 로그인 폼을 사용하고 싶다면 해당 메소드를 사용하면 됩니다.
- .defaultSuccessUrl("/{success-url}")
- 로그인이 성공적으로 완료되었을 때, 이동되는 페이지입니다.
- logout()
- 로그아웃을 처리하는 메소드입니다.
- "/logout"에 접근하면, Spring Security가 Http 세션을 제거해줍니다.
- .logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
- 로그아웃의 url을 설정할 수 있습니다.
- .exceptionHandling().accessDeniedPage("/logout/denied")
- 로그아웃 시 예외 발생했을 때, 해당 메소드로 핸들링할 수 있습니다.
- oauth2Login()
- ouath2 로그인을 할 때, 처리하는 메소드입니다.
- .defaultSuccessUrl("/{login-success-url}")
- oauth2 인증이 성공했을 때, 이동되는 url을 설정할 수 있습니다.
- userInfoEndPoint().userService(customOAuth2UserService);
- 로그인이 성공하면, 해당 유저의 정보를 들고 customOAuth2UserService에서 후처리를 해주겠다는 뜻입니다.
OAuth2UserService 구현하기
package me.isunghan.loginspring.security;
import me.isunghan.loginspring.repository.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
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.Service;
import javax.servlet.http.HttpSession;
import java.util.Collections;
@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
@Autowired
private UserRepository userRepository;
@Autowired
private HttpSession httpSession;
@Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
OAuth2UserService oAuth2UserService = new DefaultOAuth2UserService();
OAuth2User oAuth2User = oAuth2UserService.loadUser(oAuth2UserRequest);
// 현재 진행중인 서비스를 구분하기 위해 문자열로 받음. oAuth2UserRequest.getClientRegistration().getRegistrationId()에 값이 들어있다. {registrationId='naver'} 이런식으로
String registrationId = oAuth2UserRequest.getClientRegistration().getRegistrationId();
// OAuth2 로그인 시 키 값이 된다. 구글은 키 값이 "sub"이고, 네이버는 "response"이고, 카카오는 "id"이다. 각각 다르므로 이렇게 따로 변수로 받아서 넣어줘야함.
String userNameAttributeName = oAuth2UserRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
// OAuth2 로그인을 통해 가져온 OAuth2User의 attribute를 담아주는 of 메소드.
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
User user = saveOrUpdate(attributes);
httpSession.setAttribute("user", new SessionUser(user));
System.out.println(attributes.getAttributes());
return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"))
, attributes.getAttributes()
, attributes.getNameAttributeKey());
}
// 혹시 이미 저장된 정보라면, update 처리
private User saveOrUpdate(OAuthAttributes attributes) {
User user = userRepository.findByEmail(attributes.getEmail())
.map(entity -> entity.update(attributes.getName(), attributes.getPicture()))
.orElse(attributes.toEntity());
return userRepository.save(user);
}
}
OAuth2Attributes 클래스 생성
OAuth2 로그인을 통해서 가져온 OAuth2User의 정보를 담아주기 위한 클래스를 생성합니다.
public class OAuthAttributes {
private Map<String, Object> attributes;
private String nameAttributeKey;
private String name;
private String email;
private String picture;
public OAuthAttributes(Map<String, Object> attributes, String nameAttributeKey, String name, String email, String picture) {
this.attributes = attributes;
this.nameAttributeKey = nameAttributeKey;
this.name = name;
this.email = email;
this.picture = picture;
}
public OAuthAttributes() {
}
// 해당 로그인인 서비스가 kakao인지 google인지 구분하여, 알맞게 매핑을 해주도록 합니다.
// 여기서 registrationId는 OAuth2 로그인을 처리한 서비스 명("google","kakao","naver"..)이 되고,
// userNameAttributeName은 해당 서비스의 map의 키값이 되는 값이됩니다. {google="sub", kakao="id", naver="response"}
public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes) {
if (registrationId.equals("kakao")) {
return ofKakao(userNameAttributeName, attributes);
} else if (registrationId.equals("naver")) {
return ofNaver(userNameAttributeName,attributes);
}
return ofGoogle(userNameAttributeName, attributes);
}
private static OAuthAttributes ofKakao(String userNameAttributeName, Map<String, Object> attributes) {
Map<String, Object> kakao_account = (Map<String, Object>) attributes.get("kakao_account"); // 카카오로 받은 데이터에서 계정 정보가 담긴 kakao_account 값을 꺼낸다.
Map<String, Object> profile = (Map<String, Object>) kakao_account.get("profile"); // 마찬가지로 profile(nickname, image_url.. 등) 정보가 담긴 값을 꺼낸다.
return new OAuthAttributes(attributes,
userNameAttributeName,
(String) profile.get("nickname"),
(String) kakao_account.get("email"),
(String) profile.get("profile_image_url"));
}
private static OAuthAttributes ofNaver(String userNameAttributeName, Map<String, Object> attributes) {
Map<String, Object> response = (Map<String, Object>) attributes.get("response"); // 네이버에서 받은 데이터에서 프로필 정보다 담긴 response 값을 꺼낸다.
return new OAuthAttributes(attributes,
userNameAttributeName,
(String) response.get("name"),
(String) response.get("email"),
(String) response.get("profile_image"));
}
private static OAuthAttributes ofGoogle(String userNameAttributeName, Map<String, Object> attributes) {
return new OAuthAttributes(attributes,
userNameAttributeName,
(String) attributes.get("name"),
(String) attributes.get("email"),
(String) attributes.get("picture"));
}
// .. getter/setter 생략
public User toEntity() {
return new User(name, email, picture);
}
}
소셜 서비스마다 정보들의 값이 다 다르기 때문에 직접 값을 print로 찍어보면서 설정해야 됩니다.
네이버의 경우 response값 안에 프로필 정보가 들어있어서 map에서 "response" 키 값을 이용해 값을 꺼냈고,
각 서비스에서 보내준 프로필 정보들의 key값이 naver의 프로필사진 url은 profile_image이고, google은 picture 등등.. 살짝 살짝 다르기 때문에 값을 잘 설정해줘야 합니다.
SessionUser 클래스 생성
SessionUser는 인증된 사용자 정보를 저장하는 Dto 클래스입니다. HttpSession에 넣을 것이기 때문에 직렬화를 해줍니다.
import java.io.Serializable;
public class SessionUser implements Serializable {
private String name;
private String email;
private String picture;
public SessionUser(User user) {
this.name = user.getName();
this.email = user.getEmail();
this.picture = user.getPicture();
}
public SessionUser() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPicture() {
return picture;
}
public void setPicture(String picture) {
this.picture = picture;
}
}
로그인 버튼 생성
- "/oauth2/authorization/google"
- Spring security에서 기본적으로 제공하는 url을 사용합니다.
<a href="/oauth2/authorization/google">구글 아이디로 로그인</a>
<a href="/oauth2/authorization/naver">네이버 아이디로 로그인</a>
<a href="/oauth2/authorization/kakao">카카오 아이디로 로그인</a>
감사합니다!
REFERENCES
Spring Boot 간편 로그인 구현하기(with security & oauth2.0)
1. 구글 계정 만들기 먼저 OAuth 동의 화면에서 이름 짓고 밑에 3개 잘 등록되었는지만 확인하고 생성 제일 상단처럼 프로젝트 이름을 짓고 왼쪽 햄버거 버튼으로 사용자 인증 정보로 넘어오면 오
smujihoon.tistory.com
[SpringBoot] OAuth2 Google Login
OAuth2 Google Login 스프링 시큐리티 설정 build.gradle에 스프링 시큐리티 관련 의존성 추가 spring-boot-starter-oauth2-client 소셜 로그인 등 클라이언트 입장에서 소셜 기능 구현 시 필요한 의존성 spring-b..
seokr.tistory.com
스프링 부트(Spring Boot): 구글 로그인 연동 (스프링 부트 스타터의 oauth2-client) 이용 + 네이버 아이
이 방법은 JSTL, Thymeleaf, Mustache 등 서버 사이드 템플릿 엔진을 사용하는 로그인 방법입니다. SPA에서 사용할 수 있는 소셜 로그인 연동 방법은 아래 글을 참고하세요, 스프링 부트(Spring Boot): SPA
yoonbumtae.com
Spring Boot Security로 카카오 소셜 로그인 만들기
참고 할점.... 이번 포스팅은 스프링 부트와 AWS로 혼자 구현하는 웹 서비스의 소셜로그인 파트를 참조하여 만들었습니다. 즉, 이 책을 기반으로 코드를 추가한 것이기 때문에 자세한 코드는 책을
sundries-in-myidea.tistory.com
Spring Security 5 - OAuth2 Login | Baeldung
Learn how to authenticate users with Facebook, Google or other credentials using OAuth2 in Spring Security 5.
www.baeldung.com