본문 바로가기
카테고리 없음

로그인 성공시 사용자 정보도 받는다

by 두잇건강 2025. 8. 19.
반응형

아래는 “로그인 시 JWT 발급과 함께 로그인한 사용자의 이름(name)과 전화번호(phone)도 프론트로 내려받는” 방법을 Spring Boot 3 + Spring Security 6 + JWT 기준으로 단계별 정리한 것입니다. 핵심은 로그인 성공 시 응답 DTO에 사용자 프로필 필드를 포함해 반환하는 것입니다.

구현 목표
- POST /api/authenticate 요청 시
  - JWT 토큰
  - email(아이디), name(이름), phone(전화번호), roles 등 사용자 정보 함께 반환
- Vue 3에서는 로그인 응답으로 받은 name/phone을 상태에 저장해서 화면에서 바로 사용

1) 로그인 응답 DTO 확장
토큰만 내려주던 DTO를 확장해 사용자 정보도 포함합니다.

TokenResponse.java
```java
package com.example.app.auth.dto;

import java.util.Set;

public record TokenResponse(
        String jwt,
        String type,
        String email,
        String name,
        String phone,
        Set<String> roles
) {
    public static TokenResponse of(String jwt, String email, String name, String phone, Set<String> roles) {
        return new TokenResponse(jwt, "Bearer", email, name, phone, roles);
    }
}
```

2) 로그인 요청 DTO(기존 그대로)
LoginRequest.java
```java
package com.example.app.auth.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public record LoginRequest(
        @NotBlank @Email String email,
        @NotBlank String password
) {}
```

3) UserDetailsService에서 사용자 조회
이미 구성되어 있다면 그대로 쓰면 됩니다. 로그인 후 사용자 엔티티(User)를 추가로 조회할 수 있게 UserRepository를 주입받아 사용합니다.

CustomUserDetailsService.java (요약)
```java
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        var user = userRepository.findByEmail(email)
                .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + email));

        var authorities = user.getAuthorities().stream()
                .map(a -> new SimpleGrantedAuthority(a.getAuthorityName()))
                .toList();

        return new org.springframework.security.core.userdetails.User(
                user.getEmail(),
                user.getPassword(),
                authorities
        );
    }
}
```

4) AuthController에서 로그인 성공 시 사용자 정보 포함 응답
AuthenticationManager로 인증한 뒤, 토큰을 만들고 UserRepository로 사용자 엔티티를 찾아 name/phone/roles를 응답에 포함합니다.

AuthController.java
```java
package com.example.app.auth.controller;

import com.example.app.auth.dto.LoginRequest;
import com.example.app.auth.dto.TokenResponse;
import com.example.app.security.JwtUtil;
import com.example.app.user.entity.Authority;
import com.example.app.user.entity.User;
import com.example.app.user.repository.UserRepository;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

import java.util.Set;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
@CrossOrigin(origins = "http://localhost:3000")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JwtUtil jwtUtil;
    private final UserRepository userRepository;

    @PostMapping("/authenticate")
    public ResponseEntity<TokenResponse> authenticate(@RequestBody @Valid LoginRequest req) {
        // 1) 인증
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(req.email(), req.password())
        );

        // 2) 토큰 생성
        String jwt = jwtUtil.createToken(authentication);

        // 3) 사용자 정보 조회
        User user = userRepository.findByEmail(req.email())
                .orElseThrow(() -> new IllegalStateException("인증은 성공했으나 사용자 정보를 찾을 수 없습니다."));

        Set<String> roles = user.getAuthorities().stream()
                .map(Authority::getAuthorityName)
                .collect(Collectors.toSet());

        // 4) 토큰 + 사용자 정보 함께 응답
        TokenResponse body = TokenResponse.of(jwt, user.getEmail(), user.getName(), user.getPhone(), roles);

        return ResponseEntity.ok(body);
    }
}
```

설명
- 인증 성공 후 Authentication에서 username(email)을 얻을 수 있지만, name/phone 같은 추가 필드는 보통 User 엔티티에 있으므로 UserRepository로 다시 조회해 채워 줍니다.
- 만약 커스텀 UserPrincipal을 만들어 UserDetails에 name/phone을 넣어두면 Repository 재조회 없이도 가져올 수 있습니다. 아래 “대안 A” 참고.

