Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 멤버가 여러 종류의 역할을 가질 수 있도록 변경 #561

Merged
merged 25 commits into from
Aug 3, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2433227
feat: 관리자 역할 추가
uwoobeat Jul 31, 2024
420a508
feat: ADMIN을 일반 역할에서 관리자 역할로 변경
uwoobeat Jul 31, 2024
4dc4ebf
feat: 스터디 역할 추가
uwoobeat Jul 31, 2024
9ac176a
feat: 멤버 도메인에 역할 필드 추가
uwoobeat Jul 31, 2024
9e44147
feat: 멘토 역할 할당 메서드 추가
uwoobeat Jul 31, 2024
537edb2
refactor: 정적 팩토리 메서드를 사용하도록 개선
uwoobeat Jul 31, 2024
881efd1
feat: PrincipalDetails에 관리역할 및 멘토역할 추가
uwoobeat Jul 31, 2024
7cc4633
Merge branch 'develop' into feature/441-multiple-role
uwoobeat Aug 2, 2024
639385c
feat: 멤버 정보 저장하는 DTO 추가
uwoobeat Aug 2, 2024
9a60da8
refactor: AuthInfo를 인자로 받고 파싱 로직을 분리하도록 변경
uwoobeat Aug 2, 2024
ebd962e
refactor: 엑세스 토큰 생성 시 AuthInfo 받도록 변경
uwoobeat Aug 2, 2024
f84e64d
refactor: 엑세스 토큰의 내부 멤버 정보를 AuthInfo로 변경
uwoobeat Aug 2, 2024
34bb045
feat: 신규 JWT 클레임 관련 상수 추가
uwoobeat Aug 2, 2024
e498e8e
refactor: 임시 토큰 발생 로직에도 AuthInfo 추가
uwoobeat Aug 2, 2024
dfc4479
refactor: CustomOAuth2User의 내부 필드를 AuthInfo로 변경
uwoobeat Aug 2, 2024
a069ed7
refactor: 토큰 재발급 로직 수정
uwoobeat Aug 2, 2024
2d6e1df
refactor: 인증주체가 여러 개의 권한을 가질 수 있도록 변경
uwoobeat Aug 2, 2024
af1a7dc
test: 통합 테스트 템플릿 인증주체 변경 반영
uwoobeat Aug 2, 2024
a7d8fee
Merge branch 'develop' into feature/441-multiple-role
uwoobeat Aug 2, 2024
6c9d1c2
fix: 도커 컴포즈 CI 명령어 수정
uwoobeat Aug 2, 2024
8b56532
chore: 테스트 도커 컴포즈 제거
uwoobeat Aug 2, 2024
f2ac4d9
refactor: 도커 컴포즈 사용하도록 롤백 및 database cleaner 실행할때 레디스도 같이 초기화하도록 변경
seulgi99 Aug 2, 2024
8c890cc
refactor: workflow 수정
seulgi99 Aug 2, 2024
e482e95
refactor: workflow 수정
seulgi99 Aug 2, 2024
ac38e54
refactor: RedisCleaner afterPropertiesSet로 값들 초기화 하도록 변경
seulgi99 Aug 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .github/workflows/develop_build_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ jobs:
- name: Run chmod to make gradlew executable
run: chmod +x ./gradlew

# Redis 컨테이너 실행
- name: Start containers
run: docker-compose -f ./docker-compose-test.yaml up -d

# Gradle 빌드
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/production_build_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ jobs:
- name: Run chmod to make gradlew executable
run: chmod +x ./gradlew

# Redis 컨테이너 실행
- name: Start containers
run: docker-compose -f ./docker-compose-test.yaml up -d

# Gradle 빌드
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/pull_request_gradle_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ jobs:
- name: gradlew 권한 부여
run: chmod +x ./gradlew

# Redis 컨테이너 실행
- name: Start containers
run: docker-compose -f ./docker-compose-test.yaml up -d

