
์ด ๊ธ์ ์ ์ ์๋ณ์๋ฅผ ์ปจํธ๋กค๋ฌ์ ์ฃผ์ ํ๋ ๋ฐฉ์์ ํค๋ ์ง์ ํ์ฑ โ
SecurityContext์ปจํธ๋กค๋ฌ๋ง๋ค ์ง์ ์ ๊ทผ โ@CurrentUserId+HandlerMethodArgumentResolver์์๋ก ๊ฐ์ ํด ์จ ๊ณผ์ ์ ์ ๋ฆฌํ๊ณ , ์ต์ข ์ค๊ณ์ ์ ์ฉ ํจ๊ณผ๋ฅผ ์ ์ํ๋ค. ๋ถํ์ํ ์ค๋ณต๊ณผ ๋ณด์/์์ธ ์ฒ๋ฆฌ ๋ถ์ผ์น๋ฅผ ์ ๊ฑฐํ๊ณ , ์ปจํธ๋กค๋ฌ ์๊ทธ๋์ฒ๋ฅผ ๋จ์ํํ๋ ๊ฒ์ด ๋ชฉํ์ด๋ค.
๋ชฉํ๋ ๋จ์ํ๋ค. ์ ์ ID๋ ์๋์ผ๋ก, ์์ ํ๊ฒ, ์ผ๊ด๋๊ฒ ์ฃผ์ ํ๊ณ , ์ปจํธ๋กค๋ฌ๋ ๋น์ฆ๋์ค์๋ง ์ง์คํ๊ฒ ๋ง๋ ๋ค.
Authorization: Bearer ...์์ ํ ํฐ์ ์ถ์ถยท๊ฒ์ฆํด userId๋ฅผ ์ป๋๋ค.SecurityContext์์ ์ง์ ๊บผ๋Authentication์ ์ธํ
ํ๊ณ , ์ปจํธ๋กค๋ฌ/์๋น์ค์์ SecurityContextHolder๋ก userId๋ฅผ ์ฝ๋๋ค. ๊ณต์ฉ ์ ํธ SecurityContextProvider๋ก ๊ฐ์ผ๋ค.@CurrentUserId + ๋ฆฌ์กธ๋ฒ (์ต์ข
์ค๊ณ)@CurrentUserId Long userId๋ง ์ ์ธํ๋ฉด ๋ฆฌ์กธ๋ฒ๊ฐ SecurityContext์์ ์์ ํ๊ฒ ์ฃผ์
ํ๋ค.์๋๋ ์ต์ข ์ค๊ณ์ ํต์ฌ ์ฝ๋์ ํจ๊ป, ๊ฐ ํ์ผ(ํด๋์ค/์ค์ /์ปจํธ๋กค๋ฌ)์ ๋ํด โ๋ฌด์์/์/์ด๋ป๊ฒ/ํ์ฅ ํฌ์ธํธ/์ฃผ์์ โ์ ๊ตฌ์ฒด์ ์ผ๋ก ์ค๋ช ํ์๋ค.
@CurrentUserId@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurrentUserId {}
HandlerMethodArgumentResolver์๊ฒ โ์ด ํ๋ผ๋ฏธํฐ๋ฅผ ๋ค๊ฐ ์ฒ๋ฆฌํ ์ ์๋?โ๋ฅผ ์ง์ํ๋ค.supportsParameter()๊ฐ true๋ฅผ ๋ฐํํ๋ฉด resolveArgument()๊ฐ ํธ์ถ๋์ด userId๋ฅผ ์ฑ์ ๋ฃ๋๋ค.CurrentUserIdArgumentResolver@Component
public class CurrentUserIdArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter p) {
return p.getParameterAnnotation(CurrentUserId.class) != null
&& p.getParameterType().equals(Long.class);
}
@Override
public Object resolveArgument(MethodParameter p, ModelAndViewContainer m,
NativeWebRequest w, WebDataBinderFactory b) {
try {
return SecurityContextProvider.getAuthenticatedUserId();
} catch (Exception e) {
throw new CommonException(CommonErrorStatus.UNAUTHORIZED);
}
}
}
@CurrentUserId๊ฐ ๋ถ์ Long ํ์
ํ๋ผ๋ฏธํฐ๋ฅผ ๊ฐ์งํ๊ณ , SecurityContext์์ ๊ฒ์ฆ๋ userId๋ฅผ ์ถ์ถํด ์ฃผ์
ํ๋ค.Long์ผ๋ก ์๊ฒฉํ ๊ณ ์ ํด API ํ๋ฉด์ ๋ช
ํํ ์ ์งํ๋ค. (์ปจํธ๋กค๋ฌ ์๊ทธ๋์ฒ๋ง ๋ด๋ ๋ฌด์์ด ๋ค์ด์ค๋์ง ๋ถ๋ช
ํด์ง๋ค)1) ์คํ๋ง MVC๊ฐ ํ๋ผ๋ฏธํฐ ํด์ ์ค supportsParameter()๋ฅผ ํธ์ถํ๋ค.
2) ์กฐ๊ฑด์ ๋ง์กฑํ๋ฉด resolveArgument()๊ฐ ํธ์ถ๋๋ค.
3) SecurityContextProvider.getAuthenticatedUserId()๊ฐ ๋ด๋ถ์์ Authentication์ principal(์: CustomUserDetails)์์ userId๋ฅผ ์ฝ์ด์จ๋ค.
4) ์์ธ ๋ฐ์ ์ 401๋ก ํต์ผํ๋ค.
Optional<Long> ๋๋ @Nullable Long์ ํ์ฉํ๋ ๋ ๋ฒ์งธ ๋ฆฌ์กธ๋ฒ๋ฅผ ์ถ๊ฐํ ์ ์๋ค. ์ด ๊ฒฝ์ฐ ์คํจ๋ฅผ 401๋ก ๋์ง์ง ์๊ณ null์ ์ฃผ์
ํ๋ค. -> ์ต๋ช
์ ์ ์ฒ๋ฆฌ ๊ฐ์ ๊ฒฝ์ฐ ์ฌ์ฉํ ์๋ ์์.SecurityContext ์ ๊ทผ์ ์ค๋ ๋ ๋ฐ์ด๋์ด๋ฏ๋ก, ๋น๋๊ธฐ ํธ์ถ ์ ์ปจํ
์คํธ ์ ํ ์ค์ ์ ์ฌ์ฉํด์ผ ํ๋ค.WebMvcConfig@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final CurrentUserIdArgumentResolver currentUserIdArgumentResolver;
@Override public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> rs
) {
rs.add(currentUserIdArgumentResolver);
}
}
CurrentUserIdArgumentResolver๋ฅผ MVC์ ๋ฑ๋กํด ์ฃผ์
์ด ๊ฐ๋ฅํ๋๋ก ํ๋ค.addArgumentResolvers์์ ๋ฆฌ์กธ๋ฒ๋ฅผ ์ถ๊ฐํ๋ฉด ์คํ๋ง์ด ๋งค ์์ฒญ๋ง๋ค ํ๋ผ๋ฏธํฐ๋ฅผ ํด๋น ๋ฆฌ์กธ๋ฒ์ ์ง์ํ๋ค.@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<SuccessResponse<Void>> uploadData(
@Parameter(hidden = true)
@CurrentUserId Long userId,
@RequestPart("dataFile")
MultipartFile dataFile,
@RequestPart(value = "thumbnailFile", required = false)
MultipartFile thumbnailFile,
@RequestPart @Validated
UploadDataWebRequest webRequest
) { /* ... */ }
dataFile, thumbnailFile)๊ณผ JSON DTO(webRequest)๋ฅผ ํจ๊ป ๋ฐ๋๋ค.@CurrentUserId๋ก ๋ก๊ทธ์ธ ํ์์ฑ์ด ์๊ทธ๋์ฒ์ ๋๋ฌ๋๊ณ , userId๋ฅผ ํด๋ผ์ด์ธํธ๊ฐ ์์ ์ ๊ณตํ์ง ๋ชปํ๋๋ก ๋ง๋๋ค.Authorization ํค๋๋ ๋ช
์ํ๊ณ , userId ํ๋ผ๋ฏธํฐ๋ @Parameter(hidden = true)๋ก ์จ๊ธด๋ค.userId๋ก ์กฐํํ ๋ฆฌ์์ค ์ ๊ทผ ์ ์ธ๊ฐ(์์ ๊ถ/๊ถํ)๋ฅผ ์๋น์ค ๋ ๋ฒจ์์ ์ฌ๊ฒ์ฆํ๋ค.@CurrentUserId: โ์ฌ๊ธฐ์ ์ธ์ฆ๋ ์ฌ์ฉ์ ID๊ฐ ๋ค์ด์จ๋คโ๋ ์๋ ์ ์ธ์ ๋ด๋นํ๋ค. WebMvcConfig: ๋ฆฌ์กธ๋ฒ๋ฅผ ์คํ๋ง ์ํ๊ณ์ ์ฐ๊ฒฐํ๋ค. [Client] --Bearer ํ ํฐ-->
[JWT ํํฐ] --๊ฒ์ฆ OK-->
[SecurityContext(Authentication)]
โโ> [ArgumentResolver] --@CurrentUserId ์ฃผ์
-->
[Controller(Long userId)] โ [Service] (๋น์ฆ๋์ค ๋ก์ง๋ง)
401 UNAUTHORIZED๋ก ํต์ผํ๋ค.SecurityContextProvider.setupSecurityContextForTest(...)๋ก ์ธ์ฆ ๋งฅ๋ฝ์ ์์ฝ๊ฒ ๊ตฌ์ฑํ๋ค.Authorization๋ง ์๊ตฌ, ๋ด๋ถ ์ฃผ์
ํ๋ผ๋ฏธํฐ๋ ์จ๊ฒจ ์คํ์ด ๊ฐ๊ฒฐํด์ง๋ค.@CurrentUserRole, @CurrentOrgId ๋ฑ ๋ฆฌ์กธ๋ฒ๋ฅผ ์ถ๊ฐํด๋ ์ปจํธ๋กค๋ฌ๋ ๋ณํ์ง ์๋๋ค.1) JWT ํํฐ ์ ๊ฒ: ๊ฒ์ฆ ์ฑ๊ณต ์ Authentication(principal=userId, role)์ด ์ ํํ ์ฑ์์ง๋์ง ํ์ธํ๋ค.
2) ๋ฆฌ์กธ๋ฒ ๋์
: @CurrentUserId, ๋ฆฌ์กธ๋ฒ, WebMvcConfig๋ฅผ ์ถ๊ฐํ๋ค.
3) ์ปจํธ๋กค๋ฌ ์๊ทธ๋์ฒ ๊ต์ฒด: @CurrentUserId Long userId๋ก ๋ฐ๊พธ๊ณ ๋ด๋ถ ์ธ์ฆ ์ ๊ทผ ์ฝ๋๋ฅผ ์ญ์ ํ๋ค.
4) ๋ฌธ์ํ ์ ๋ฆฌ: Swagger์์ Authorization ํค๋๋ ๋ช
์ํ๊ณ , userId๋ ์จ๊ธด๋ค.
@CurrentUserId๋ฅผ ์ฐ์ง ์๊ณ , ํ์ ์ nullable ์ฃผ์
์ฉ ๋ณ๋ ๋ฆฌ์กธ๋ฒ๋ฅผ ๋๋ค. Long ์ธ ํ์
์?supportsParameter๋ฅผ ํ์ฅํ๊ฑฐ๋ ์ฌ์ฉ์ ๊ฐ์ฒด ์์ฒด๋ฅผ ์ฃผ์
ํ๋ ์ ๋
ธํ
์ด์
์ ์ถ๊ฐํ๋ค.SecurityContextProvider.setupSecurityContextForTest(1L, RoleType.USER);
// ํ
์คํธ ...
SecurityContextProvider.clearSecurityContext();
์ปจํธ๋กค๋ฌ์๋ ๋น์ฆ๋์ค ํ๋ผ๋ฏธํฐ๋ง ๋จ๊ธด๋ค. ์ ์ ID๋ ๋ฆฌ์กธ๋ฒ๊ฐ ์ผ๊ด๋๊ฒ ์ฃผ์
ํ๋ค.
์ด๋ ๊ฒ ํ๋ฉด ์ฝ๋๋ ์งง์์ง๊ณ , ๋ณด์/์์ธ๋ ํต์ผ๋๋ฉฐ, ํ
์คํธ์ ๋ฌธ์ํ๊ฐ ์ฌ์์ง๋ค. ๐