아래는 “로그인 시 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로 분리.
출처
카테고리 없음
로그인 성공시 사용자 정보도 받는다
반응형
반응형