Skip to content

Commit de4cdeb

Browse files
authored
[FEATURE] 액세스 토큰 클레임 추가 (역할 아이디) (#309)
* feat: accessToken claims 추가를 위한 레코드 * feat(user): userId 기준 roleId 조회 기능 추가 * feat(userIdentity): jwt accessToken에 roleId 추가를 위한 수정 - 가독성을 위한 리팩토링 포함. * test(jwt): 프로덕션 코드 수정에 따라서 테스트 코드 수정 - jwt claims에서 UUID 추출이 불가능해서 String으로 추출 후, UUID로 변경 * test: 유저 아이디 기준 봉사자, 기관 조회 테스트 추가 * feat(annotation): userId, roleId 추출 어노테이션 추가 * feat(annotation): userId, roleId 추출 예외 및 핸들링 기능 추가 - 예외가 클라이언트에게 전달되지 않도록 처리, 로그로 기록. * feat(annotation): config 등록 * refactor: 개행 문제 해결 * feat(userIdentity): 직렬화 가능하도록 수정 * refactor: 불필요한 파라미터 제거
1 parent 677cbeb commit de4cdeb

30 files changed

+490
-118
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.somemore.center.service;
2+
3+
import com.somemore.center.domain.NEWCenter;
4+
import com.somemore.center.repository.NEWCenterRepository;
5+
import com.somemore.center.usecase.NEWCenterQueryUseCase;
6+
import com.somemore.global.exception.ExceptionMessage;
7+
import com.somemore.global.exception.NoSuchElementException;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.stereotype.Service;
10+
import org.springframework.transaction.annotation.Transactional;
11+
12+
import java.util.UUID;
13+
14+
@Service
15+
@RequiredArgsConstructor
16+
@Transactional(readOnly = true)
17+
public class NEWCenterQueryService implements NEWCenterQueryUseCase {
18+
19+
private final NEWCenterRepository centerRepository;
20+
21+
@Override
22+
public NEWCenter getByUserId(UUID userId) {
23+
return centerRepository.findByUserId(userId)
24+
.orElseThrow(() -> new NoSuchElementException(ExceptionMessage.NOT_EXISTS_CENTER));
25+
}
26+
27+
@Override
28+
public UUID getIdByUserId(UUID userId) {
29+
return getByUserId(userId).getId();
30+
}
31+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.somemore.center.usecase;
2+
3+
import com.somemore.center.domain.NEWCenter;
4+
5+
import java.util.UUID;
6+
7+
public interface NEWCenterQueryUseCase {
8+
9+
NEWCenter getByUserId(UUID userId);
10+
11+
UUID getIdByUserId(UUID userId);
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.somemore.global.auth.annotation;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
@Retention(RetentionPolicy.RUNTIME)
9+
@Target(ElementType.PARAMETER)
10+
public @interface RoleId {
11+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.somemore.global.auth.annotation;
2+
3+
import com.somemore.global.auth.authentication.UserIdentity;
4+
import com.somemore.global.exception.InvalidAuthenticationException;
5+
import org.springframework.core.MethodParameter;
6+
import org.springframework.security.core.Authentication;
7+
import org.springframework.security.core.context.SecurityContextHolder;
8+
import org.springframework.stereotype.Component;
9+
import org.springframework.web.bind.support.WebDataBinderFactory;
10+
import org.springframework.web.context.request.NativeWebRequest;
11+
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
12+
import org.springframework.web.method.support.ModelAndViewContainer;
13+
14+
import static com.somemore.global.exception.ExceptionMessage.AUTHENTICATION_MISSING;
15+
import static com.somemore.global.exception.ExceptionMessage.INVALID_PRINCIPAL_TYPE;
16+
17+
@Component
18+
public class RoleIdArgumentResolver implements HandlerMethodArgumentResolver {
19+
20+
@Override
21+
public boolean supportsParameter(MethodParameter parameter) {
22+
return parameter.hasParameterAnnotation(RoleId.class);
23+
}
24+
25+
@Override
26+
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
27+
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
28+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
29+
30+
if (authentication == null) {
31+
throw new InvalidAuthenticationException(AUTHENTICATION_MISSING);
32+
}
33+
34+
if (authentication.getPrincipal() instanceof UserIdentity principal) {
35+
return principal.roleId();
36+
}
37+
38+
throw new InvalidAuthenticationException(INVALID_PRINCIPAL_TYPE);
39+
}
40+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.somemore.global.auth.annotation;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
@Retention(RetentionPolicy.RUNTIME)
9+
@Target(ElementType.PARAMETER)
10+
public @interface UserId {
11+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.somemore.global.auth.annotation;
2+
3+
import com.somemore.global.auth.authentication.UserIdentity;
4+
import com.somemore.global.exception.InvalidAuthenticationException;
5+
import org.springframework.core.MethodParameter;
6+
import org.springframework.security.core.Authentication;
7+
import org.springframework.security.core.context.SecurityContextHolder;
8+
import org.springframework.stereotype.Component;
9+
import org.springframework.web.bind.support.WebDataBinderFactory;
10+
import org.springframework.web.context.request.NativeWebRequest;
11+
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
12+
import org.springframework.web.method.support.ModelAndViewContainer;
13+
14+
import static com.somemore.global.exception.ExceptionMessage.AUTHENTICATION_MISSING;
15+
import static com.somemore.global.exception.ExceptionMessage.INVALID_PRINCIPAL_TYPE;
16+
17+
@Component
18+
public class UserIdArgumentResolver implements HandlerMethodArgumentResolver {
19+
20+
@Override
21+
public boolean supportsParameter(MethodParameter parameter) {
22+
return parameter.hasParameterAnnotation(UserId.class);
23+
}
24+
25+
@Override
26+
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
27+
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
28+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
29+
30+
if (authentication == null) {
31+
throw new InvalidAuthenticationException(AUTHENTICATION_MISSING);
32+
}
33+
34+
if (authentication.getPrincipal() instanceof UserIdentity principal) {
35+
return principal.userId();
36+
}
37+
38+
return new InvalidAuthenticationException(INVALID_PRINCIPAL_TYPE);
39+
}
40+
}
Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
11
package com.somemore.global.auth.authentication;
22

33
import com.somemore.global.auth.jwt.domain.EncodedToken;
4-
import com.somemore.user.domain.User;
54
import lombok.EqualsAndHashCode;
65
import org.springframework.security.authentication.AbstractAuthenticationToken;
76
import org.springframework.security.core.GrantedAuthority;
87
import org.springframework.security.core.authority.SimpleGrantedAuthority;
98

10-
import java.io.Serializable;
119
import java.util.Collection;
1210
import java.util.List;
1311

1412
@EqualsAndHashCode(callSuper = true)
1513
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
16-
private final Serializable principal;
14+
private final UserIdentity principal;
1715
private final transient Object credentials;
1816

19-
public JwtAuthenticationToken(Serializable principal,
17+
public JwtAuthenticationToken(UserIdentity principal,
2018
Object credentials,
2119
Collection<? extends GrantedAuthority> authorities) {
2220
super(authorities);
@@ -35,19 +33,11 @@ public Object getPrincipal() {
3533
return principal;
3634
}
3735

38-
public static JwtAuthenticationToken of(User user, EncodedToken accessToken) {
36+
public static JwtAuthenticationToken of(UserIdentity userIdentity, EncodedToken accessToken) {
3937
return new JwtAuthenticationToken(
40-
user.getId(),
38+
userIdentity,
4139
accessToken,
42-
List.of(new SimpleGrantedAuthority(user.getRole().getAuthority()))
43-
);
44-
}
45-
46-
public static JwtAuthenticationToken of(String userId, String role, EncodedToken accessToken) {
47-
return new JwtAuthenticationToken(
48-
userId,
49-
accessToken,
50-
List.of(new SimpleGrantedAuthority(role))
40+
List.of(new SimpleGrantedAuthority(userIdentity.role().getAuthority()))
5141
);
5242
}
5343
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.somemore.global.auth.authentication;
2+
3+
import com.somemore.user.domain.UserRole;
4+
import io.jsonwebtoken.Claims;
5+
6+
import java.io.Serializable;
7+
import java.util.UUID;
8+
9+
public record UserIdentity(
10+
UUID userId,
11+
UUID roleId,
12+
UserRole role
13+
) implements Serializable {
14+
15+
public static UserIdentity of(UUID userId, UUID roleId, UserRole role) {
16+
return new UserIdentity(userId, roleId, role);
17+
}
18+
19+
public static UserIdentity from(Claims claims) {
20+
UUID userId = UUID.fromString(claims.get("userId", String.class));
21+
UUID roleId = UUID.fromString(claims.get("roleId", String.class));
22+
UserRole role = UserRole.from(
23+
claims.get("role", String.class));
24+
25+
return new UserIdentity(userId, roleId, role);
26+
}
27+
}

src/main/java/com/somemore/global/auth/idpw/filter/IdPwAuthFilter.java

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package com.somemore.global.auth.idpw.filter;
22

33
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.somemore.global.auth.authentication.UserIdentity;
45
import com.somemore.global.auth.jwt.domain.EncodedToken;
56
import com.somemore.global.auth.jwt.usecase.GenerateTokensOnLoginUseCase;
6-
import com.somemore.user.domain.UserRole;
77
import jakarta.servlet.FilterChain;
88
import jakarta.servlet.http.HttpServletRequest;
99
import jakarta.servlet.http.HttpServletResponse;
@@ -16,19 +16,16 @@
1616
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
1717
import org.springframework.security.core.Authentication;
1818
import org.springframework.security.core.AuthenticationException;
19-
import org.springframework.security.core.GrantedAuthority;
2019
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
2120

2221
import java.io.IOException;
23-
import java.util.UUID;
2422

2523
@RequiredArgsConstructor
2624
@Slf4j
2725
public class IdPwAuthFilter extends UsernamePasswordAuthenticationFilter {
2826

2927
private final AuthenticationManager authenticationManager;
3028
private final GenerateTokensOnLoginUseCase generateTokensOnLoginUseCase;
31-
// private final CookieUseCase cookieUseCase;
3229
private final ObjectMapper objectMapper;
3330

3431
@Override
@@ -43,22 +40,11 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ
4340
@Override
4441
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) {
4542
response.setStatus(HttpServletResponse.SC_OK);
46-
String userId = authResult.getName();
47-
String role = extractRole(authResult);
48-
EncodedToken accessToken =
49-
generateTokensOnLoginUseCase.generateAuthTokensAndReturnAccessToken(
50-
UUID.fromString(userId),
51-
UserRole.from(role));
5243

53-
response.setHeader("Authorization", accessToken.getValueWithPrefix());
54-
// cookieUseCase.setAccessToken(response, accessToken.value());
55-
}
44+
UserIdentity userIdentity = (UserIdentity) authResult.getPrincipal();
45+
EncodedToken accessToken = generateTokensOnLoginUseCase.generateAuthTokensAndReturnAccessToken(userIdentity);
5646

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

6450
@Override

src/main/java/com/somemore/global/auth/idpw/provider/CustomAuthenticationProvider.java

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package com.somemore.global.auth.idpw.provider;
22

3+
import com.somemore.center.usecase.NEWCenterQueryUseCase;
34
import com.somemore.global.auth.authentication.JwtAuthenticationToken;
5+
import com.somemore.global.auth.authentication.UserIdentity;
46
import com.somemore.global.auth.jwt.domain.EncodedToken;
57
import com.somemore.global.auth.jwt.domain.TokenType;
68
import com.somemore.global.auth.jwt.usecase.JwtUseCase;
79
import com.somemore.user.domain.User;
10+
import com.somemore.user.domain.UserRole;
811
import com.somemore.user.usecase.UserQueryUseCase;
12+
import com.somemore.volunteer.usecase.NEWVolunteerQueryUseCase;
913
import lombok.RequiredArgsConstructor;
1014
import org.springframework.security.authentication.AuthenticationProvider;
1115
import org.springframework.security.authentication.BadCredentialsException;
@@ -16,6 +20,8 @@
1620
import org.springframework.stereotype.Component;
1721
import org.springframework.transaction.annotation.Transactional;
1822

23+
import java.util.UUID;
24+
1925
@Component
2026
@RequiredArgsConstructor
2127
@Transactional(readOnly = true)
@@ -24,6 +30,8 @@ public class CustomAuthenticationProvider implements AuthenticationProvider {
2430
private final JwtUseCase jwtUseCase;
2531
private final PasswordEncoder passwordEncoder;
2632
private final UserQueryUseCase userQueryUseCase;
33+
private final NEWVolunteerQueryUseCase volunteerQueryUseCase;
34+
private final NEWCenterQueryUseCase centerQueryUseCase;
2735

2836
@Override
2937
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
@@ -34,20 +42,32 @@ public Authentication authenticate(Authentication authentication) throws Authent
3442

3543
validatePassword(rawAccountPassword, user.getAccountPassword());
3644

37-
EncodedToken accessToken = generateAccessToken(user);
45+
UUID userId = user.getId();
46+
UserRole role = user.getRole();
47+
UUID roleId = getRoleIdByUserId(role, user.getId());
48+
49+
UserIdentity userIdentity = UserIdentity.of(userId, roleId, role);
50+
51+
EncodedToken accessToken = generateAccessToken(userIdentity);
3852

39-
return JwtAuthenticationToken.of(user, accessToken);
53+
return JwtAuthenticationToken.of(userIdentity, accessToken);
4054
}
4155

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

47-
private EncodedToken generateAccessToken(User user) {
61+
private UUID getRoleIdByUserId(UserRole role, UUID userId) {
62+
if (role.equals(UserRole.VOLUNTEER)) {
63+
return volunteerQueryUseCase.getIdByUserId(userId);
64+
}
65+
return centerQueryUseCase.getIdByUserId(userId);
66+
}
67+
68+
private EncodedToken generateAccessToken(UserIdentity userIdentity) {
4869
return jwtUseCase.generateToken(
49-
user.getId().toString(),
50-
user.getRole().getAuthority(),
70+
userIdentity,
5171
TokenType.ACCESS
5272
);
5373
}

0 commit comments

Comments
 (0)