Skip to content

Commit

Permalink
feat: 게스트 로그인 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
sooyoungh committed Apr 19, 2024
1 parent 5e0b4c2 commit c72ebcc
Show file tree
Hide file tree
Showing 14 changed files with 151 additions and 18 deletions.
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 @@ -28,7 +28,10 @@ public enum AuthErrorCode implements ErrorCode {
OAUTH_UNSUPPORTED(UNAUTHORIZED, "해당 OAuth 타입은 지원하지 않습니다."),

NICKNAME_ALREADY_EXIST(BAD_REQUEST, "중복된 닉네임입니다."),
INVALID_BLANK_NICKNAME(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 @@ -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 Down Expand Up @@ -30,12 +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); // 게스트 구분용 필드 추가
}
}
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 @@ -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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.pyonsnalcolor.member.security;

import com.pyonsnalcolor.member.GuestValidator;
import com.pyonsnalcolor.member.RedisUtil;
import com.pyonsnalcolor.exception.PyonsnalcolorAuthException;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -40,6 +41,9 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private RedisUtil redisUtil;

@Autowired
private GuestValidator guestValidator;

private String OAUTH_ID = "oAuthId";

@Override
Expand All @@ -53,9 +57,9 @@ protected void doFilterInternal(HttpServletRequest request,
saveAuthenticationIfValidate(accessToken);

} catch (Exception e) {
log.info("JwtAuthenticationFilter doFilterInternal() ");
request.setAttribute("exception", e);
}

filterChain.doFilter(request, response);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.pyonsnalcolor.member.dto.TokenDto;
import com.pyonsnalcolor.exception.PyonsnalcolorAuthException;
import com.pyonsnalcolor.member.enumtype.Role;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -31,7 +32,9 @@ public class JwtTokenProvider {
private long accessTokenValidity;
private long refreshTokenValidity;
private SecretKey secretKey;
private String OAUTH_ID = "oAuthId";
private static final String OAUTH_ID = "oAuthId";
private static final String GUEST = "GUEST";
private static final String ROLE = "ROLE";

public JwtTokenProvider(@Value("${jwt.secret}") String jwtSecretKey,
@Value("${jwt.access-token.validity}") long accessTokenValidity,
Expand All @@ -41,7 +44,7 @@ public JwtTokenProvider(@Value("${jwt.secret}") String jwtSecretKey,
this.secretKey = Keys.hmacShaKeyFor(jwtSecretKey.getBytes());
}

public TokenDto createAccessAndRefreshTokenDto(String oauthId) {
public TokenDto createAccessAndRefreshTokenDto(Role role, String oauthId) {
String accessToken = createBearerTokenWithValidity(oauthId, accessTokenValidity);
String refreshToken = createBearerTokenWithValidity(oauthId, refreshTokenValidity);

Expand All @@ -52,11 +55,11 @@ public TokenDto createAccessAndRefreshTokenDto(String oauthId) {
}

public String createBearerTokenWithValidity(String oauthId, long tokenValidity){
String accessToken = createTokenWithValidity(oauthId, tokenValidity);
String accessToken = createTokenWithRoleAndValidity(oauthId, tokenValidity);
return createBearerHeader(accessToken);
}

private String createTokenWithValidity(String oAuthId, long tokenValidity){
private String createTokenWithRoleAndValidity(String oAuthId, long tokenValidity){
Date now = new Date();
Date expirationAt = new Date(now.getTime() + tokenValidity);

Expand Down
33 changes: 32 additions & 1 deletion src/main/java/com/pyonsnalcolor/member/service/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import static com.pyonsnalcolor.exception.model.AuthErrorCode.*;
Expand All @@ -42,6 +44,7 @@ public class AuthService {
private String bearerHeader;

private static final String LOGOUT_BLACKLIST = "logout";
private static final String GUEST = "GUEST";

private final MemberRepository memberRepository;
private final PushProductStoreRepository pushProductStoreRepository;
Expand Down Expand Up @@ -69,12 +72,13 @@ private LoginResponseDto reLogin(Member member) {
.accessToken(newAccessToken)
.refreshToken(refreshToken)
.isFirstLogin(false)
.isGuest(false)
.build();
}

public LoginResponseDto join(OAuthType oAuthType, String email) {
String oauthId = oAuthType.addOAuthTypeHeaderWithEmail(email);
TokenDto tokenDto = jwtTokenProvider.createAccessAndRefreshTokenDto(oauthId);
TokenDto tokenDto = jwtTokenProvider.createAccessAndRefreshTokenDto(Role.ROLE_USER, oauthId);
String accessToken = tokenDto.getAccessToken();
String refreshToken = tokenDto.getRefreshToken();

Expand All @@ -93,6 +97,7 @@ public LoginResponseDto join(OAuthType oAuthType, String email) {
.accessToken(accessToken)
.refreshToken(refreshToken)
.isFirstLogin(true)
.isGuest(false)
.build();
}

Expand Down Expand Up @@ -126,6 +131,7 @@ public LoginResponseDto reissueAccessToken(TokenDto tokenDto) {

return LoginResponseDto.builder()
.isFirstLogin(false)
.isGuest(false)
.accessToken(newAccessToken)
.refreshToken(refreshToken)
.build();
Expand Down Expand Up @@ -178,4 +184,29 @@ public JoinStatusResponseDto getJoinStatus(LoginRequestDto loginRequestDto) {
.isJoined(isJoined)
.build();
}


public LoginResponseDto guestLogin() {
log.info("게스트 로그인 시도 : " + new Date());
TokenDto tokenDto = jwtTokenProvider.createAccessAndRefreshTokenDto(Role.ROLE_GUEST, Role.ROLE_GUEST.toString());
String token = tokenDto.getRefreshToken();

Optional<Member> findMember = memberRepository.findByoAuthId(Role.ROLE_GUEST.toString());

if (findMember.isEmpty()) {
Member member = Member.builder()
.refreshToken(token)
.oAuthId(Role.ROLE_GUEST.toString()) // PK
.role(Role.ROLE_GUEST)
.build();
memberRepository.save(member);
}

return LoginResponseDto.builder()
.accessToken(token)
.refreshToken(token)
.isFirstLogin(true)
.isGuest(true)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.pyonsnalcolor.exception.PyonsnalcolorAuthException;
import com.pyonsnalcolor.exception.PyonsnalcolorProductException;
import com.pyonsnalcolor.member.GuestValidator;
import com.pyonsnalcolor.member.dto.FavoriteRequestDto;
import com.pyonsnalcolor.member.dto.MemberInfoResponseDto;
import com.pyonsnalcolor.member.dto.NicknameRequestDto;
Expand Down
Loading

0 comments on commit c72ebcc

Please sign in to comment.