회원 인증 기능은 다음과 같은 디렉토리 구조로 구성되어 있습니다.
.
├── controller
│ └── MemberController.java
├── domain
│ ├── Member.java
│ └── Role.java
├── dto
│ ├── MemberListResDto.java
│ ├── MemberLoginReqDto.java
│ └── MemberSaveReqDto.java
├── repository
│ └── MemberRepository.java
└── service
└── MemberService.java
async doLogin(){
const loginData = {
email : this.email,
password : this.password
};
const response = await axios.post(`${process.env.VUE_APP_API_BASE_URL}/member/login`, loginData);
const token = response.data.token;
localStorage.setItem("token", token); // 토큰 저장
window.location.href = "/";
}
email
, password
로 로그인 요청을 보냄localStorage
에 저장@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody MemberLoginReqDto dto) {
try {
Member member = memberService.login(dto);
String jwtToken = jwtTokenProvider.createToken(
member.getEmail(),
member.getRole().toString()
);
Map<String, Object> loginInfo = new HashMap<>();
loginInfo.put("id", member.getId());
loginInfo.put("token", jwtToken);
return ResponseEntity.ok(loginInfo);
} catch (AuthenticationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("이메일 또는 비밀번호가 틀렸습니다.");
}
}
public String createToken(String email, String role){
Claims claims = Jwts.claims().setSubject(email); // 이메일을 subject로 설정
claims.put("role", role); // 권한 추가
claims.put("id", member.getId()); // 사용자 ID 추가
Date now = new Date();
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + expiration * 60 * 1000L)) // 만료 시간
.signWith(SECRET_KEY) // 서명
.compact();
}
String token = httpServletRequest.getHeader("Authorization");
try {
if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
String jwtToken = token.substring(7);
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(jwtToken)
.getBody();
List<GrantedAuthority> authorities = List.of(
new SimpleGrantedAuthority("ROLE_" + claims.get("role"))
);
UserDetails userDetails = new User(claims.getSubject(), "", authorities);
Authentication auth = new UsernamePasswordAuthenticationToken(
userDetails, "", userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
} catch (ExpiredJwtException e) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("만료된 토큰입니다.");
} catch (JwtException | IllegalArgumentException e) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("유효하지 않은 토큰입니다.");
}
Bearer
prefix 체크인증된 사용자는 @AuthenticationPrincipal
로 접근할 수 있습니다.
@GetMapping("/me")
public ResponseEntity<?> myInfo(@AuthenticationPrincipal User user) {
return ResponseEntity.ok(user.getUsername()); // 이메일 반환
}
항목 | 설명 |
---|---|
HTTPS | 토큰이 평문으로 노출되지 않도록 HTTPS 사용 필수 |
토큰 저장소 | XSS에 취약한 localStorage 보다 HttpOnly Cookie 고려 가능 |
만료 처리 | 클라이언트에서 만료 여부 확인하고 자동 로그아웃 또는 재로그인 처리 필요 |
Refresh Token | 장기 세션을 위해 Access + Refresh 토큰 방식 적용 가능 |