- name: Setup Gradle
id: gradle
uses: gradle/actions/setup-gradle@v3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
import com.gdschongik.gdsc.domain.auth.domain.RefreshToken;
import com.gdschongik.gdsc.domain.auth.dto.AccessTokenDto;
import com.gdschongik.gdsc.domain.auth.dto.RefreshTokenDto;
import com.gdschongik.gdsc.domain.member.domain.MemberManageRole;
import com.gdschongik.gdsc.domain.member.domain.MemberRole;
import com.gdschongik.gdsc.domain.member.domain.MemberStudyRole;
import com.gdschongik.gdsc.global.security.MemberAuthInfo;
import com.gdschongik.gdsc.global.util.JwtUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
Expand All @@ -22,8 +26,8 @@ public class JwtService {
private final JwtUtil jwtUtil;
private final RefreshTokenRepository refreshTokenRepository;

public AccessTokenDto createAccessToken(Long memberId, MemberRole memberRole) {
return jwtUtil.generateAccessToken(memberId, memberRole);
public AccessTokenDto createAccessToken(MemberAuthInfo authInfo) {
return jwtUtil.generateAccessToken(authInfo);
}

public RefreshTokenDto createRefreshToken(Long memberId) {
Expand Down Expand Up @@ -86,9 +90,16 @@ public AccessTokenDto reissueAccessTokenIfExpired(String accessTokenValue) {
jwtUtil.parseAccessToken(accessTokenValue);
return null;
} catch (ExpiredJwtException e) {
Long memberId = Long.parseLong(e.getClaims().getSubject());
MemberRole memberRole = MemberRole.valueOf(e.getClaims().get(TOKEN_ROLE_NAME, String.class));
return createAccessToken(memberId, memberRole);
Claims claims = e.getClaims();

Long memberId = Long.parseLong(claims.getSubject());
MemberRole memberRole = MemberRole.valueOf(claims.get(TOKEN_ROLE_NAME, String.class));
MemberManageRole memberManageRole =
MemberManageRole.valueOf(claims.get(TOKEN_MANAGE_ROLE_NAME, String.class));
MemberStudyRole memberStudyRole = MemberStudyRole.valueOf(claims.get(TOKEN_STUDY_ROLE_NAME, String.class));
var authInfo = new MemberAuthInfo(memberId, memberRole, memberManageRole, memberStudyRole);

return createAccessToken(authInfo);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package com.gdschongik.gdsc.domain.auth.dto;

import com.gdschongik.gdsc.domain.member.domain.MemberRole;
import com.gdschongik.gdsc.global.security.MemberAuthInfo;

public record AccessTokenDto(Long memberId, MemberRole memberRole, String tokenValue) {}
public record AccessTokenDto(MemberAuthInfo authInfo, String tokenValue) {}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import static com.gdschongik.gdsc.global.exception.ErrorCode.*;

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.member.domain.MemberRole;
import com.gdschongik.gdsc.domain.member.domain.MemberManageRole;
import com.gdschongik.gdsc.global.annotation.DomainService;
import com.gdschongik.gdsc.global.exception.CustomException;

Expand Down Expand Up @@ -32,7 +32,7 @@ public void validateVerifyDiscordCode(
}

public void validateAdminPermission(Member currentMember) {
if (!currentMember.getRole().equals(MemberRole.ADMIN)) {
if (!currentMember.getManageRole().equals(MemberManageRole.ADMIN)) {
throw new CustomException(INVALID_ROLE);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.gdschongik.gdsc.domain.recruitment.application.OnboardingRecruitmentService;
import com.gdschongik.gdsc.domain.recruitment.domain.RecruitmentRound;
import com.gdschongik.gdsc.global.exception.CustomException;
import com.gdschongik.gdsc.global.security.MemberAuthInfo;
import com.gdschongik.gdsc.global.util.EnvironmentUtil;
import com.gdschongik.gdsc.global.util.MemberUtil;
import java.util.Optional;
Expand Down Expand Up @@ -88,7 +89,7 @@ public MemberTokenResponse createTemporaryToken(MemberTokenRequest request) {
.findByOauthId(request.oauthId())
.orElseThrow(() -> new CustomException(MEMBER_NOT_FOUND));

AccessTokenDto accessTokenDto = jwtService.createAccessToken(member.getId(), member.getRole());
AccessTokenDto accessTokenDto = jwtService.createAccessToken(MemberAuthInfo.from(member));
RefreshTokenDto refreshTokenDto = jwtService.createRefreshToken(member.getId());

return new MemberTokenResponse(accessTokenDto.tokenValue(), refreshTokenDto.tokenValue());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.gdschongik.gdsc.domain.member.domain;

import static com.gdschongik.gdsc.domain.member.domain.MemberManageRole.*;
import static com.gdschongik.gdsc.domain.member.domain.MemberRole.*;
import static com.gdschongik.gdsc.domain.member.domain.MemberStudyRole.*;
import static com.gdschongik.gdsc.global.exception.ErrorCode.*;

import com.gdschongik.gdsc.domain.common.model.BaseEntity;
Expand Down Expand Up @@ -34,6 +36,12 @@ public class Member extends BaseEntity {
@Enumerated(EnumType.STRING)
private MemberRole role;

@Enumerated(EnumType.STRING)
private MemberManageRole manageRole;

@Enumerated(EnumType.STRING)
private MemberStudyRole studyRole;

@Enumerated(EnumType.STRING)
private MemberStatus status;

Expand Down Expand Up @@ -67,6 +75,8 @@ public class Member extends BaseEntity {
@Builder(access = AccessLevel.PRIVATE)
private Member(
MemberRole role,
MemberManageRole manageRole,
MemberStudyRole studyRole,
MemberStatus status,
String name,
String studentId,
Expand All @@ -80,6 +90,8 @@ private Member(
String univEmail,
AssociateRequirement associateRequirement) {
this.role = role;
this.manageRole = manageRole;
this.studyRole = studyRole;
this.status = status;
this.name = name;
this.studentId = studentId;
Expand All @@ -99,6 +111,8 @@ public static Member createGuestMember(String oauthId) {
return Member.builder()
.oauthId(oauthId)
.role(GUEST)
.manageRole(NONE)
.studyRole(STUDENT)
.status(MemberStatus.NORMAL)
.associateRequirement(associateRequirement)
.build();
Expand Down Expand Up @@ -268,6 +282,13 @@ public void demoteToGuest() {
associateRequirement.demoteAssociateRequirement();
}

// 기타 역할 변경 로직

public void assignToMentor() {
validateStatusUpdatable();
studyRole = MENTOR;
}

// 기타 상태 변경 로직

public void updateLastLoginAt() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.gdschongik.gdsc.domain.member.domain;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum MemberManageRole {
ADMIN("ROLE_ADMIN"),
NONE("ROLE_NONE");

private final String value;
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package com.gdschongik.gdsc.domain.member.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@AllArgsConstructor
@RequiredArgsConstructor
public enum MemberRole {
GUEST("ROLE_GUEST"),
ASSOCIATE("ROLE_ASSOCIATE"),
REGULAR("ROLE_REGULAR"),
ADMIN("ROLE_ADMIN");
REGULAR("ROLE_REGULAR");

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

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum MemberStudyRole {
MENTOR("ROLE_MENTOR"),
STUDENT("ROLE_STUDENT");

private final String value;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ public class SecurityConstant {
public static final String ACCESS_TOKEN_PARAM = "access";
public static final String REFRESH_TOKEN_PARAM = "refresh";
public static final String TOKEN_ROLE_NAME = "role";
public static final String TOKEN_MANAGE_ROLE_NAME = "manageRole";
public static final String TOKEN_STUDY_ROLE_NAME = "studyRole";
public static final String GITHUB_NAME_ATTR_KEY = "id";
public static final String ACCESS_TOKEN_HEADER_PREFIX = "Bearer ";
public static final String ACCESS_TOKEN_HEADER_NAME = "Authorization";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,19 @@
import static com.gdschongik.gdsc.global.common.constant.SecurityConstant.GITHUB_NAME_ATTR_KEY;

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.member.domain.MemberRole;
import lombok.Getter;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;

@Getter
public class CustomOAuth2User extends DefaultOAuth2User {

private final Long memberId;
private final MemberRole memberRole;
private final MemberAuthInfo memberAuthInfo;
private final LandingStatus landingStatus;

public CustomOAuth2User(OAuth2User oAuth2User, Member member) {
super(oAuth2User.getAuthorities(), oAuth2User.getAttributes(), GITHUB_NAME_ATTR_KEY);
memberId = member.getId();
memberRole = member.getRole();
memberAuthInfo = MemberAuthInfo.from(member);
landingStatus = LandingStatus.TO_DASHBOARD;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ public void onAuthenticationSuccess(
CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();

// 토큰 생성 후 쿠키에 저장
AccessTokenDto accessTokenDto =
jwtService.createAccessToken(oAuth2User.getMemberId(), oAuth2User.getMemberRole());
RefreshTokenDto refreshTokenDto = jwtService.createRefreshToken(oAuth2User.getMemberId());
MemberAuthInfo memberAuthInfo = oAuth2User.getMemberAuthInfo();
AccessTokenDto accessTokenDto = jwtService.createAccessToken(memberAuthInfo);
RefreshTokenDto refreshTokenDto = jwtService.createRefreshToken(memberAuthInfo.memberId());
cookieUtil.addTokenCookies(response, accessTokenDto.tokenValue(), refreshTokenDto.tokenValue());

// 임시로 헤더에 엑세스 토큰 추가
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import com.gdschongik.gdsc.domain.auth.application.JwtService;
import com.gdschongik.gdsc.domain.auth.dto.AccessTokenDto;
import com.gdschongik.gdsc.domain.auth.dto.RefreshTokenDto;
import com.gdschongik.gdsc.domain.member.domain.MemberRole;
import com.gdschongik.gdsc.global.common.constant.JwtConstant;
import com.gdschongik.gdsc.global.util.CookieUtil;
import jakarta.servlet.FilterChain;
Expand Down Expand Up @@ -42,7 +41,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
if (accessTokenHeaderValue != null) {
AccessTokenDto accessTokenDto = jwtService.retrieveAccessToken(accessTokenHeaderValue);
if (accessTokenDto != null) {
setAuthenticationToContext(accessTokenDto.memberId(), accessTokenDto.memberRole());
setAuthenticationToContext(PrincipalDetails.from(accessTokenDto));
filterChain.doFilter(request, response);
return;
}
Expand All @@ -58,7 +57,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse

// AT가 유효하면 통과
if (accessTokenDto != null) {
setAuthenticationToContext(accessTokenDto.memberId(), accessTokenDto.memberRole());
UserDetails userDetails = PrincipalDetails.from(accessTokenDto);
setAuthenticationToContext(userDetails);
filterChain.doFilter(request, response);
return;
}
Expand All @@ -72,7 +72,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
AccessTokenDto accessToken = reissueAccessToken.get();
RefreshTokenDto refreshToken = jwtService.createRefreshToken(refreshTokenDto.memberId());
cookieUtil.addTokenCookies(response, accessToken.tokenValue(), refreshToken.tokenValue());
setAuthenticationToContext(accessToken.memberId(), accessToken.memberRole());
UserDetails userDetails = PrincipalDetails.from(accessToken);
setAuthenticationToContext(userDetails);
}

// AT, RT 둘 다 만료되었으면 실패
Expand All @@ -92,8 +93,7 @@ private String extractAccessTokenFromHeader(HttpServletRequest request) {
.orElse(null);
}

private void setAuthenticationToContext(Long memberId, MemberRole memberRole) {
UserDetails userDetails = new PrincipalDetails(memberId, memberRole);
private void setAuthenticationToContext(UserDetails userDetails) {
Authentication authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.gdschongik.gdsc.global.security;

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.member.domain.MemberManageRole;
import com.gdschongik.gdsc.domain.member.domain.MemberRole;
import com.gdschongik.gdsc.domain.member.domain.MemberStudyRole;

/**
* 엑세스 토큰 및 시큐리티 내부 로직에서 사용되는 회원 정보 DTO입니다.
*/
public record MemberAuthInfo(Long memberId, MemberRole role, MemberManageRole manageRole, MemberStudyRole studyRole) {
public static MemberAuthInfo from(Member member) {
return new MemberAuthInfo(member.getId(), member.getRole(), member.getManageRole(), member.getStudyRole());
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
package com.gdschongik.gdsc.global.security;

import com.gdschongik.gdsc.domain.auth.dto.AccessTokenDto;
import com.gdschongik.gdsc.domain.member.domain.MemberManageRole;
import com.gdschongik.gdsc.domain.member.domain.MemberRole;
import com.gdschongik.gdsc.domain.member.domain.MemberStudyRole;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

@AllArgsConstructor
@RequiredArgsConstructor
public class PrincipalDetails implements UserDetails {

private final Long memberId;
private final MemberRole memberRole;
private final MemberRole role;
private final MemberManageRole manageRole;
private final MemberStudyRole studyRole;

public static PrincipalDetails from(AccessTokenDto token) {
MemberAuthInfo authInfo = token.authInfo();
return new PrincipalDetails(authInfo.memberId(), authInfo.role(), authInfo.manageRole(), authInfo.studyRole());
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(new SimpleGrantedAuthority(memberRole.getValue()));
Collection<GrantedAuthority> authorities = new ArrayList<>();

authorities.add(new SimpleGrantedAuthority(role.name()));
authorities.add(new SimpleGrantedAuthority(manageRole.name()));
authorities.add(new SimpleGrantedAuthority(studyRole.name()));

return authorities;
}

@Override
Expand Down
Loading
Loading