최근 프로젝트에서 사용자 인증 정보를 처리하는 방식에 대해 고민이 많았다.
매번 요청마다 헤더에서 사용자 ID를 추출하고 전달하는 방식은 코드 중복도 많고 관리하기 어려웠기 때문이다ㅋ
그래서 이번에는 인증 정보를 더 효율적으로 관리할 수 있는 방법을 찾고, 보안도 강화하고 싶었다(금카 강화 느낌 ㅋ)
이번에는 Spring Security의 SecurityContextHolder를 활용해서 인증 정보를 중앙에서 관리하는 방식으로 개선해 보려고한다
또한, 학생과 선생님이라는 서로 다른 역할에 맞게 인증 로직을 분리해, 역할 기반으로 안전하고
확장 가능한 구조를 만들어가는 과정을 적어보려한다!!!!!!!!! 드가자 슛
🔒 SecurityContextHolder를 활용한 인증 정보 처리 개선
이전 방식: RequestHeader를 통한 처리
@PostMapping("/paper/create")
public ResponseEntity<SnResponse<PaperResponseDTO>> createPaper(
@RequestHeader("studentId") Long studentId,// 매 요청마다 헤더에서 추출
@Valid @RequestBody PaperRequestDTO paperRequestDTO
) {
PaperResponseDTO responseDTO = paperService.createPaper(studentId, paperRequestDTO);
return ResponseEntity.status(CREATE.getStatus())
.body(new SnResponse<>(CREATE, responseDTO));
}
@Service
public class PaperService {
public PaperResponseDTO createPaper(Long studentId, PaperRequestDTO paperRequestDTO) {
Student student = studentRepository.findById(studentId)
.orElseThrow(() -> new IllegalArgumentException("학생을 찾을 수 없습니다."));
// ... 나머지 로직
}
}
문제점:
- 모든 컨트롤러 메서드마다 @RequestHeader 어노테이션 반복 작성
- 서비스 메서드의 파라미터로 매번 studentId 전달 필요
- 인증 정보의 일관성 보장이 어려움
- 코드 중복이 많이 발생
개선된 방식: SecurityContextHolder 활용
@PostMapping("/paper/create")
public ResponseEntity<SnResponse<PaperResponseDTO>> createPaper(
@Valid @RequestBody PaperRequestDTO paperRequestDTO
) {
PaperResponseDTO responseDTO = paperService.createPaper(paperRequestDTO);
return ResponseEntity.status(CREATE.getStatus())
.body(new SnResponse<>(CREATE, responseDTO));
}
@Service
public class PaperService {
public PaperResponseDTO createPaper(PaperRequestDTO paperRequestDTO) {
Long studentId = getAuthenticatedStudentId();
Student stuㅂent = studentRepository.findById(studentId)
.orElseThrow(() -> new IllegalArgumentException("학생을 찾을 수 없습니다."));
// ... 나머지 로직
}
private Long getAuthenticatedStudentId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new SecurityException("인증 정보가 없습니다.");
}
Object principal = authentication.getPrincipal();
if (principal instanceof CustomStudentDetails) {
return ((CustomStudentDetails) principal).getStudentId();
}
throw new SecurityException("학생 인증 정보를 찾을 수 없습니다.");
}
}
개선사항:
- 컨트롤러 계층 간소화
- 서비스 계층에서 일관된 인증 정보 접근
- 코드 재사용성 향상
- 인증 정보 관리의 중앙화
OAuth2User와 UserDetails 구분을 통한 역할 기반 인증 구현
인증 객체 구현
CustomOAuth2User (선생님용 - 소셜 로그인)
public class CustomOAuth2User implements OAuth2User {
private final UserResponseDTO userResponseDTO;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(
new SimpleGrantedAuthority(userResponseDTO.getRole().getAuthority())
);
}
@Override
public Map<String, Object> getAttributes() {
// OAuth2 제공자로부터 받은 속성 정보
return attributes;
}
@Override
public String getName() {
return userResponseDTO.getUsername();
}
}
CustomStudentDetails (학생용 - 일반 로그인)
public class CustomStudentDetails implements UserDetails {
private final StudentResponseDTO studentResponseDTO;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(
new SimpleGrantedAuthority(studentResponseDTO.getRole().getAuthority())
);
}
public Long getStudentId() {
return studentResponseDTO.getStudentId();
}
// UserDetails 필수 메서드 구현
@Override
public String getPassword() {
return studentResponseDTO.getPassword();
}
@Override
public String getUsername() {
return studentResponseDTO.getName();
}
// 계정 상태 관련 메서드는 현재 모두 활성화 상태로 반환
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
@Override
public boolean isEnabled() { return true; }
}
역할 기반 인증 처리
@RequiredArgsConstructor
public class JWTFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// JWT 토큰 검증 후
String username = jwtUtil.getUsername(token);
String roleName = jwtUtil.getRole(token);
Role role = Role.valueOf(roleName);
// 역할에 따른 인증 객체 생성
Authentication authToken;
if (role == Role.TEACHER) {
// 선생님: OAuth2 인증 처리
UserResponseDTO userResponseDTO = new UserResponseDTO();
userResponseDTO.setUsername(username);
userResponseDTO.setRole(role);
CustomOAuth2User oauth2User = new CustomOAuth2User(userResponseDTO);
authToken = new UsernamePasswordAuthenticationToken(
oauth2User,
null,
oauth2User.getAuthorities()
);
} else {
// 학생: 일반 인증 처리
StudentResponseDTO studentResponseDTO = StudentResponseDTO.builder()
.studentId(Long.parseLong(username))
.name(username)
.role(role)
.build();
CustomStudentDetails studentDetails = new CustomStudentDetails(studentResponseDTO);
authToken = new UsernamePasswordAuthenticationToken(
studentDetails,
null,
studentDetails.getAuthorities()
);
}
SecurityContextHolder.getContext().setAuthentication(authToken);
filterChain.doFilter(request, response);
}
}
구분의 이점
- 명확한 역할 구분
- 선생님: 소셜 로그인 기반의 OAuth2 인증
- 학생: 일반 로그인 기반의 UserDetails 인증
- 타입 안전성
- 각 역할에 맞는 인증 객체 사용으로 타입 캐스팅 오류 방지
- 역할별 필요한 정보만 포함하여 효율적인 메모리 사용
- 확장성
- 각 역할별로 필요한 추가 정보나 기능을 독립적으로 확장 가능
- 새로운 인증 방식 추가 시 기존 코드 영향 최소화
- 보안성
- 역할별 적절한 인증 방식 적용
- 불필요한 정보 노출 방지
이러한 구조를 통해 각 사용자 유형별로 적절한 인증 처리가 가능하며, 코드의 유지보수성과 확장성이 향상할 수 있었다
🙇♂️ 마무리
프로젝트를 진행하면서, 우선 잘 굴러가게끔 해놓은 뒤 리팩토링을 하니 더 수월한 거 같다.
뭐가 실무에서 쓰이는지, 어떤 것이 최적의 것인지를 생각하며 구현을 하기엔 내 실력이 아직 역부족이다.
조급하게 생각하지말고, 천천히 내 실력을 키워나가야겠다. (사실 급함 ㅋ)
아 그리고 풋살팀을 창설하였다. 나의 고향 '대 계룡시'에서 서울로 진출한 친구들과 풋살을 하기 위해 이리저리 인맥을 끌어모았다 ㅎㅎ
재밌게 운동하면서 코딩도 재밌게 해야겠다 ㅎㅎ
'Project > Sparkle-Note' 카테고리의 다른 글
[Project] Sparkle Note / Paper 서비스의 선생님 작성 권한 추가 과정 (1) | 2024.11.13 |
---|---|
[Project] Spakrle Note / JWT 토큰 내 사용자 정보 개선하기 (2) | 2024.11.12 |
[Project] Sparkle Note / JWT 토큰 전달 (0) | 2024.11.02 |
[Project] Sparkle-Note / 데일리로그 (0) | 2024.10.25 |
[Project] Sparkle-Note / 데일리로그 (1) | 2024.10.14 |