Skip to content

Commit

Permalink
feat: 회원 도메인 및 시큐리티 유틸리티 구현 (#56)
Browse files Browse the repository at this point in the history
* refactor: 도메인 이름을 user에서 member로 변경

* feat: 회원 도메인 구현

* test: 회원 테스트 작성

* refactor: `.gitkeep` 삭제

* chore: 시큐리티 의존성 추가

* feat: 시큐리티 설정 추가

* fix: 전역 응답객체와 호환되기 위해 String에서 임시 응답객체로 변경

* feat: 멤버 레포지터리 추가

* feat: 임시 UserDetail 추가

* refactor: 시큐리티 패키지로 이동

* feat: PrincipalDetails 구현

* fix: 에러코드 생성자 수정

* feat: 멤버 레포지터리 추가

* style: 포매팅 적용

* feat: 시큐리티 유틸리티 구현

* feat: 멤버 유틸리티 구현

* refactor: gitkeep 삭제

* feat: 에러코드 추가

* fix: 미수정 코드 제거

* test: 멤버 유틸리티 테스트 추가

* style: 포매팅 적용

* fix: 오타 수정

* test: 임시 회원 삽입 테스트

* style: 포매팅 수정

* refactor: gitkeep 삭제

* refactor: 빌더를 사용하도록 변경

* fix: 빌더 접근제어 설정 추가

* refactor: 기본 생성자 접근제어 수정

* docs: TODO 추가

* style: 포매팅 수정

* docs: 주석 수정
  • Loading branch information
uwoobeat authored Dec 11, 2023
1 parent d647df5 commit 670b6ee
Show file tree
Hide file tree
Showing 23 changed files with 383 additions and 5 deletions.
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-actuator'

// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-test'
}

