Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.somemore.center.service;

import com.somemore.center.domain.NEWCenter;
import com.somemore.center.repository.NEWCenterRepository;
import com.somemore.center.usecase.NEWCenterQueryUseCase;
import com.somemore.global.exception.ExceptionMessage;
import com.somemore.global.exception.NoSuchElementException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.UUID;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class NEWCenterQueryService implements NEWCenterQueryUseCase {

private final NEWCenterRepository centerRepository;

@Override
public NEWCenter getByUserId(UUID userId) {
return centerRepository.findByUserId(userId)
.orElseThrow(() -> new NoSuchElementException(ExceptionMessage.NOT_EXISTS_CENTER));
}

@Override
public UUID getIdByUserId(UUID userId) {
return getByUserId(userId).getId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.somemore.center.usecase;

import com.somemore.center.domain.NEWCenter;

import java.util.UUID;

public interface NEWCenterQueryUseCase {

NEWCenter getByUserId(UUID userId);

UUID getIdByUserId(UUID userId);
}
11 changes: 11 additions & 0 deletions src/main/java/com/somemore/global/auth/annotation/RoleId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.somemore.global.auth.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RoleId {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.somemore.global.auth.annotation;

import com.somemore.global.auth.authentication.UserIdentity;
import com.somemore.global.exception.InvalidAuthenticationException;
import org.springframework.core.MethodParameter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import static com.somemore.global.exception.ExceptionMessage.AUTHENTICATION_MISSING;
import static com.somemore.global.exception.ExceptionMessage.INVALID_PRINCIPAL_TYPE;

@Component
public class RoleIdArgumentResolver implements HandlerMethodArgumentResolver {

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RoleId.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication == null) {
throw new InvalidAuthenticationException(AUTHENTICATION_MISSING);
}

if (authentication.getPrincipal() instanceof UserIdentity principal) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

authentication == null를 통과하면
authentication.getPrincipal()도 통과가 가능한건지 여쭤보고 싶습니다
Principal이 비어 있는 경우는 없겠죠?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. authentication == null를 통과하면, authentication.getPrincipal()은 NPE 없이 호출 가능합니다. 또한, principal이 null이거나 비어 있는 값으로 설정될 가능성은 없습니다.

  2. authentication은 Spring Security의 인증 과정에서 생성되며, principal은 생성자에서 초기화됩니다.
    인증이 성공하면 authentication은 항상 principal, credentials, authorities를 포함한 상태로 SecurityContext에 저장됩니다.
    따라서 authentication이 null이 아닌 경우, principal 또한 초기화된 상태입니다.

  3. null instanceof UserIdentity
    authentication.getPrincipal()이 null이라면, instanceof 연산에서 항상 false를 반환하므로, 안전하게 처리됩니다.

  4. Spring Security는 필터 체인에서 인증 객체를 생성한 후 SecurityContext에 저장합니다.
    컨트롤러 레벨에서 접근하는 경우, 이미 인증 객체가 완전히 구성된 상태입니다.
    만약 authentication이 제대로 초기화되지 않았다면, 컨트롤러까지 요청이 도달하지 못합니다.

사실 상 로직에서는 NPE 방어를 진행했고, 인증 객체가 올바르게 구성되지 않는 상황이나 익명 유저(토큰X)는 @Secured로 필터에서 처리되어 예외가 발생할 것 같습니다~

return principal.roleId();
}

throw new InvalidAuthenticationException(INVALID_PRINCIPAL_TYPE);
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/somemore/global/auth/annotation/UserId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.somemore.global.auth.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface UserId {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.somemore.global.auth.annotation;

import com.somemore.global.auth.authentication.UserIdentity;
import com.somemore.global.exception.InvalidAuthenticationException;
import org.springframework.core.MethodParameter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import static com.somemore.global.exception.ExceptionMessage.AUTHENTICATION_MISSING;
import static com.somemore.global.exception.ExceptionMessage.INVALID_PRINCIPAL_TYPE;

@Component
public class UserIdArgumentResolver implements HandlerMethodArgumentResolver {

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(UserId.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication == null) {
throw new InvalidAuthenticationException(AUTHENTICATION_MISSING);
}

if (authentication.getPrincipal() instanceof UserIdentity principal) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분도 RoleId 에서 리뷰 남긴 부분이라 코멘트 달아두겠습니다!

return principal.userId();
}

return new InvalidAuthenticationException(INVALID_PRINCIPAL_TYPE);
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
package com.somemore.global.auth.authentication;

import com.somemore.global.auth.jwt.domain.EncodedToken;
import com.somemore.user.domain.User;
import lombok.EqualsAndHashCode;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import java.io.Serializable;
import java.util.Collection;
import java.util.List;

@EqualsAndHashCode(callSuper = true)
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private final Serializable principal;
private final UserIdentity principal;
private final transient Object credentials;

public JwtAuthenticationToken(Serializable principal,
public JwtAuthenticationToken(UserIdentity principal,
Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
Expand All @@ -35,19 +33,11 @@ public Object getPrincipal() {
return principal;
}

public static JwtAuthenticationToken of(User user, EncodedToken accessToken) {
public static JwtAuthenticationToken of(UserIdentity userIdentity, EncodedToken accessToken) {
return new JwtAuthenticationToken(
user.getId(),
userIdentity,
accessToken,
List.of(new SimpleGrantedAuthority(user.getRole().getAuthority()))
);
}

public static JwtAuthenticationToken of(String userId, String role, EncodedToken accessToken) {
return new JwtAuthenticationToken(
userId,
accessToken,
List.of(new SimpleGrantedAuthority(role))
List.of(new SimpleGrantedAuthority(userIdentity.role().getAuthority()))
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.somemore.global.auth.authentication;

import com.somemore.user.domain.UserRole;
import io.jsonwebtoken.Claims;

import java.io.Serializable;
import java.util.UUID;

public record UserIdentity(
UUID userId,
UUID roleId,
UserRole role
) implements Serializable {

public static UserIdentity of(UUID userId, UUID roleId, UserRole role) {
return new UserIdentity(userId, roleId, role);
}

public static UserIdentity from(Claims claims) {
UUID userId = UUID.fromString(claims.get("userId", String.class));
UUID roleId = UUID.fromString(claims.get("roleId", String.class));
UserRole role = UserRole.from(
claims.get("role", String.class));

return new UserIdentity(userId, roleId, role);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.somemore.global.auth.idpw.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.somemore.global.auth.authentication.UserIdentity;
import com.somemore.global.auth.jwt.domain.EncodedToken;
import com.somemore.global.auth.jwt.usecase.GenerateTokensOnLoginUseCase;
import com.somemore.user.domain.UserRole;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
Expand All @@ -16,19 +16,16 @@
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.IOException;
import java.util.UUID;

@RequiredArgsConstructor
@Slf4j
public class IdPwAuthFilter extends UsernamePasswordAuthenticationFilter {

private final AuthenticationManager authenticationManager;
private final GenerateTokensOnLoginUseCase generateTokensOnLoginUseCase;
// private final CookieUseCase cookieUseCase;
private final ObjectMapper objectMapper;

@Override
Expand All @@ -43,22 +40,11 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) {
response.setStatus(HttpServletResponse.SC_OK);
String userId = authResult.getName();
String role = extractRole(authResult);
EncodedToken accessToken =
generateTokensOnLoginUseCase.generateAuthTokensAndReturnAccessToken(
UUID.fromString(userId),
UserRole.from(role));

response.setHeader("Authorization", accessToken.getValueWithPrefix());
// cookieUseCase.setAccessToken(response, accessToken.value());
}
UserIdentity userIdentity = (UserIdentity) authResult.getPrincipal();
EncodedToken accessToken = generateTokensOnLoginUseCase.generateAuthTokensAndReturnAccessToken(userIdentity);

private static String extractRole(Authentication authResult) {
return authResult.getAuthorities().stream()
.findFirst()
.map(GrantedAuthority::getAuthority)
.orElseThrow(() -> new IllegalStateException("유저 권한 자체가 존재하지 않습니다."));
response.setHeader("Authorization", accessToken.getValueWithPrefix());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.somemore.global.auth.idpw.provider;

import com.somemore.center.usecase.NEWCenterQueryUseCase;
import com.somemore.global.auth.authentication.JwtAuthenticationToken;
import com.somemore.global.auth.authentication.UserIdentity;
import com.somemore.global.auth.jwt.domain.EncodedToken;
import com.somemore.global.auth.jwt.domain.TokenType;
import com.somemore.global.auth.jwt.usecase.JwtUseCase;
import com.somemore.user.domain.User;
import com.somemore.user.domain.UserRole;
import com.somemore.user.usecase.UserQueryUseCase;
import com.somemore.volunteer.usecase.NEWVolunteerQueryUseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
Expand All @@ -16,6 +20,8 @@
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.UUID;

@Component
@RequiredArgsConstructor
@Transactional(readOnly = true)
Expand All @@ -24,6 +30,8 @@ public class CustomAuthenticationProvider implements AuthenticationProvider {
private final JwtUseCase jwtUseCase;
private final PasswordEncoder passwordEncoder;
private final UserQueryUseCase userQueryUseCase;
private final NEWVolunteerQueryUseCase volunteerQueryUseCase;
private final NEWCenterQueryUseCase centerQueryUseCase;

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Expand All @@ -34,20 +42,32 @@ public Authentication authenticate(Authentication authentication) throws Authent

validatePassword(rawAccountPassword, user.getAccountPassword());

EncodedToken accessToken = generateAccessToken(user);
UUID userId = user.getId();
UserRole role = user.getRole();
UUID roleId = getRoleIdByUserId(role, user.getId());

UserIdentity userIdentity = UserIdentity.of(userId, roleId, role);

EncodedToken accessToken = generateAccessToken(userIdentity);

return JwtAuthenticationToken.of(user, accessToken);
return JwtAuthenticationToken.of(userIdentity, accessToken);
}

@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}

private EncodedToken generateAccessToken(User user) {
private UUID getRoleIdByUserId(UserRole role, UUID userId) {
if (role.equals(UserRole.VOLUNTEER)) {
return volunteerQueryUseCase.getIdByUserId(userId);
}
return centerQueryUseCase.getIdByUserId(userId);
}

private EncodedToken generateAccessToken(UserIdentity userIdentity) {
return jwtUseCase.generateToken(
user.getId().toString(),
user.getRole().getAuthority(),
userIdentity,
TokenType.ACCESS
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.somemore.global.auth.jwt.filter;

import com.somemore.global.auth.authentication.JwtAuthenticationToken;
import com.somemore.global.auth.authentication.UserIdentity;
import com.somemore.global.auth.jwt.domain.EncodedToken;
import com.somemore.global.auth.jwt.domain.TokenType;
import com.somemore.global.auth.jwt.exception.JwtErrorType;
Expand All @@ -14,7 +15,6 @@
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
Expand Down Expand Up @@ -49,7 +49,7 @@ protected void doFilterInternal(HttpServletRequest request,
jwtUseCase.processAccessToken(accessToken, response);

Claims claims = jwtUseCase.getClaims(accessToken);
Authentication auth = createAuthenticationToken(claims, accessToken);
JwtAuthenticationToken auth = createAuthenticationToken(claims, accessToken);

SecurityContextHolder.getContext().setAuthentication(auth);
filterChain.doFilter(request, response);
Expand Down Expand Up @@ -96,9 +96,8 @@ private EncodedToken findAccessTokenFromCookie(HttpServletRequest request) {

private JwtAuthenticationToken createAuthenticationToken(Claims claims,
EncodedToken accessToken) {
String userId = claims.get("id", String.class);
String role = claims.get("role", String.class);
UserIdentity userIdentity = UserIdentity.from(claims);

return JwtAuthenticationToken.of(userId, role, accessToken);
return JwtAuthenticationToken.of(userIdentity, accessToken);
}
}
Loading
Loading