
의존성은 아래와 같다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.0.2'
id 'io.spring.dependency-management' version '1.1.0'
}
group = 'com.sungjun'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
implementation 'org.projectlombok:lombok:1.18.28'
runtimeOnly 'com.oracle.database.jdbc:ojdbc8'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
tasks.named('test') {
useJUnitPlatform()
}
카카오톡 로그인을 추가하고 정보를 화면에 출력해보는것이 목표다. 템플릿은 Thymeleaf를 사용한다.
저번 포스팅에 Spring Security를 이용한 Form Login에 이어 oAuth2.0을 사용한 카카오톡 로그인을 구현해보려 한다. 카카오톡 로그인에 앞서 먼저 KakaoDevlopers 사이트에서 선행할 작업이 있다.
애플리케이션명.. 등등 입력하고 추가하면 Rest API키가 나온다.
properties에 추가할 key이니 잘 기록해두자
카카오톡 로그인을 ON 상태로 변경하고 동의항목에서 필수항목을 전부 사용한다. 필수적으로 동의해야하는 항목외 추가적인 항목은 비즈니스 앱으로 변경해야하는 경우도 있으니 유념하자
보안키를 발급받는다. 코드행에 있는 Key도 properties에 들어가니 잘 기록하자
이제 모든 준비가 끝났다면 프로젝트로 가보자. 먼저 properties에 아래 내용을 추가하자
카카오 로그인 RestAPI 문서
위 링크를 참고하여 properties를 아래와 같이 작성하여 추가하자
#kakao oAuth
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
spring.security.oauth2.client.registration.kakao.client-id= 발급받은 Rest Api Key 추가
spring.security.oauth2.client.registration.kakao.client-secret= 보안에서 발급받은 코드 추가
spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post
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.client-name=kakao
spring.security.oauth2.client.registration.kakao.scope=profile_image,profile_nickname,account_email
Scope에는 내가 선택한 동의항목만 추가한다.
config 패키지를 만들고 spring security 설정을 위해 SecurityConfig 클래스를 생성한다.
저번 포스팅과 마찬가지로 FilterChain을 Bean으로 등록하여 사용한다.
csrf, cors는 전부 disable로 하였다.
@Bean
public Security FilterChain filterChain(HttpSecurity http) {
http.csrf().disable().cors().disable()
.authorizeHttpRequest(request -> resquest
// 인가에 대한내용
)
.formLogin(login -> login
.loginPage("/")
.loginProcessingUrl("/login")
.usernameParameter("userid")
.passwordParameter("userpw")
.defaultSuccessUrl("/dashboard")
)
.oauth2Login(login -> login
.loginPage("/")
.defaultSuccessUrl("/dashboard")
.userInfoEndpoint()
.userService(principalOauthUserService)
);
return http.build();
}
FormLogin과 함께 사용할것이기 때문에 위와 같이 추가하였다.
동일한 로그인페이지에 formLogin용 form에 대한 설정과
카카오톡 로그인에 대한 설정도 함께하였다.
전 포스팅에서는 로그인시 UserDetails에 로그인된 계정의 정보가 전부 들어갔다. 하지만 OAuth로 로그인하게 된다면 oAuth2User에 정보가 담기게 된다. 따로따로 처리하여도 되지만 번거롭고 관리하기에 적합하지 않기 때문에 둘다 담을 수 있는 객체를 만들어 관리하고자 한다.
public class PrincipalDetails implements UserDetail, OAuthUser {
private User user;
private Map<String, Object> attributes;
// 일반 로그인 생성자
public PrincipalDetails(User user) {
this.user = user;
}
// OAuth 로그인 생성자
public PrincipalDetails(User user, Map<String, Object> attributes) {
this.user = user;
this.attributes = attributes;
}
... 아래는 Override하여 메서드들을 구현해주자
}
Service는 2개로 나누어 작성한다.
하나는 FormLogin용 하나는 oAuth용으로 작성한다.
각 서비스는 아래와 같이 return한다.
@Service
@RequiredArgsConstructor
public class PrincipalService implements UserDetailService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String userid) throws UsernameNotFoundException {
User findUser = userRepository.findByUserid(userid);
if(findUser!=null) {
retrun new PrincipalDetails(findUser);
}
retrun null;
}
}
DB에 자장된 회원이 로그인을 한다면 그대로 User객체에 넣어 PrincipalDetails로 리턴해준다.
@Service
@RequiredArgsConstructor
@Slf4j
public class PrincipalOauthUserService extends DefaultOAuth2UserService {
@Autowired
private UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthorizationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
log.info("getAttributes: {}", oAuth2User.getAttributes());
Map<String, Object> properties = oAuth2User.getAttribute("properties");
Map<String, Object> account = oAuth2User.getAttribute("kakao_account");
String provider = userRequest.getClientRegistration().getRegistrationId();
String providerId = oAuth2User.getAttribute("id").toString();
String loginId = provider + "_" + providerId;
String email = (String) account.get("email");
String profileImg = (String) properties.get("profile_image");
Optional<User> optionalUser = Optional.ofNullable(userRepository.findByUserid(loginId));
User user;
if(optionalUser.isEmpty()) {
user = User.builder()
.userid(loginId)
.role("USER")
.email(email)
.profileimg(profileImg)
.build();
userRepository.save(user);
} else {
user = optionalUser.get();
}
return new PrincipalDetails(user, oAuth2User.getAttributes());
}
}
필요한 정보들을 아래와 같이 저장하고 Builder를 사용하여 User를 만들고 DB에 저장한다. 만약 한번이라도 카카오톡 로그인을 한 회원이면 DB에 저장되었기 때문에 가져온 정보 그대로 User에 담아 PrincipalDetails를 생성해 리턴해준다.
문제가 없다면 화면에 다음과 같이 출력해보자
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<!-- head 내용 -->
</head>
<body>
<div sec:authorize="isAuthenticated()">
Only Authenticated user can see this Text
</div>
<!--인증시 사용된 객체에 대한 정보-->
<b>Authenticated DTO:</b>
<div sec:authentication="principal"></div>
<!--인증시 사용된 객체의 Username (ID)-->
<b>Authenticated username:</b>
<div sec:authentication="name"></div>
</body>
</html>
정보가 잘 출력된다면 정상!
혼자 공부하며 작성하다보니 많은 코드를 생략하고 작성했다.
기록용으로 작성하다보니 포스팅을 보고 따라하려면 무리가 있을것같다.