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/154] Security 설정 및 Jwt @AuthMember 연동 #164

Merged
merged 5 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.security:spring-security-crypto:5.8.0'

// swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
Expand Down Expand Up @@ -68,7 +68,7 @@ dependencies {
def generated = 'src/main/generated'

// querydsl QClass 파일 생성 위치를 지정
tasks.withType(JavaCompile) {
tasks.withType(JavaCompile).configureEach {
options.getGeneratedSourceOutputDirectory().set(file(generated))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package com.gamegoo.gamegoo_v2.account.auth.annotation.resolver;

import com.gamegoo.gamegoo_v2.account.auth.annotation.AuthMember;
import com.gamegoo.gamegoo_v2.core.exception.JwtAuthException;
import com.gamegoo.gamegoo_v2.account.auth.security.SecurityUtil;
import com.gamegoo.gamegoo_v2.core.exception.MemberException;
import com.gamegoo.gamegoo_v2.core.exception.common.ErrorCode;
import com.gamegoo.gamegoo_v2.account.member.domain.Member;
import com.gamegoo.gamegoo_v2.account.member.repository.MemberRepository;
import jakarta.servlet.http.HttpServletRequest;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
Expand All @@ -32,16 +33,13 @@ public boolean supportsParameter(MethodParameter parameter) {
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
public Object resolveArgument(@NonNull MethodParameter parameter, ModelAndViewContainer mavContainer,
@NonNull NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
Long currentMemberId = SecurityUtil.getCurrentMemberId();
return memberRepository.findById(currentMemberId)
.orElseThrow(() -> new MemberException(ErrorCode.MEMBER_NOT_FOUND));

Long memberId = (Long) request.getAttribute("memberId");
if (memberId == null) {
throw new JwtAuthException(ErrorCode.MEMBER_EXTRACTION_FAILED);
}

return memberRepository.findById(memberId).orElseThrow(() -> new JwtAuthException(ErrorCode.MEMBER_NOT_FOUND));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.gamegoo.gamegoo_v2.account.auth.domain;

public enum Role {
MEMBER, ADMIN
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.gamegoo.gamegoo_v2.account.auth.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.gamegoo.gamegoo_v2.core.common.ApiResponse;
import com.gamegoo.gamegoo_v2.core.exception.common.ErrorCode;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.PrintWriter;

@Slf4j
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
response.setContentType("application/json; charset=UTF-8");
response.setStatus(403);
PrintWriter writer = response.getWriter();

// 실패 응답
ApiResponse<Object> apiResponse =
ApiResponse.builder()
.code(ErrorCode._FORBIDDEN.getCode())
.message(ErrorCode._FORBIDDEN.getMessage())
.data(null)
.status(ErrorCode._FORBIDDEN.getStatus())
.build();

try {
ObjectMapper objectMapper = new ObjectMapper();
String jsonResponse = objectMapper.writeValueAsString(apiResponse);
writer.write(jsonResponse);
} catch (NullPointerException e) {
log.error("응답 메시지 작성 에러", e);
} finally {
if (writer != null) {
writer.flush();
writer.close();
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.gamegoo.gamegoo_v2.account.auth.security;

import com.gamegoo.gamegoo_v2.account.auth.domain.Role;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

@Getter
public class CustomUserDetails implements UserDetails {

Long memberId;
Role role; // Member, Admin

public CustomUserDetails(Long memberId, Role role) {
this.memberId = memberId;
this.role = role;
}


@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<String> roles = new ArrayList<>();
roles.add("ROLE_" + role);

return roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}

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

// getUserId 메소드 대체
@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,40 @@
package com.gamegoo.gamegoo_v2.account.auth.security;

import com.gamegoo.gamegoo_v2.account.auth.domain.Role;
import com.gamegoo.gamegoo_v2.account.member.repository.MemberRepository;
import com.gamegoo.gamegoo_v2.account.member.domain.Member;
import com.gamegoo.gamegoo_v2.core.exception.JwtAuthException;
import com.gamegoo.gamegoo_v2.core.exception.common.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

private final MemberRepository memberRepository;

public UserDetails loadUserByMemberId(Long memberId) throws UsernameNotFoundException {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new JwtAuthException(ErrorCode.MEMBER_NOT_FOUND));

// 탈퇴한 회원인 경우 에러 발생
if (member.isBlind()) {
throw new JwtAuthException(ErrorCode.INACTIVE_MEMBER);
}

return new CustomUserDetails(member.getId(), Role.MEMBER);
}

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
throw new UnsupportedOperationException("loadUserByUsername(String username) is not supported. Use " +
"loadUserByMemberIdAndSocialId(Long socialId, String role) instead.");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.gamegoo.gamegoo_v2.account.auth.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.gamegoo.gamegoo_v2.core.common.ApiResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.PrintWriter;

import static com.gamegoo.gamegoo_v2.core.exception.common.ErrorCode._UNAUTHORIZED;

@Slf4j
@Component
public class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setContentType("application/json; charset=UTF-8");
response.setStatus(401);
PrintWriter writer = response.getWriter();

// 실패 응답
ApiResponse<Object> apiResponse =
ApiResponse.builder()
.code(_UNAUTHORIZED.getCode())
.message(_UNAUTHORIZED.getMessage())
.data(null)
.status(_UNAUTHORIZED.getStatus())
.build();
try {
ObjectMapper objectMapper = new ObjectMapper();
String jsonResponse = objectMapper.writeValueAsString(apiResponse);
writer.write(jsonResponse);
} catch (NullPointerException e) {
log.error("응답 메시지 작성 에러", e);
} finally {
if (writer != null) {
writer.flush();
writer.close();
}
}
}

}

Loading
Loading