Skip to content

Commit

Permalink
Merge pull request #95 from YAPP-Github/feature/guest-login
Browse files Browse the repository at this point in the history
Feature/guest login 게스트 로그인 추가 및 home-banner 이벤트 관련 조회 기능 구현
  • Loading branch information
sooyoungh authored Apr 19, 2024
2 parents c1ce9cd + c72ebcc commit 2faec68
Show file tree
Hide file tree
Showing 48 changed files with 885 additions and 237 deletions.
7 changes: 5 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ dependencies {
runtimeOnly 'com.mysql:mysql-connector-j:8.0.33'

// Test
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
testImplementation 'junit:junit'
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0'
testImplementation 'org.junit.platform:junit-platform-launcher:1.9.0'
testImplementation 'org.junit.vintage:junit-vintage-engine:5.9.0'

// Security
implementation 'org.springframework.security:spring-security-core:5.5.0'
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/pyonsnalcolor/admin/AdminController.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ public ResponseEntity<String> getAASAData() throws IOException {
.body(new String(jsonData));
}

@GetMapping("/")
public ResponseEntity healthCheck() {
return new ResponseEntity(HttpStatus.OK);
}

// ios 테스트용
@GetMapping("/fcm/test")
public ResponseEntity fcmTest(DeviceTokenRequestDto deviceTokenRequestDto) throws ExecutionException, InterruptedException {
Expand Down
16 changes: 13 additions & 3 deletions src/main/java/com/pyonsnalcolor/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.configuration.WebSecurityCustomizer;
Expand All @@ -19,8 +20,8 @@
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.client.RestTemplate;

@EnableWebSecurity
@Configuration
@EnableWebSecurity//(debug = true) // Spring Security 활성화
public class SecurityConfig {

@Autowired
Expand All @@ -35,7 +36,8 @@ public class SecurityConfig {
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring()
.antMatchers( "/resources/**",
.antMatchers(
"/resources/**",
"/v3/api-docs/**",
"/swagger-ui/**",
"/health-check",
Expand All @@ -55,7 +57,15 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.and()
.authorizeRequests()
.antMatchers("/auth/**", "/promotions/**", "/fcm/**", "/manage/**").permitAll()
.antMatchers("/member/**").hasRole("USER")
.antMatchers("/products/pb-products", "/products/event-products").authenticated() // 전체 조회
.antMatchers("/products/pb-products/**/reviews/**"
, "/products/event-products/**/reviews/**").hasRole("USER") // 리뷰 작성/좋아요/싫어요
.antMatchers("/products/pb-products/**", "/products/event-products/**").authenticated() // 단건 조회
.antMatchers("/products/**").authenticated() // 검색, 메타 데이터 등
.antMatchers(HttpMethod.POST, "/favorites").hasRole("USER") // 찜하기 등록
.antMatchers(HttpMethod.DELETE, "/favorites").hasRole("USER") // 찜하기 삭제
.antMatchers(HttpMethod.PATCH, "/member/profile", "/member/nickname").hasRole("USER") // 프로필 수정
.antMatchers("/member/**").authenticated()
.anyRequest().authenticated()
.and()
.exceptionHandling((exceptions) -> exceptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ public enum AuthErrorCode implements ErrorCode {
// OAuth
OAUTH_UNAUTHORIZED(UNAUTHORIZED, "OAuth 인증에 실패했습니다."),
EMAIL_UNAUTHORIZED(UNAUTHORIZED, "이메일이 유효하지 않습니다."),
OAUTH_UNSUPPORTED(UNAUTHORIZED, "해당 OAuth 타입은 지원하지 않습니다.");
OAUTH_UNSUPPORTED(UNAUTHORIZED, "해당 OAuth 타입은 지원하지 않습니다."),

NICKNAME_ALREADY_EXIST(BAD_REQUEST, "중복된 닉네임입니다."),
INVALID_BLANK_NICKNAME(BAD_REQUEST, "닉네임은 공백이 아닌 값을 입력해주세요."),

// 게스트
GUEST_FORBIDDEN(FORBIDDEN, "게스트는 접근 불가합니다.");

private final HttpStatus httpStatus;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
package com.pyonsnalcolor.handler;

import com.pyonsnalcolor.exception.PyonsnalcolorAuthException;
import com.pyonsnalcolor.member.enumtype.Role;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;

import static com.pyonsnalcolor.exception.model.AuthErrorCode.GUEST_FORBIDDEN;

@Slf4j
@Component
Expand All @@ -20,11 +29,25 @@ public class JwtAccessDeniedHandler implements AccessDeniedHandler {
private HandlerExceptionResolver resolver;

@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) {
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
log.info("JwtAccessDeniedHandler authentication : {}", authentication.getAuthorities());

if (isGuestUser(authentication)) {
// 게스트 사용자인 경우 커스텀 예외를 던집니다.
resolver.resolveException(request, response, null, new PyonsnalcolorAuthException(GUEST_FORBIDDEN));
} else {
resolver.resolveException(request, response, null, accessDeniedException);
}
}

private boolean isGuestUser(Authentication authentication) {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

Exception e = (Exception) request.getAttribute("exception"); // req에 담았던 예외 꺼내기
resolver.resolveException(request, response, null, e);
boolean hasGuestRole = authorities.stream()
.anyMatch(authority -> authority.getAuthority().equals(Role.ROLE_GUEST.toString()));
return hasGuestRole;
}
}
30 changes: 30 additions & 0 deletions src/main/java/com/pyonsnalcolor/member/GuestValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.pyonsnalcolor.member;

import com.pyonsnalcolor.exception.PyonsnalcolorAuthException;
import com.pyonsnalcolor.member.security.JwtTokenProvider;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import static com.pyonsnalcolor.exception.model.AuthErrorCode.*;

@Slf4j
@Component
@RequiredArgsConstructor
public class GuestValidator {
private static final String ROLE = "ROLE";
private static final String GUEST = "GUEST";

private final JwtTokenProvider jwtTokenProvider;

public boolean validateIfGuest(String token) {
Claims claims = jwtTokenProvider.getClaims(token);
String role = (String) claims.get(ROLE);

if (role.equals(GUEST)) {
throw new PyonsnalcolorAuthException(GUEST_FORBIDDEN);
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,11 @@ public ResponseEntity<LoginResponseDto> testLogin(
LoginResponseDto loginResponseDto = authService.join(oAuthType, email);
return new ResponseEntity(loginResponseDto, HttpStatus.OK);
}
}

@Operation(summary = "게스트 로그인", description = "둘러보기 버튼 누를 시, 게스트용 토큰을 반환합니다.")
@PostMapping("/guest/login")
public ResponseEntity<LoginResponseDto> guestLogin() {
LoginResponseDto loginResponseDto = authService.guestLogin();
return new ResponseEntity(loginResponseDto, HttpStatus.OK);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
import com.pyonsnalcolor.member.dto.NicknameRequestDto;
import com.pyonsnalcolor.member.dto.TokenDto;
import com.pyonsnalcolor.member.service.AuthService;
import com.pyonsnalcolor.member.service.MemberService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.validation.Valid;

Expand All @@ -22,6 +25,7 @@
public class MemberController {

private final AuthService authService;
private final MemberService memberService;

@Operation(summary = "로그아웃", description = "사용자의 JWT 토큰을 무효화합니다.")
@Parameter(name = "tokenDto", description = "JWT 로그인 토큰")
Expand All @@ -36,18 +40,38 @@ public ResponseEntity logout(@RequestBody TokenDto tokenDto) {
public ResponseEntity<MemberInfoResponseDto> getMemberInfo(
@Parameter(hidden = true) @AuthMemberId Long memberId
) {
MemberInfoResponseDto memberInfoResponseDto = authService.getMemberInfo(memberId);
MemberInfoResponseDto memberInfoResponseDto = memberService.getMemberInfo(memberId);
return new ResponseEntity(memberInfoResponseDto, HttpStatus.OK);
}

@Operation(summary = "닉네임 변경", description = "사용자의 닉네임을 변경합니다.")
@Parameter(name = "nickname", description = "변경할 닉네임, 15자 이내 특수문자 허용안됨")
@Operation(summary = "이전) 닉네임 변경", description = "사용자의 닉네임을 변경합니다.")
@PatchMapping("/nickname")
public ResponseEntity<TokenDto> updateNickname(
public ResponseEntity<Void> updateNickname(
@RequestBody @Valid NicknameRequestDto nicknameRequestDto,
@Parameter(hidden = true) @AuthMemberId Long memberId
) {
authService.updateNickname(memberId, nicknameRequestDto);
memberService.updateProfile(memberId, null, nicknameRequestDto);
return new ResponseEntity(HttpStatus.OK);
}

@Operation(summary = "프로필 변경", description = "사용자의 닉네임, 프로필 사진을 변경합니다.")
@PatchMapping(value = "/profile", consumes = { MediaType.MULTIPART_FORM_DATA_VALUE })
public ResponseEntity<Void> updateProfile(
@RequestPart(value = "nicknameRequestDto", required = false) @Valid NicknameRequestDto nicknameRequestDto,
@RequestPart(value="imageFile", required = false) MultipartFile imageFile,
@Parameter(hidden = true) @AuthMemberId Long memberId
) {
memberService.updateProfile(memberId, imageFile, nicknameRequestDto);
return new ResponseEntity(HttpStatus.OK);
}

@Operation(summary = "닉네임 중복 검사", description = "닉네임 중복 여부를 검사합니다.")
@PostMapping("/nickname")
public ResponseEntity<Void> validateNickname(
@RequestBody @Valid NicknameRequestDto nicknameRequestDto
) {

memberService.validateNicknameFormat(nicknameRequestDto);
return new ResponseEntity(HttpStatus.OK);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ public class LoginResponseDto {
@NotBlank
private Boolean isFirstLogin;

@Schema(description = "게스트 유저인지 구분용", required = true)
@NotBlank
private Boolean isGuest;

@NotBlank
private String accessToken;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.pyonsnalcolor.member.dto;

import com.pyonsnalcolor.member.entity.Member;
import com.pyonsnalcolor.member.enumtype.Role;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;

Expand All @@ -18,6 +19,9 @@ public class MemberInfoResponseDto {
@NotBlank
private String oauthType;

@NotBlank
private String profileImage;

@NotBlank
private Long memberId;

Expand All @@ -27,11 +31,17 @@ public class MemberInfoResponseDto {
@NotBlank
private String email;

@Schema(description = "게스트 유저인지 구분용", required = true)
@NotBlank
private Boolean isGuest;

public MemberInfoResponseDto(Member member) {
this.memberId = member.getId();
this.oauthId = member.getOAuthId();
this.oauthType = member.getOAuthType().toString();
this.nickname = member.getNickname();
this.profileImage = member.getProfileImage();
this.email = member.getEmail();
this.isGuest = member.getRole().equals(Role.ROLE_GUEST); // 게스트 구분용 필드 추가
}
}
}
7 changes: 7 additions & 0 deletions src/main/java/com/pyonsnalcolor/member/entity/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ public class Member extends BaseTimeEntity {
@Pattern(regexp="^[0-9a-zA-Zㄱ-ㅎ가-힣 ]{1,15}")
private String nickname;

@Column(name = "profile_image")
private String profileImage;

@Column(length = 500)
private String refreshToken;

Expand All @@ -61,4 +64,8 @@ public class Member extends BaseTimeEntity {
public void updateNickname(String updatedNickname) {
this.nickname = updatedNickname;
}

public void updateProfileImage(String updatedProfileImage) {
this.profileImage = updatedProfileImage;
}
}
9 changes: 7 additions & 2 deletions src/main/java/com/pyonsnalcolor/member/enumtype/Role.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package com.pyonsnalcolor.member.enumtype;

import lombok.Getter;

@Getter
public enum Role {
ROLE_USER, ROLE_GUEST, ROLE_ADMIN;
}
ROLE_USER,
ROLE_GUEST,
ROLE_ADMIN;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ public interface FavoriteRepository extends JpaRepository<Favorite, Long> {

Optional<Favorite> findByProductIdAndMemberId(String productId, Long memberId);

List<Favorite> getFavoriteByMemberId(Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
Expand All @@ -17,4 +18,6 @@ public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<String> findRefreshTokenByoAuthId(@Param("oAuthId") String oAuthId);

Optional<Member> findByRefreshToken(String refreshToken);

List<Member> findByNickname(String nickname);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import com.pyonsnalcolor.member.repository.MemberRepository;
import com.pyonsnalcolor.exception.PyonsnalcolorAuthException;
import com.pyonsnalcolor.exception.model.AuthErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class AuthUserDetailsService implements UserDetailsService {

Expand All @@ -17,6 +19,7 @@ public class AuthUserDetailsService implements UserDetailsService {

@Override
public AuthUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("loadUserByUsername() {}", username);
Member member = memberRepository.findByoAuthId(username)
.orElseThrow(() -> new PyonsnalcolorAuthException(AuthErrorCode.INVALID_OAUTH_ID));

Expand Down
Loading

0 comments on commit 2faec68

Please sign in to comment.