Skip to content

Commit

Permalink
Merge pull request #3 from kimyubi/feature/social-login
Browse files Browse the repository at this point in the history
close #2 Stateless OAuth2.0 Social Login (Spring Security + JWT + OAuth 2.0)
  • Loading branch information
kimyubi authored Dec 15, 2023
2 parents 2861330 + 147604e commit 423c269
Show file tree
Hide file tree
Showing 46 changed files with 1,408 additions and 82 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/SonarCloudAnalysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ jobs:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
- name: Set YML
run: |
mkdir -p src/main/resources
echo "${{ secrets.APPLICATION_TEST_YML }}" | base64 --decode > src/test/resources/application-test.yml
find src
- name: Build and analyze
env:
GITHUB_TOKEN: ${{ secrets.GHP_TOKEN }} # Needed to get PR information, if any
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,6 @@ gradle-app.setting
# Java heap dump
*.hprof

# End of https://www.toptal.com/developers/gitignore/api/gradle,windows,java,intellij
src/main/java/com/ourhours/server/global/util/cipher/CipherConstant.java
src/test/resources/application-test.yml
# End of https://www.toptal.com/developers/gitignore/api/gradle,windows,java,intellij
37 changes: 22 additions & 15 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.4'
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.3'
id "com.epages.restdocs-api-spec" version '0.19.0'
id 'org.hidetake.swagger.generator' version '2.19.2'
Expand Down Expand Up @@ -34,19 +34,25 @@ jacocoTestReport {
}
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
//configurations {
// compileOnly {
// extendsFrom annotationProcessor
// }
//}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.projectlombok:lombok:1.18.22'

// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'

annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'