5) JWT에 사용자 정보까지 넣을지 여부
- 일반적으로 JWT에는 최소한의 정보(sub, roles 등)만 넣고, 이름/전화번호는 응답 바디로만 내려주는 것을 권장합니다. 클라이언트는 로그인 직후 받은 바디를 상태에 저장해 쓰면 됩니다.
- 꼭 JWT에 name/phone을 claim으로 넣고 싶다면 JwtUtil.createToken()에서 .claim("name", user.getName()) 식으로 추가할 수 있습니다. 단, 토큰 크기 증가 및 정보 노출 리스크를 고려하세요.

6) Vue 3 측 사용 예시
로그인 API 응답에서 바로 name/phone을 받아 저장합니다. Pinia 또는 로컬 상태에 보관 후 UI에 바인딩하세요.

예: Pinia auth store의 login 액션
```javascript
const login = async (credentials) => {
  try {
    const res = await axios.post('/api/authenticate', credentials)
    const { jwt, email, name, phone, roles, type } = res.data

    // 토큰 저장
    localStorage.setItem('jwt-token', jwt)
    axios.defaults.headers.common['Authorization'] = `${type} ${jwt}`

    // 사용자 정보 저장
    user.value = { email, name, phone, roles }
    localStorage.setItem('user-info', JSON.stringify(user.value))

    return { success: true }
  } catch (err) {
    // 에러 처리 동일
    return { success: false, error: err.response?.data?.message || '로그인 실패' }
  }
}
```

화면에서 표시
```vue
<p>이름: {{ authStore.user?.name }}</p>
<p>전화: {{ authStore.user?.phone }}</p>
```

대안 A) 커스텀 UserDetails(UserPrincipal)에 이름/전화번호 포함
매번 Repository 재조회가 싫다면, 인증 시점에 User 엔티티 기반으로 커스텀 UserPrincipal을 만들고, Authentication.getPrincipal()에서 바로 name/phone을 꺼낼 수 있게 합니다.

UserPrincipal.java
```java
@Getter
@AllArgsConstructor
public class UserPrincipal implements UserDetails {
    private Long id;
    private String email;
    private String password;
    private String name;
    private String phone;
    private Collection<? extends GrantedAuthority> authorities;

    // UserDetails 메서드들 구현...

    public static UserPrincipal from(User user) {
        var auths = user.getAuthorities().stream()
                .map(a -> new SimpleGrantedAuthority(a.getAuthorityName()))
                .toList();
        return new UserPrincipal(user.getId(), user.getEmail(), user.getPassword(), user.getName(), user.getPhone(), auths);
    }
}
```

CustomUserDetailsService.java
```java
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
    var user = userRepository.findByEmail(email)
            .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + email));
    return UserPrincipal.from(user);
}
```

AuthController에서
```java
Authentication authentication = authenticationManager.authenticate(
    new UsernamePasswordAuthenticationToken(req.email(), req.password())
);
UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();

String jwt = jwtUtil.createToken(authentication);

TokenResponse body = TokenResponse.of(
    jwt, principal.getEmail(), principal.getName(), principal.getPhone(),
    principal.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet())
);

return ResponseEntity.ok(body);
```

대안 B) 로그인 후 /api/user/profile로 추가 정보 호출
보안/구조상 로그인 응답은 토큰만, 사용자 정보는 별도 API로 분리하고 싶으면:
- 로그인 성공 → jwt 저장 → 즉시 /api/user/profile 호출 → name/phone 수신/저장
- 장점: 로그인 응답을 슬림하게 유지, 프로필 캐시·갱신 로직을 재사용하기 쉬움
- 단점: 요청 1회 증가

추가 체크포인트
- name/phone 컬럼이 User 엔티티와 DB에 존재하는지 확인
- CORS 설정에서 프론트 도메인 허용
- JWT 발급 후 Authorization 헤더(“Bearer ...”)가 프론트 Axios에 확실히 설정되는지
- 예외 시 글로벌 예외 핸들러로 일관된 에러 포맷 제공(이미 구성됨)

정리
- 가장 간단하고 실용적인 방법: 로그인 성공 시 TokenResponse에 name/phone을 함께 담아 반환 → 프론트에서 상태 저장.
- 대규모/정교한 구조를 원하면 커스텀 UserPrincipal을 써서 Repository 재조회 없이도 name/phone 접근, 또는 별도 프로필 API로 분리.

출처

반응형