tasks.named('bootBuildImage') {
Expand Down
9 changes: 6 additions & 3 deletions src/main/java/com/depromeet/ExampleController.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.depromeet;

import com.depromeet.global.error.ErrorResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

Expand All @@ -10,8 +13,8 @@
public class ExampleController {

@Operation(summary = "게시글 생성", description = "API health check")
@GetMapping("/health-check")
public String hello() {
return "hello";
@GetMapping("/v1/health-check")
public ResponseEntity<ErrorResponse> hello() {
return ResponseEntity.ok(ErrorResponse.of(HttpStatus.OK, "Hello World!"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.depromeet.domain.member.dao;

import com.depromeet.domain.member.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {}
57 changes: 57 additions & 0 deletions src/main/java/com/depromeet/domain/member/domain/Member.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.depromeet.domain.member.domain;

import com.depromeet.domain.common.model.BaseTimeEntity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

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

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;

@Embedded private Profile profile;

@Enumerated(EnumType.STRING)
private MemberStatus status;

@Enumerated(EnumType.STRING)
private MemberRole role;

@Enumerated(EnumType.STRING)
private MemberVisibility visibility;

private LocalDateTime lastLoginAt;

@Builder(access = AccessLevel.PRIVATE)
private Member(
Profile profile,
MemberStatus status,
MemberRole role,
MemberVisibility visibility,
LocalDateTime lastLoginAt) {
this.profile = profile;
this.status = status;
this.role = role;
this.visibility = visibility;
this.lastLoginAt = lastLoginAt;
}

public static Member createNormalMember(Profile profile) {
return Member.builder()
.profile(profile)
.status(MemberStatus.NORMAL)
.role(MemberRole.USER)
.visibility(MemberVisibility.PUBLIC)
.lastLoginAt(LocalDateTime.now())
.build();
}
}
13 changes: 13 additions & 0 deletions src/main/java/com/depromeet/domain/member/domain/MemberRole.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.depromeet.domain.member.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum MemberRole {
USER("USER"),
ADMIN("ADMIN");

private final String value;
}
14 changes: 14 additions & 0 deletions src/main/java/com/depromeet/domain/member/domain/MemberStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.depromeet.domain.member.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum MemberStatus {
NORMAL("NORMAL"),
DELETED("DELETED"),
FORBIDDEN("FORBIDDEN");

private final String value;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.depromeet.domain.member.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum MemberVisibility {
PUBLIC("PUBLIC"),
PRIVATE("PRIVATE");

private final String value;
}
3 changes: 3 additions & 0 deletions src/main/java/com/depromeet/domain/member/domain/Profile.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.depromeet.domain.member.domain;

public record Profile(String nickname, String profileImageUrl) {}
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.depromeet.global.config.security;

import java.util.Collection;
import java.util.Collections;
import lombok.AllArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

@AllArgsConstructor
public class PrincipalDetails implements UserDetails {

private final Long memberId;
private final String role;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(new SimpleGrantedAuthority("ROLE_" + role));
}

@Override
public String getPassword() {
return null;
}

@Override
public String getUsername() {
return memberId.toString();
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.depromeet.global.config.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable);

http.authorizeHttpRequests(
authorize ->
authorize
.requestMatchers("/10mm-actuator/**")
.permitAll() // 액추에이터
.requestMatchers("/v1/**")
.permitAll() // 임시로 모든 요청 허용
.anyRequest()
.authenticated());

return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();

configuration.addAllowedOriginPattern("https://10mm.today");

// TODO: 운영환경에 따라 허용되는 도메인이 달라지도록 개선
configuration.addAllowedOriginPattern("http://localhost:3000");

configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.depromeet.global.error.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class CustomException extends RuntimeException {

private final ErrorCode errorCode;

public CustomException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
@AllArgsConstructor
public enum ErrorCode {
SAMPLE_ERROR(HttpStatus.BAD_REQUEST, "Sample Error Message"),

// Member
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 회원을 찾을 수 없습니다."),

// Security
AUTH_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "시큐리티 인증 정보를 찾을 수 없습니다."),
;

private final HttpStatus status;
Expand Down
Empty file.
36 changes: 36 additions & 0 deletions src/main/java/com/depromeet/global/util/MemberUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.depromeet.global.util;

import com.depromeet.domain.member.dao.MemberRepository;
import com.depromeet.domain.member.domain.Member;
import com.depromeet.domain.member.domain.Profile;
import com.depromeet.global.error.exception.CustomException;
import com.depromeet.global.error.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class MemberUtil {

private final SecurityUtil securityUtil;
private final MemberRepository memberRepository;

// TODO: 데이터베이스 연동 및 픽스쳐 데이터 삽입 이후 삭제
private void insertMockMemberIfNotExist() {
if (memberRepository.count() != 0) {
return;
}

Member memeber = Member.createNormalMember(new Profile("testNickname", "testImageUrl"));

memberRepository.save(memeber);
}

public Member getCurrentMember() {
insertMockMemberIfNotExist();

return memberRepository
.findById(securityUtil.getCurrentMemberId())
.orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));
}
}
27 changes: 27 additions & 0 deletions src/main/java/com/depromeet/global/util/SecurityUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.depromeet.global.util;

import com.depromeet.global.config.security.PrincipalDetails;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

@Component
public class SecurityUtil {

private void setMockAuthentication() {
PrincipalDetails principal = new PrincipalDetails(1L, "USER");
Authentication authentication =
new UsernamePasswordAuthenticationToken(
principal, "password", principal.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}

public Long getCurrentMemberId() {
setMockAuthentication();
PrincipalDetails principal =
(PrincipalDetails)
SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return Long.parseLong(principal.getUsername());
}
}
53 changes: 53 additions & 0 deletions src/test/java/com/depromeet/domain/member/domain/MemberTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.depromeet.domain.member.domain;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class MemberTest {

// Fixture
Profile profile;

@BeforeEach
void setUp() {
profile = new Profile("testNickname", "testProfileImageUrl");
}

@Test
void 회원가입시_초기_상태는_NORMAL이다() {
// given
Member member = Member.createNormalMember(profile);

// when
MemberStatus status = member.getStatus();

// then
assertEquals(MemberStatus.NORMAL, status);
}

@Test
void 회원가입시_초기_역할은_USER이다() {
// given
Member member = Member.createNormalMember(profile);

// when
MemberRole role = member.getRole();

// then
assertEquals(MemberRole.USER, role);
}

@Test
void 회원가입시_초기_공개여부는_PUBLIC이다() {
// given
Member member = Member.createNormalMember(profile);

// when
MemberVisibility visibility = member.getVisibility();

// then
assertEquals(MemberVisibility.PUBLIC, visibility);
}
}
Loading

0 comments on commit 670b6ee

Please sign in to comment.