728x90
320x100
🎯 오늘의 목표
- 로그인 구현
⚙️ 진행한 작업
- 로그인 구현
- JWT 기반 인증 활용
- 리프레시 토큰 도입-
🛠️ 개발내용
- 나는 이 프로젝트에서 세션 기반 인증이 아닌 토큰 기반인증 즉, JWT를 도입하기로 함
- why? 이 프로젝트에서는 웹/앱을 둘 다 고려중이 기 때문에 JWT방식이 더 유연하다고 판단.
📂 세션 기반 인증과 토큰 기반 인증이 뭔가요?
서버 기반 인증 VS 토큰 기반 인증 (Session, JWT 등)
인증 방식엔 크게 서버 기반 인증과 토큰 기반 인증으로 나뉜다. [🔐 인증 방식 분류]1. 서버 기반 인증 (Server-Side Authentication)서버가 사용자 인증 상태를 기억하고 있음 (Stateful)대표 방식: 세션
ddururiiiiiii.tistory.com
📌 의존성 추가 (build.gradle)
- JWT 토큰을 생성하고 파싱하기 위해 jjwt 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' // JSON 처리용
📌 JWT 유틸 클래스 생성 (JwtUtil.java)
- JWT 생성, 검증, 이메일 추출 등 핵심 기능 담당
- SECRET_KEY, 만료시간, 서명 알고리즘 설정 포함
package ddururi.bookbookclub.global.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
@Component
public class JwtUtil {
private static final long EXPIRATION_MS = 1000 * 60 * 60 * 24; // 1일
private static final String SECRET_KEY = "비밀번호는비밀이에용"; // 32바이트 이상
private Key key;
@PostConstruct
public void init() {
this.key = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
}
// JWT 생성
public String createToken(String email) {
return Jwts.builder()
.setSubject(email)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_MS))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
// JWT 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
// JWT에서 이메일 추출
public String getEmailFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}
- createToken(String email) → 로그인 성공 시 토큰 생성
- validateToken(String token) → 요청마다 토큰 검증
- getEmailFromToken(String token) → 사용자 식별 정보 추출
📌 사용자 정보 포장 클래스 (CustomUserDetails.java)
- 스프링 시큐리티가 이해할 수 있도록 User 객체를 래핑하는 클래스
- JWT에서 사용자 정보 꺼내 인증 시에 이 객체로 감싸서 넣어줌
package ddururi.bookbookclub.global.security;
import ddururi.bookbookclub.domain.user.entity.User;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
@Getter
public class CustomUserDetails implements UserDetails {
private final User user;
public CustomUserDetails(User user) {
this.user = user;
}
// 권한 설정
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(new SimpleGrantedAuthority("ROLE_" + user.getRole().name()));
}
@Override
public String getPassword() {
return user.getPassword(); // 로그인용일 때만 사용됨
}
@Override
public String getUsername() {
return user.getEmail(); // unique key
}
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
@Override
public boolean isEnabled() { return true; }
public Long getId() {
return user.getId();
}
public String getNickname() {
return user.getNickname();
}
public User getUser() {
return user;
}
}
- UserDetails를 구현
- getUsername()은 이메일, getAuthorities()는 역할 반환
📌 인증 필터 구현 (JwtAuthenticationFilter.java)
- 모든 요청 전에 작동하는 필터
- Authorization 헤더에서 토큰 꺼내고 → 검증하고 → SecurityContext에 인증 정보 저장
package ddururi.bookbookclub.global.jwt;
import ddururi.bookbookclub.domain.user.entity.User;
import ddururi.bookbookclub.domain.user.repository.UserRepository;
import ddururi.bookbookclub.global.security.CustomUserDetails;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserRepository userRepository;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Authorization 헤더에서 토큰 추출
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7); // "Bearer " 이후
if (jwtUtil.validateToken(token)) {
String email = jwtUtil.getEmailFromToken(token);
User user = userRepository.findByEmail(email)
.orElse(null); // 탈퇴하거나 삭제된 유저면 인증 안 함
if (user != null) {
CustomUserDetails customUserDetails = new CustomUserDetails(user);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(request, response);
}
}
- 헤더에 Bearer <token> 형식이 있으면
- jwtUtil로 검증 → UserRepository로 사용자 조회
- 인증 성공 시 SecurityContextHolder에 등록
📌 시큐리티 설정 (SecurityConfig.java)
- 시큐리티 전체 설정 담당
- CSRF 비활성화, 세션 사용 안 함, 인증 필터 등록, 경로별 권한 설정 등
package ddururi.bookbookclub.global.config;
import ddururi.bookbookclub.global.jwt.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/api/users/login",
"/api/users",
"/api/users/check-email",
"/api/users/check-nickname"
).permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
}
- permitAll(): 회원가입, 로그인 API는 인증 없이 허용
- anyRequest().authenticated(): 그 외는 토큰 필요
✅ CSRF를 disable() 해야 하는 이유
- 스프링 시큐리티는 기본적으로 CSRF 방어를 켜둠
- 하지만 우리는 JWT를 헤더에 직접 넣어 요청하기 때문에 CSRF 공격에 해당되지 않음
- 그래서 설정에서 반드시 명시적으로 꺼줘야 함
📌 컨트롤러 구현 (UserController.java)
1. 인증 사용자 정보 조회 (@AuthenticationPrincipal 사용)
- 로그인 성공 시 jwtUtil.createToken(email) 호출 → 클라이언트에게 토큰 전달
@PostMapping("/login")
public ResponseEntity<ApiResponse<LoginResponse>> login(@RequestBody @Valid UserLoginRequest request) {
UserResponse user = userService.login(request);
String token = jwtUtil.createToken(user.getEmail());
LoginResponse response = new LoginResponse(token, user);
return ResponseEntity.ok(ApiResponse.success(response));
}
📌 로그인에 빌요한 DTO 생성 (LoginResponse.java)
package ddururi.bookbookclub.domain.user.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class LoginResponse {
private String token;
private UserResponse user;
}
[Postman 을 통한 테스트]
POST http://localhost:8080/api/users/login
{
"email": "test@example.com",
"password": "12345678"
}
1. 인증 사용자 정보 조회 (@AuthenticationPrincipal 사용)
@GetMapping("/me")
public ResponseEntity<ApiResponse<UserResponse>> getMyInfo(
@AuthenticationPrincipal CustomUserDetails userDetails
) {
return ResponseEntity.ok(ApiResponse.success(UserResponse.from(userDetails.getUser())));
}
[Postman 을 통한 테스트]
GET http://localhost:8080/api/users/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
{
"success": true,
"data": {
"id": 1,
"email": "test@example.com",
"nickname": "북덕이",
"role": "USER"
},
"message": null
}
728x90
320x100
'💻 뚝딱뚝딱 > 북북클럽' 카테고리의 다른 글
[개발일지 #007] 로그인 구현2 (refreshToken 도입) / 로그아웃 (0) | 2025.04.23 |
---|---|
[개발일지 #006] 회원가입 전, 이메일 인증 구현 (0) | 2025.04.22 |
[개발일지 #004] 회원 정보 수정 API 구현 (0) | 2025.04.22 |
[개발일지 #003] 회원(User) 도메인 회원가입 API 구현 및 테스트 (0) | 2025.04.22 |
[개발일지#002] 회원(User) 도메인 단위 테스트 (0) | 2025.04.18 |