diff --git a/.gitignore b/.gitignore index 53870f4..4e5ab58 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ # Ignore Gradle build output directory build/ +# application config +application-oauth.yml + ### intelliJ .idea/ diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b589d56..ca1ce44 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index ff95d61..d7c4317 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,6 +31,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter:2.6.1' testImplementation 'org.springframework.boot:spring-boot-starter-test:2.6.1' + // OAuth2 - 소셜 로그인 기능 구현시 필요한 의존성 + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client:2.6.2' + + // Lombok compileOnly 'org.projectlombok:lombok:1.18.22' annotationProcessor 'org.projectlombok:lombok:1.18.22' @@ -41,6 +45,12 @@ dependencies { // H2-db runtimeOnly 'com.h2database:h2' + // Spring Security + implementation 'org.springframework.boot:spring-boot-starter-security' + + // Spring Developer Tools + developmentOnly 'org.springframework.boot:spring-boot-devtools' + // This dependency is used by the application. implementation 'com.google.guava:guava:30.1.1-jre' } diff --git a/app/src/main/java/mentorview/App.java b/app/src/main/java/mentorview/App.java index 7c3162c..b8851bf 100644 --- a/app/src/main/java/mentorview/App.java +++ b/app/src/main/java/mentorview/App.java @@ -3,6 +3,9 @@ */ package mentorview; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication public class App { public String getGreeting() { return "Hello World!"; diff --git a/app/src/main/java/mentorview/Infra/UserRepository.java b/app/src/main/java/mentorview/Infra/UserRepository.java index 8f89386..7646ba7 100644 --- a/app/src/main/java/mentorview/Infra/UserRepository.java +++ b/app/src/main/java/mentorview/Infra/UserRepository.java @@ -4,6 +4,9 @@ import mentorview.domain.User; import org.springframework.data.jpa.repository.JpaRepository; -public interface SignUpRepository extends JpaRepository { - User findByEmail(String email); +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + // email을 통해 가입된 회원 인지를 확인 + Optional findByEmail(String email); } diff --git a/app/src/main/java/mentorview/application/CustomOAuth2UserService.java b/app/src/main/java/mentorview/application/CustomOAuth2UserService.java new file mode 100644 index 0000000..9a748e3 --- /dev/null +++ b/app/src/main/java/mentorview/application/CustomOAuth2UserService.java @@ -0,0 +1,56 @@ +package mentorview.application; + +import lombok.RequiredArgsConstructor; +import mentorview.Infra.UserRepository; +import mentorview.domain.User; +import mentorview.dto.OAuthAttributes; +import mentorview.dto.SessionUser; +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 +@RequiredArgsConstructor +public class CustomOAuth2UserService implements OAuth2UserService { + private final UserRepository userRepository; + private final HttpSession httpSession; + + @Override + public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException { + OAuth2UserService delegate = new DefaultOAuth2UserService(); + OAuth2User oAuth2User = delegate.loadUser(oAuth2UserRequest); + + String registrationId = oAuth2UserRequest.getClientRegistration() + .getRegistrationId(); + + String userNameAttributeName = oAuth2UserRequest.getClientRegistration() + .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); + + OAuthAttributes attributes = OAuthAttributes.of( + registrationId, userNameAttributeName, oAuth2User.getAttributes() + ); + + User user = saveOrUpdate(attributes); + httpSession.setAttribute("user", new SessionUser(user)); + + return new DefaultOAuth2User(Collections.singleton( + new SimpleGrantedAuthority(user.getRoleKey())), + attributes.getAttributes(), + attributes.getNameAttributeKey()); + } + + private User saveOrUpdate(OAuthAttributes attributes) { + User user = (User) userRepository.findByEmail(attributes.getEmail()) + .map(entity -> entity.update(attributes.getName(), attributes.getPicture())) + .orElse(attributes.toEntity()); + return userRepository.save(user); + } +} diff --git a/app/src/main/java/mentorview/application/UserService.java b/app/src/main/java/mentorview/application/UserService.java new file mode 100644 index 0000000..ffe3ab6 --- /dev/null +++ b/app/src/main/java/mentorview/application/UserService.java @@ -0,0 +1,8 @@ +package mentorview.application; + +import org.springframework.stereotype.Service; + +@Service +public class UserService { + +} diff --git a/app/src/main/java/mentorview/controllers/LoginController.java b/app/src/main/java/mentorview/controllers/LoginController.java new file mode 100644 index 0000000..dee48ba --- /dev/null +++ b/app/src/main/java/mentorview/controllers/LoginController.java @@ -0,0 +1,21 @@ +package mentorview.controllers; + + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpSession; + + +@RestController +public class LoginController { + + private final HttpSession httpSession; + + public LoginController(HttpSession httpSession) { + this.httpSession = httpSession; + } + + @GetMapping("/user") + public +} diff --git a/app/src/main/java/mentorview/domain/BaseTimeEntity.java b/app/src/main/java/mentorview/domain/BaseTimeEntity.java index 921fef1..0f57c20 100644 --- a/app/src/main/java/mentorview/domain/BaseTimeEntity.java +++ b/app/src/main/java/mentorview/domain/BaseTimeEntity.java @@ -1,7 +1,37 @@ package mentorview.domain; -import javax.persistence.Entity; +import lombok.Getter; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; -@Entity -public class BaseTimeEntity { +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseTimeEntity { + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdDate; + + @LastModifiedDate + @Column + private LocalDateTime modifiedDate; + + @CreatedBy + @Column(updatable = false) + private String createdBy; + + @LastModifiedBy + @Column + private String updatedBy; + + public abstract Object update(String name, String picture); } \ No newline at end of file diff --git a/app/src/main/java/mentorview/domain/Role.java b/app/src/main/java/mentorview/domain/Role.java index b722ac6..510ad65 100644 --- a/app/src/main/java/mentorview/domain/Role.java +++ b/app/src/main/java/mentorview/domain/Role.java @@ -1,2 +1,15 @@ -package mentorview.domain;public class Role { +package mentorview.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum Role { + USER("ROLE_USER", "회원", true), + GET_TOTAL_REVIEW("ROLE_GET_TOTAL_REVIEW", "비밀답변 권한", false); + + private final String key; + private final String title; + private final boolean total_review; } diff --git a/app/src/main/java/mentorview/domain/User.java b/app/src/main/java/mentorview/domain/User.java index b849aee..23101b1 100644 --- a/app/src/main/java/mentorview/domain/User.java +++ b/app/src/main/java/mentorview/domain/User.java @@ -15,30 +15,35 @@ @Entity @Getter @NoArgsConstructor -public class User extends BaseTimeEntity { +public abstract class User extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) + @Column(length = 20, nullable = false) private String name; @Column(nullable = false) private String email; + @Column + private String picture; + @Enumerated(EnumType.STRING) @Column(nullable = false) private Role role; @Builder - public User(String name, String email, Role role) { + public User(String name, String email, String picture, Role role) { this.name = name; this.email = email; + this.picture = picture; this.role = role; } - public User update(String name, Role role) { + public User update(String name, String picture, Role role) { this.name = name; + this.picture = picture; this.role = role; return this; @@ -47,4 +52,6 @@ public User update(String name, Role role) { public String getRoleKey() { return this.role.getKey(); } + + } diff --git a/app/src/main/java/mentorview/dto/OAuthAttributes.java b/app/src/main/java/mentorview/dto/OAuthAttributes.java new file mode 100644 index 0000000..eff28c0 --- /dev/null +++ b/app/src/main/java/mentorview/dto/OAuthAttributes.java @@ -0,0 +1,56 @@ +package mentorview.dto; + +import lombok.Builder; +import lombok.Getter; +import mentorview.domain.Role; +import mentorview.domain.User; + +import java.util.Map; + +@Getter +public class OAuthAttributes { + private Map attributes; + private String nameAttributeKey; + private String name; + private String email; + private String picture; + + @Builder + public OAuthAttributes(Map 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 static OAuthAttributes of( + String registrationId, String userNameAttributeName, + Map attributes + ) { + return ofGoogle(userNameAttributeName, attributes); + } + + private static OAuthAttributes ofGoogle( + String userNameAttributeName, Map attributes + ) { + return OAuthAttributes.builder() + .name((String) attributes.get("name")) + .email((String) attributes.get("email")) + .picture((String) attributes.get("picture")) + .attributes(attributes) + .nameAttributeKey(userNameAttributeName) + .build(); + } + + public User toEntity() { + return User.builder() + .name(name) + .email(email) + .picture(picture) + .role(Role.USER) + .build(); + } +} diff --git a/app/src/main/java/mentorview/dto/SessionUser.java b/app/src/main/java/mentorview/dto/SessionUser.java new file mode 100644 index 0000000..d84e3e8 --- /dev/null +++ b/app/src/main/java/mentorview/dto/SessionUser.java @@ -0,0 +1,20 @@ +package mentorview.dto; + +import lombok.Getter; +import mentorview.domain.User; + +import java.io.Serializable; + +@Getter +public class SessionUser implements Serializable { + private String name; + private String email; + private String picture; + + + public SessionUser(User user) { + this.name = name; + this.email = email; + this.picture = picture; + } +} diff --git a/app/src/main/java/mentorview/security/SecurityConfig.java b/app/src/main/java/mentorview/security/SecurityConfig.java new file mode 100644 index 0000000..b1a64f2 --- /dev/null +++ b/app/src/main/java/mentorview/security/SecurityConfig.java @@ -0,0 +1,41 @@ +package mentorview.security; + +import mentorview.application.CustomOAuth2UserService; +import mentorview.domain.Role; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +@Configuration +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + private final CustomOAuth2UserService customOAuth2UserService; + + public SecurityConfig(CustomOAuth2UserService customOAuth2UserService) { + this.customOAuth2UserService = customOAuth2UserService; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .csrf().disable() + .headers().frameOptions().disable() + .and() + .authorizeRequests() + .antMatchers("/", "/css/**", "/images/**", "/js/**", "/h2-console/**") + .permitAll() + .antMatchers("/api/v1/**").hasRole(Role.USER.name()) + .anyRequest().authenticated() + .and() + .logout() + .logoutSuccessUrl("/") + .and() + .oauth2Login() + .userInfoEndpoint() + .userService(customOAuth2UserService); + + super.configure(http); + } +} diff --git a/app/src/main/resources/templates/index.html b/app/src/main/resources/templates/index.html new file mode 100644 index 0000000..2d35e5e --- /dev/null +++ b/app/src/main/resources/templates/index.html @@ -0,0 +1,11 @@ + + + + + mentorview + + + +
+ + \ No newline at end of file diff --git a/app/src/test/resources/application-oauth.yml b/app/src/test/resources/application-oauth.yml index e69de29..0c851d1 100644 --- a/app/src/test/resources/application-oauth.yml +++ b/app/src/test/resources/application-oauth.yml @@ -0,0 +1,22 @@ +spring: + security: + oauth2: + client: + registration: + google: + client-id: 43640499580-o53kqsicb746knel82o6biu41bn9jqfu.apps.googleusercontent.com + client-secret: "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" + scope: profile, email + kakao: + client-id: 4aca4b4f37fbf39105fe0d0e43d2a609 + redirect-uri: "https://localhost:8080/login/oauth2/code/kakao" + client-authentication-method: POST + authorization-grant-type: authorization_code + scope: profile_nickname, 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 \ No newline at end of file diff --git a/app/src/test/resources/application.yml b/app/src/test/resources/application.yml new file mode 100644 index 0000000..9695d78 --- /dev/null +++ b/app/src/test/resources/application.yml @@ -0,0 +1,3 @@ +spring: + profiles: + include: oauth \ No newline at end of file