// test
Expand All @@ -61,14 +67,6 @@ dependencies {
// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'


// postgresql
runtimeOnly 'org.postgresql:postgresql'

Expand All @@ -81,6 +79,15 @@ dependencies {
testImplementation "org.testcontainers:testcontainers"
testImplementation "org.testcontainers:postgresql"
testImplementation "org.testcontainers:junit-jupiter"

// Spring Security + JWT
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// Oauth2Client
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}

openapi3 {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.ourhours.server.domain.member.domain.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private Long kakaoId;

private String name;

@Builder
public Member(String name, Long kakaoId) {
this.name = name;
this.kakaoId = kakaoId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.ourhours.server.domain.member.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.ourhours.server.domain.member.domain.entity.Member;

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByKakaoId(Long kakaoId);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.ourhours.server.domain.sample.domain.entity;

import com.ourhours.server.global.model.BaseEntity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Sample extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String content;

@Builder
public Sample(String content) {
this.content = content;
}

public void updateContent(String content) {
this.content = content;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.ourhours.server.domain.sample.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.ourhours.server.domain.sample.domain.entity.Sample;

public interface SampleRepository extends JpaRepository<Sample, Long> {
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,27 @@
import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

@EnableJpaRepositories(basePackages = {
"com.ourhours.server.domain.sample.repository"})
import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class RoutingDataSourceConfiguration {

private static final String ROUTING_DATA_SOURCE = "routingDataSource";
private static final String DATA_SOURCE = "dataSource";

private final JpaProperties jpaProperties;

@Bean(ROUTING_DATA_SOURCE)
public DataSource routingDataSource(@Qualifier(MAIN_DATA_SOURCE) final DataSource mainDataSource,
@Qualifier(STANDBY_DATA_SOURCE) final DataSource standbyDataSource) {
Expand All @@ -49,20 +52,16 @@ public DataSource dataSource(@Qualifier(ROUTING_DATA_SOURCE) DataSource routingD

@Bean("entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(@Qualifier(DATA_SOURCE) DataSource dataSource) {
LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean();
entityManagerFactory.setDataSource(dataSource);
entityManagerFactory.setPackagesToScan("com.ourhours.server.domain.sample.domain.entity");
entityManagerFactory.setJpaVendorAdapter(this.jpaVendorAdapter());
entityManagerFactory.setPersistenceUnitName("entityManager");
return entityManagerFactory;
EntityManagerFactoryBuilder entityManagerFactoryBuilder = generateEntityManagerFactoryBuilder(jpaProperties);

return entityManagerFactoryBuilder
.dataSource(dataSource)
.packages("com.ourhours.server.domain.*.domain.entity")
.build();
}

private JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setGenerateDdl(false);
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setDatabasePlatform("org.hibernate.dialect.PostgreSQLDialect");
return hibernateJpaVendorAdapter;
private EntityManagerFactoryBuilder generateEntityManagerFactoryBuilder(JpaProperties jpaProperties) {
return new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), jpaProperties.getProperties(), null);
}

@Bean("transactionManager")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.ourhours.server.global.config.jpa;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing(auditorAwareRef = "customAuditorAware")
public class JpaAuditingConfiguration {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.ourhours.server.global.config.security;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

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 com.ourhours.server.domain.member.domain.entity.Member;
import com.ourhours.server.domain.member.repository.MemberRepository;
import com.ourhours.server.global.model.security.Role;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

private final MemberRepository memberRepository;
public static final String ATTRIBUTE_KEY = "memberId";

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService();
OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest);

Map<String, Object> kakaoAccount = (Map<String, Object>)oAuth2User.getAttributes().get("kakao_account");
Map<String, Object> kakaoProfile = (Map<String, Object>)kakaoAccount.get("profile");
String name = (String)kakaoProfile.get("nickname");
Long kakaoId = (Long)oAuth2User.getAttributes().get("id");

Map<String, Object> attributes = new HashMap<>();
attributes.put(ATTRIBUTE_KEY, getOrSaveMember(kakaoId, name).getId());

return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(Role.USER.name())),
attributes, ATTRIBUTE_KEY);
}

private Member getOrSaveMember(Long kakaoId, String name) {
Optional<Member> optionalMember = memberRepository.findByKakaoId(kakaoId);
if (optionalMember.isEmpty())
return saveMember(kakaoId, name);

return optionalMember.get();
}

private Member saveMember(Long kakaoId, String name) {
Member member = Member.builder()
.name(name)
.kakaoId(kakaoId)
.build();
return memberRepository.save(member);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.ourhours.server.global.config.security;

import static java.util.Objects.*;

import java.nio.charset.StandardCharsets;
import java.time.Duration;

import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.SerializationUtils;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.ourhours.server.global.util.cipher.Aes256;
import com.ourhours.server.global.util.jpa.cookie.CookieUtil;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class HttpCookieOAuth2AuthorizedClientRepository
implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {

public static final String OAUTH2_COOKIE_NAME = "OAUTH2_AUTHORIZATION_REQUEST";
public static final Duration OAUTH_COOKIE_EXPIRY = Duration.ofMinutes(5);

private final ObjectMapper objectMapper;

@Override
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
return getCookie(request);
}

@Override
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request,
HttpServletResponse response) {
if (isNull(authorizationRequest)) {
removeAuthorizationRequest(request, response);
return;
}

CookieUtil.addCookie(response, OAUTH2_COOKIE_NAME, encrypt(authorizationRequest), OAUTH_COOKIE_EXPIRY);
}

@Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request,
HttpServletResponse response) {
OAuth2AuthorizationRequest oAuth2AuthorizationRequest = getCookie(request);
CookieUtil.removeCookie(request, response, OAUTH2_COOKIE_NAME);
return oAuth2AuthorizationRequest;
}

private OAuth2AuthorizationRequest getCookie(HttpServletRequest request) {
return CookieUtil.getCookie(request, OAUTH2_COOKIE_NAME).map(this::decrypt).orElse(null);
}

private String encrypt(OAuth2AuthorizationRequest authorizationRequest) {
byte[] bytes = SerializationUtils.serialize(authorizationRequest);
return Aes256.encrypt(bytes);
}

private OAuth2AuthorizationRequest decrypt(Cookie cookie) {
byte[] bytes = Aes256.decrypt(cookie.getValue().getBytes(StandardCharsets.UTF_8));
return (OAuth2AuthorizationRequest)SerializationUtils.deserialize(bytes);
}

}



Loading

0 comments on commit 423c269

Please sign in to comment.