diff --git a/src/main/java/Gotcha/common/config/SecurityFilterConfig.java b/src/main/java/Gotcha/common/config/SecurityFilterConfig.java index d4e6a283..bc732a93 100644 --- a/src/main/java/Gotcha/common/config/SecurityFilterConfig.java +++ b/src/main/java/Gotcha/common/config/SecurityFilterConfig.java @@ -4,6 +4,7 @@ import Gotcha.common.jwt.TokenProvider; import Gotcha.common.jwt.filter.JwtAuthenticationFilter; import Gotcha.common.jwt.filter.JwtExceptionFilter; +import Gotcha.domain.guestUser.service.GuestUserService; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -17,10 +18,11 @@ public class SecurityFilterConfig { private final TokenProvider tokenProvider; private final ObjectMapper objectMapper; private final BlackListTokenService blackListTokenService; + private final GuestUserService guestUserService; @Bean public JwtAuthenticationFilter authenticationFilter() { - return new JwtAuthenticationFilter(userDetailsService, tokenProvider, blackListTokenService); + return new JwtAuthenticationFilter(userDetailsService, tokenProvider, blackListTokenService, guestUserService); } @Bean diff --git a/src/main/java/Gotcha/common/constants/SecurityConstants.java b/src/main/java/Gotcha/common/constants/SecurityConstants.java index d3d137fa..4363f066 100644 --- a/src/main/java/Gotcha/common/constants/SecurityConstants.java +++ b/src/main/java/Gotcha/common/constants/SecurityConstants.java @@ -3,7 +3,7 @@ public class SecurityConstants { public static final String[] PUBLIC_ENDPOINTS = { "/swagger-resources/**", "/swagger-ui/**", "/v3/api-docs/**", - "/webjars/**", "/error", "/api/v1/auth/**", "/api/v1/users/nickname-check" + "/webjars/**", "/error", "/api/v1/auth/**", "/api/v1/users/nickname-check", "/api/v1/guest/sign-in" }; public static final String[] ADMIN_ENDPOINTS = {"/api/v1/admin/**"}; diff --git a/src/main/java/Gotcha/common/jwt/JwtHelper.java b/src/main/java/Gotcha/common/jwt/JwtHelper.java index 4a907cfc..cfb9abd4 100644 --- a/src/main/java/Gotcha/common/jwt/JwtHelper.java +++ b/src/main/java/Gotcha/common/jwt/JwtHelper.java @@ -4,6 +4,8 @@ import Gotcha.common.jwt.exception.JwtExceptionCode; import Gotcha.common.util.CookieUtil; import Gotcha.domain.auth.dto.TokenDto; +import Gotcha.domain.guestUser.entity.GuestUser; +import Gotcha.domain.user.entity.Role; import Gotcha.domain.user.entity.User; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -25,30 +27,42 @@ public class JwtHelper { public TokenDto createToken(User user) { Long userId = user.getId(); - String email = user.getEmail(); + String username = user.getEmail(); String role = String.valueOf(user.getRole()); - String accessToken = TOKEN_PREFIX + tokenProvider.createAccessToken(role, userId, email); - String refreshToken = tokenProvider.createRefreshToken(role, userId, email); + String accessToken = TOKEN_PREFIX + tokenProvider.createAccessToken(role, userId, username); + String refreshToken = tokenProvider.createRefreshToken(role, userId, username); - refreshTokenService.saveRefreshToken(email, refreshToken); + refreshTokenService.saveRefreshToken(username, refreshToken); + return new TokenDto(accessToken, refreshToken); + } + + public TokenDto createGuestToken(GuestUser guestUser) { + Long userId = guestUser.getGuestId(); + String nickname = guestUser.getGuestNickname(); + String role = String.valueOf(Role.GUEST); + + String accessToken = TOKEN_PREFIX + tokenProvider.createAccessToken(role, userId, nickname); + String refreshToken = tokenProvider.createRefreshToken(role, userId, nickname); + + refreshTokenService.saveRefreshToken(nickname, refreshToken); return new TokenDto(accessToken, refreshToken); } public TokenDto reissueToken(String refreshToken) { - String email = tokenProvider.getEmail(refreshToken); + String username = tokenProvider.getUsername(refreshToken); - if (!refreshTokenService.existedRefreshToken(email, refreshToken)) + if (!refreshTokenService.existedRefreshToken(username, refreshToken)) throw new CustomException(JwtExceptionCode.REFRESH_TOKEN_NOT_FOUND); Long userId = tokenProvider.getUserId(refreshToken); String role = tokenProvider.getRole(refreshToken); - String newAccessToken = TOKEN_PREFIX + tokenProvider.createAccessToken(role, userId, email); - String newRefreshToken = tokenProvider.createRefreshToken(role, userId, email); + String newAccessToken = TOKEN_PREFIX + tokenProvider.createAccessToken(role, userId, username); + String newRefreshToken = tokenProvider.createRefreshToken(role, userId, username); refreshTokenService.deleteRefreshToken(refreshToken); - refreshTokenService.saveRefreshToken(email, newRefreshToken); + refreshTokenService.saveRefreshToken(username, newRefreshToken); return new TokenDto(newAccessToken, newRefreshToken); } diff --git a/src/main/java/Gotcha/common/jwt/RefreshTokenService.java b/src/main/java/Gotcha/common/jwt/RefreshTokenService.java index 907b8652..64e76a16 100644 --- a/src/main/java/Gotcha/common/jwt/RefreshTokenService.java +++ b/src/main/java/Gotcha/common/jwt/RefreshTokenService.java @@ -16,20 +16,20 @@ public class RefreshTokenService { @Value("${token.refresh.in-redis}") private long REFRESH_EXPIRATION; - public void saveRefreshToken(String email, String refreshToken) { - String key = REFRESH_TOKEN_KEY_PREFIX + email; + public void saveRefreshToken(String username, String refreshToken) { + String key = REFRESH_TOKEN_KEY_PREFIX + username; redisUtil.setData(key, refreshToken); redisUtil.setDataExpire(key, REFRESH_EXPIRATION); } public void deleteRefreshToken(String refreshToken) { - String email = tokenProvider.getEmail(refreshToken); - String key = REFRESH_TOKEN_KEY_PREFIX + email; + String username = tokenProvider.getUsername(refreshToken); + String key = REFRESH_TOKEN_KEY_PREFIX + username; redisUtil.deleteData(key); } - public boolean existedRefreshToken(String email, String requestRefreshToken) { - String key = REFRESH_TOKEN_KEY_PREFIX + email; + public boolean existedRefreshToken(String username, String requestRefreshToken) { + String key = REFRESH_TOKEN_KEY_PREFIX + username; String storedRefreshToken = (String) redisUtil.getData(key); diff --git a/src/main/java/Gotcha/common/jwt/TokenProvider.java b/src/main/java/Gotcha/common/jwt/TokenProvider.java index af64f8f0..21ef2ad1 100644 --- a/src/main/java/Gotcha/common/jwt/TokenProvider.java +++ b/src/main/java/Gotcha/common/jwt/TokenProvider.java @@ -68,7 +68,7 @@ private Claims getClaims(String token) { .getPayload(); } - public String getEmail(String token) { + public String getUsername(String token) { return getClaims(token).getSubject(); } diff --git a/src/main/java/Gotcha/common/jwt/UserDetailsServiceImpl.java b/src/main/java/Gotcha/common/jwt/UserDetailsServiceImpl.java index 5eede8a9..6c67c517 100644 --- a/src/main/java/Gotcha/common/jwt/UserDetailsServiceImpl.java +++ b/src/main/java/Gotcha/common/jwt/UserDetailsServiceImpl.java @@ -1,5 +1,6 @@ package Gotcha.common.jwt; +import Gotcha.common.jwt.userDetails.SecurityUserDetails; import Gotcha.domain.user.entity.User; import Gotcha.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/Gotcha/common/jwt/filter/JwtAuthenticationFilter.java b/src/main/java/Gotcha/common/jwt/filter/JwtAuthenticationFilter.java index 1f446564..4afa9cee 100644 --- a/src/main/java/Gotcha/common/jwt/filter/JwtAuthenticationFilter.java +++ b/src/main/java/Gotcha/common/jwt/filter/JwtAuthenticationFilter.java @@ -4,6 +4,10 @@ import Gotcha.common.jwt.BlackListTokenService; import Gotcha.common.jwt.exception.JwtExceptionCode; import Gotcha.common.jwt.TokenProvider; +import Gotcha.common.jwt.userDetails.GuestUserDetails; +import Gotcha.domain.guestUser.entity.GuestUser; +import Gotcha.domain.guestUser.service.GuestUserService; +import Gotcha.domain.user.entity.Role; import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -32,6 +36,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final UserDetailsService userDetailsService; private final TokenProvider tokenProvider; private final BlackListTokenService blackListTokenService; + private final GuestUserService guestUserService; private static final String SPECIAL_CHARACTERS_PATTERN = "[`':;|~!@#$%()^&*+=?/{}\\[\\]\\\"\\\\\"]+$"; private final AntPathMatcher antPathMatcher = new AntPathMatcher(); @@ -52,8 +57,17 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse String accessToken = resolveAccessToken(response, accessTokenHeader); - String username = tokenProvider.getEmail(accessToken); - UserDetails userDetails = userDetailsService.loadUserByUsername(username); + String role = tokenProvider.getRole(accessToken); + UserDetails userDetails; + + if (String.valueOf(Role.GUEST).equals(role)) { + Long guestId = tokenProvider.getUserId(accessToken); + GuestUser guestUser = guestUserService.getGuestUser(guestId); + userDetails = new GuestUserDetails(guestUser); + } else { + String username = tokenProvider.getUsername(accessToken); + userDetails = userDetailsService.loadUserByUsername(username); + } Authentication auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); diff --git a/src/main/java/Gotcha/common/jwt/userDetails/GuestUserDetails.java b/src/main/java/Gotcha/common/jwt/userDetails/GuestUserDetails.java new file mode 100644 index 00000000..db1380a3 --- /dev/null +++ b/src/main/java/Gotcha/common/jwt/userDetails/GuestUserDetails.java @@ -0,0 +1,63 @@ +package Gotcha.common.jwt.userDetails; + +import Gotcha.common.security.CustomGrantedAuthority; +import Gotcha.domain.guestUser.entity.GuestUser; +import Gotcha.domain.user.entity.Role; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@NoArgsConstructor +@Getter +@JsonIgnoreProperties(ignoreUnknown = true) +public class GuestUserDetails implements UserDetails { + private GuestUser guestUser; + + public GuestUserDetails(GuestUser guestUser) { + this.guestUser = guestUser; + } + + @Override + public Collection getAuthorities() { + return List.of(new CustomGrantedAuthority(Role.GUEST.getValue())); + } + + @Override + public String getPassword() { + return null; + } + + public Long getGuestId() { + return guestUser.getGuestId(); + } + + @Override + public String getUsername() { + return guestUser.getGuestNickname(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/Gotcha/common/jwt/SecurityUserDetails.java b/src/main/java/Gotcha/common/jwt/userDetails/SecurityUserDetails.java similarity index 97% rename from src/main/java/Gotcha/common/jwt/SecurityUserDetails.java rename to src/main/java/Gotcha/common/jwt/userDetails/SecurityUserDetails.java index abeed6f0..b42e0442 100644 --- a/src/main/java/Gotcha/common/jwt/SecurityUserDetails.java +++ b/src/main/java/Gotcha/common/jwt/userDetails/SecurityUserDetails.java @@ -1,4 +1,4 @@ -package Gotcha.common.jwt; +package Gotcha.common.jwt.userDetails; import Gotcha.common.security.CustomGrantedAuthority; import Gotcha.domain.user.entity.User; diff --git a/src/main/java/Gotcha/common/util/RedisUtil.java b/src/main/java/Gotcha/common/util/RedisUtil.java index f853bbb7..78efb085 100644 --- a/src/main/java/Gotcha/common/util/RedisUtil.java +++ b/src/main/java/Gotcha/common/util/RedisUtil.java @@ -16,7 +16,7 @@ public Object getData(String key) { return redisTemplate.opsForValue().get(key); } - public void setData(String key, String value) { + public void setData(String key, Object value) { redisTemplate.opsForValue().set(key, value); } diff --git a/src/main/java/Gotcha/domain/guestUser/api/GuestUserApi.java b/src/main/java/Gotcha/domain/guestUser/api/GuestUserApi.java new file mode 100644 index 00000000..361cb173 --- /dev/null +++ b/src/main/java/Gotcha/domain/guestUser/api/GuestUserApi.java @@ -0,0 +1,25 @@ +package Gotcha.domain.guestUser.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +@Tag(name = "[게스트 API]", description = "게스트 관련 API") +public interface GuestUserApi { + @Operation(summary = "게스트 로그인", description = "게스트 로그인 API") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "게스트 로그인 성공", + content = @Content(mediaType = "application/json", examples = { + @ExampleObject(value = """ + { + "accessToken": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLsi5zrgYTrn6zsmrTrgpntg4AiLCJyb2xlIjoiR1VFU1QiLCJ1c2VySWQiOjY3OTkyNTU1MjE4ODE1NzMyMDEsImlzcyI6ImdvdGNoYSEiLCJpYXQiOjE3NDI5OTUyMjksImV4cCI6MTc0Mjk5NzAyOX0.jwE4E2ZS0jNaKFifOQjeDUFGhlfaqXOyN_kxgfC6rLw" + } + """) + })) + }) + ResponseEntity guestSignIn(); +} diff --git a/src/main/java/Gotcha/domain/guestUser/controller/GuestUserController.java b/src/main/java/Gotcha/domain/guestUser/controller/GuestUserController.java new file mode 100644 index 00000000..45d882ba --- /dev/null +++ b/src/main/java/Gotcha/domain/guestUser/controller/GuestUserController.java @@ -0,0 +1,43 @@ +package Gotcha.domain.guestUser.controller; + +import Gotcha.common.util.CookieUtil; +import Gotcha.domain.auth.dto.TokenDto; +import Gotcha.domain.guestUser.api.GuestUserApi; +import Gotcha.domain.guestUser.service.GuestUserService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + +import static Gotcha.common.jwt.JwtProperties.REFRESH_COOKIE_VALUE; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/guest") +public class GuestUserController implements GuestUserApi { + private final GuestUserService guestUserService; + private final CookieUtil cookieUtil; + + @PostMapping("/sign-in") + public ResponseEntity guestSignIn(){ + TokenDto tokenDto = guestUserService.createGuestAccessToken(); + + return createTokenRes(tokenDto); + } + + private ResponseEntity createTokenRes(TokenDto tokenDto) { + Map responseData = new HashMap<>(); + responseData.put("accessToken", tokenDto.accessToken()); + + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, + cookieUtil.createCookie(REFRESH_COOKIE_VALUE, + tokenDto.refreshToken()).toString()) + .body(responseData); + } +} diff --git a/src/main/java/Gotcha/domain/guestUser/entity/GuestUser.java b/src/main/java/Gotcha/domain/guestUser/entity/GuestUser.java new file mode 100644 index 00000000..ef5671d8 --- /dev/null +++ b/src/main/java/Gotcha/domain/guestUser/entity/GuestUser.java @@ -0,0 +1,16 @@ +package Gotcha.domain.guestUser.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GuestUser { + Long guestId; + String guestNickname; +} diff --git a/src/main/java/Gotcha/domain/guestUser/service/GuestUserService.java b/src/main/java/Gotcha/domain/guestUser/service/GuestUserService.java new file mode 100644 index 00000000..b24a410e --- /dev/null +++ b/src/main/java/Gotcha/domain/guestUser/service/GuestUserService.java @@ -0,0 +1,49 @@ +package Gotcha.domain.guestUser.service; + +import Gotcha.common.jwt.JwtHelper; +import Gotcha.common.util.RedisUtil; +import Gotcha.domain.auth.dto.TokenDto; +import Gotcha.domain.guestUser.entity.GuestUser; +import Gotcha.domain.guestUser.util.RandomNicknameGenerator; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@RequiredArgsConstructor +@Service +public class GuestUserService { + private final JwtHelper jwtHelper; + private final RedisUtil redisUtil; + + private static final long GUEST_TTL_SECONDS = 30 * 60; + + public TokenDto createGuestAccessToken() { + //무작위 아이디 값 생성 + Long guestId; + do{ + guestId = UUID.randomUUID().getMostSignificantBits(); + }while(redisUtil.existed("guest:" + guestId)); + //무작위 닉네임 생성 + String nickname = RandomNicknameGenerator.generateNickname(); + + //게스트 유저 생성 + GuestUser guestUser = GuestUser.builder() + .guestId(guestId) + .guestNickname(nickname) + .build(); + + //게스트 유저 정보를 Redis에 저장 + redisUtil.setData("guest:" + guestId, guestUser); + redisUtil.setDataExpire("guest:" + guestId, GUEST_TTL_SECONDS); + + //게스트 유저 토큰 생성 + return jwtHelper.createGuestToken(guestUser); + } + + public GuestUser getGuestUser(Long guestId){ + //매 요청 시 게스트가 활동 중이라는 의미이므로 데이터가 삭제되지 않게 하기 위해 TTL을 갱신 + redisUtil.setDataExpire("guest:" + guestId, GUEST_TTL_SECONDS); + return (GuestUser) redisUtil.getData("guest:" + guestId); + } +} diff --git a/src/main/java/Gotcha/domain/guestUser/util/RandomNicknameGenerator.java b/src/main/java/Gotcha/domain/guestUser/util/RandomNicknameGenerator.java new file mode 100644 index 00000000..619e7882 --- /dev/null +++ b/src/main/java/Gotcha/domain/guestUser/util/RandomNicknameGenerator.java @@ -0,0 +1,30 @@ +package Gotcha.domain.guestUser.util; + +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +public class RandomNicknameGenerator { + private static final List adjectives = Arrays.asList( + "귀여운", "무서운", "배고픈", "졸린", "쓸쓸한", "취한", "웃긴", "멋진", "시끄러운", "느린" + ); + + private static final List nouns = Arrays.asList( + "고양이", "강아지", "곰", "너구리", "햄스터", "호랑이", "펭귄", "낙타", "토끼", "벌새" + ); + + private static final Random random = new Random(); + + public static String generateNickname() { + String nickname; + + do { + String adj = adjectives.get(random.nextInt(adjectives.size())); + String noun = nouns.get(random.nextInt(nouns.size())); + nickname = adj + noun; + } while (nickname.length() > 6); + + return nickname; + } +} + diff --git a/src/main/java/Gotcha/domain/user/entity/Role.java b/src/main/java/Gotcha/domain/user/entity/Role.java index 0d812f10..c0c1c8cf 100644 --- a/src/main/java/Gotcha/domain/user/entity/Role.java +++ b/src/main/java/Gotcha/domain/user/entity/Role.java @@ -6,7 +6,7 @@ @RequiredArgsConstructor @Getter public enum Role { - USER("ROLE_USER"), ADMIN("ROLE_ADMIN"); + USER("ROLE_USER"), ADMIN("ROLE_ADMIN"), GUEST("ROLE_GUEST"); private final String value; }