최종 순서 요약
이 순서를 따라가면 Spring Security와 JWT 기반 인증을 효율적이고 최신 방식으로 구현 가능!! 🚀
- User 엔티티와 리포지토리 구현: 사용자 데이터 저장 및 조회를 위한 기반.
- UserPrincipal과 CustomUserDetailsService 구현: Spring Security와 사용자 데이터 연동.
- JwtUtil 구현: JWT 생성, 검증, 파싱 로직
- JwtRequestFilter 구현: Spring Security 필터 체인에 JWT 기반 인증 추가.
- Spring Security 설정: 필터 체인, 세션 관리, 접근 제어 설정.
- 인증 컨트롤러 구현: 로그인 및 JWT 발급/검증 API 제공.
1. User 엔티티와 리포지토리 구현
- 목적: 애플리케이션에서 사용자 정보를 데이터베이스에 저장하고 관리하기 위함.
구현
User 엔티티
사용자 정보(이메일, 이름, 역할 등)를 정의. (entity/User.java)
package pda5th.backend.theOne.entity;
@Entity
@Table(name = "users")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id; // 유저ID
@Column(nullable = false, length = 50)
private String name; // 이름
@Column(unique = true, nullable = false, length = 100)
private String email; // 이메일
private String password; // 프로필 이미지 URL
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role; // 역할 (ADMIN, USER)
@Column(updatable = false)
private LocalDateTime createdAt; // 생성 날짜
// 주요 데이터만 설정하는 생성자 제공
@Builder
public User(String name, String email, String password, Role role) {
this.name = name;
this.email = email;
this.password = password;
this.role = role;
this.createdAt = LocalDateTime.now(); // 생성 시간 자동 설정
}
}
UserRepository
• 사용자 검색(이메일 기반) 및 CRUD 작업을 수행하는 JpaRepository. (repository/UserRepository.java)
package pda5th.backend.theOne.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import pda5th.backend.theOne.entity.User;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Integer> {
Optional<User> findByEmail(String email);
}
2. UserPrincipal과 CustomUserDetailsService 구현
- 목적: Spring Security의 인증 시스템과 연동하여 사용자 정보를 로딩 및 관리
- Spring Security는 UserDetails 인터페이스 구현체로 사용자 인증을 처리
구현:
UserPrincipal
- UserDetails 인터페이스를 구현하여 User 엔티티를 Spring Security와 연결.
- 사용자 역할 및 권한 관리.
- common/security/UserPrincipal.java
package pda5th.backend.theOne.common.security;
@Getter
@RequiredArgsConstructor
public class UserPrincipal implements UserDetails {
private final User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(() -> "ROLE_" + user.getRole());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
CustomUserDetailsService
- UserRepository를 통해 User 엔티티를 조회하고, UserPrincipal을 반환.
- Spring Security가 사용자 인증 시 호출.
- common/security/CustomUserDetailsService.java
package pda5th.backend.theOne.common.security;
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + email));
return new UserPrincipal(user); // UserPrincipal 반환
}
}
3. JwtUtil 구현
- 목적: JWT 생성, 파싱, 검증 로직을 캡슐화하여 토큰 기반 인증을 처리.
- 토큰에서 사용자 정보를 추출하거나, 토큰의 유효성을 확인.
• 구현:
- JWT 생성: 사용자 정보를 기반으로 JWT 토큰 생성.
- JWT 검증: 토큰의 유효성을 확인(만료 시간, 서명).
- 사용자 정보 추출: 토큰에서 사용자 이름과 권한 정보를 추출.
common/jwt/util/JwtUtil.java
package pda5th.backend.theOne.common.jwt.util;
@Component
public class JwtUtil {
// application.yml에서 주입된 값
@Value("${jwt.secret}")
private String SECRET_KEY;
@Value("${jwt.expiration}")
private long EXPIRATION_TIME;
// 비밀 키를 Key 객체로 변환 (최신 JJWT에서 권장)
private Key getSigningKey() {
return Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
}
/**
* 사용자 이름(Username)을 기반으로 JWT 토큰을 생성
* @param username 사용자 이름
* @return 생성된 JWT 토큰
*/
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, username);
}
/**
* JWT 토큰 생성 메서드
* @param claims 클레임(Claims)
* @param subject 사용자 식별 정보 (주로 사용자 이름)
* @return 생성된 JWT 토큰
*/
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis())) // 발행 시간
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) // 만료 시간
.signWith(getSigningKey(), SignatureAlgorithm.HS256) // 최신 방식: Key 객체와 알고리즘 설정
.compact();
}
/**
* JWT 토큰에서 사용자 이름(Username)을 추출
* @param token JWT 토큰
* @return 사용자 이름
*/
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
/**
* JWT 토큰에서 만료 시간을 추출
* @param token JWT 토큰
* @return 토큰 만료 시간
*/
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
/**
* JWT 토큰에서 특정 클레임(Claim)을 추출
* @param token JWT 토큰
* @param claimsResolver 클레임을 추출하는 함수
* @return 추출된 클레임 값
*/
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
/**
* JWT 토큰에서 모든 클레임(Claims)을 추출
* @param token JWT 토큰
* @return 클레임 객체
*/
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey()) // 최신 방식: Key 객체로 서명 검증
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* JWT 토큰이 만료되었는지 확인
* @param token JWT 토큰
* @return 만료 여부 (true: 만료됨, false: 유효함)
*/
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
/**
* JWT 토큰의 유효성을 검증
* @param token JWT 토큰
* @param username 검증 대상 사용자 이름
* @return 유효 여부 (true: 유효함, false: 유효하지 않음)
*/
public boolean validateToken(String token, String username) {
final String extractedUsername = extractUsername(token);
return (extractedUsername.equals(username) && !isTokenExpired(token));
}
}
4. JwtRequestFilter 구현
- 목적: Spring Security의 필터 체인에 JWT 인증을 추가.
- 클라이언트 요청의 헤더에서 JWT를 추출하고, 이를 검증하여 SecurityContext에 인증 정보를 설정.
• 구현:
- JWT 추출 및 검증: 요청의 Authorization 헤더에서 JWT 추출.
- JwtUtil을 사용하여 토큰 검증.
- SecurityContext 설정: UserPrincipal을 기반으로 인증 객체 생성 후, SecurityContext에 설정.
common/jwt/util/JwtUtil.java
package pda5th.backend.theOne.common.jwt.filter;
@Component
@RequiredArgsConstructor
public class JwtRequestFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final CustomUserDetailsService customUserDetailsService; // CustomUserDetailsService 사용
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
try {
// 1. 요청에서 JWT 추출
String jwt = extractJwtFromRequest(request);
// 2. JWT가 존재하고 SecurityContext에 인증 정보가 없는 경우
if (jwt != null && SecurityContextHolder.getContext().getAuthentication() == null) {
String username = jwtUtil.extractUsername(jwt); // JWT에서 사용자 이름 추출
if (username != null) {
UserPrincipal userPrincipal = (UserPrincipal) customUserDetailsService.loadUserByUsername(username);
// 3. JWT 유효성 검증
if (jwtUtil.validateToken(jwt, userPrincipal.getUsername())) {
setAuthentication(userPrincipal, request); // 인증 설정
}
}
}
} catch (Exception e) {
// 예외 발생 시 401 상태 코드와 에러 메시지 반환
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Unauthorized: " + e.getMessage());
return; // 필터 체인 중단
}
// 다음 필터로 요청 전달
chain.doFilter(request, response);
}
/**
* 1. Authorization 헤더에서 JWT 추출
*
* @param request HttpServletRequest 객체
* @return JWT 문자열 또는 null
*/
private String extractJwtFromRequest(HttpServletRequest request) {
final String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
return authorizationHeader.substring(7); // "Bearer " 이후의 JWT 반환
}
return null; // 유효한 JWT가 없는 경우 null 반환
}
/**
* 2. SecurityContext에 인증 정보 설정
*
* @param userPrincipal 사용자 인증 정보
* @param request HttpServletRequest 객체
*/
private void setAuthentication(UserPrincipal userPrincipal, HttpServletRequest request) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userPrincipal, null, userPrincipal.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
5. Spring Security 설정
- 목적: 애플리케이션의 보안 규칙을 설정.
- Stateless 인증(JWT 기반)과 특정 엔드포인트의 접근 권한을 정의.
• 구현:
- SecurityFilterChain:
- JWTRequestFilter를 필터 체인에 추가.
- CSRF 비활성화, Stateless 세션 설정.
- PasswordEncoder: 비밀번호를 암호화/복호화하기 위한 BCryptPasswordEncoder 설정.
- AuthenticationManager: CustomUserDetailsService와 연동하여 인증 관리.
config/SecurityConfig.java
package pda5th.backend.theOne.config;
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtRequestFilter jwtRequestFilter; // JWT 필터 주입
private final CustomUserDetailsService customUserDetailsService; // CustomUserDetailsService 주입
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // CSRF 비활성화 (JWT 사용 시 필요 없음)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 비활성화 (Stateless)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/authenticate", "/swagger-ui/**", "/v3/api-docs/**", "/api/auth/login", "/api/users/signup", "/api/timer/*/events", "/api/questions/stream").permitAll() // 인증 필요 없는 경로 설정
.anyRequest().authenticated() // 나머지 요청은 인증 필요
)
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); // JWT 필터를 인증 필터 전에 추가
return http.build(); // SecurityFilterChain 객체 생성
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("http://localhost"); // 허용할 Origin 설정
configuration.addAllowedMethod("*"); // 모든 HTTP 메서드 허용
configuration.addAllowedHeader("*"); // 모든 헤더 허용
configuration.setAllowCredentials(true); // 인증 정보 포함 허용
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 비밀번호 암호화를 위한 BCrypt 사용
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager(); // AuthenticationManager 빈 등록
}
}
6. 인증 컨트롤러 구현
- 목적: 사용자 인증 요청을 처리하고, JWT 토큰을 반환.
- 로그인, 토큰 검증 등의 인증 관련 API 제공.
• 구현:
- 로그인 엔드포인트: 클라이언트가 이메일/비밀번호를 제출하면, 이를 인증하고 JWT 토큰을 반환.
- 토큰 검증 엔드포인트 (선택): 클라이언트가 제출한 JWT 토큰의 유효성을 확인.
controller/AuthControler.java
package pda5th.backend.theOne.controller;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth") // 모든 인증 관련 API를 '/api/auth' 경로로 그룹화
public class AuthController {
private final AuthenticationManager authenticationManager; // Spring Security 인증 관리자
private final JwtUtil jwtUtil; // JWT 유틸리티 클래스
/**
* 로그인하여 JWT 토큰을 발급받는 엔드포인트
*
* @param authRequest 사용자 로그인 요청 (username, password)
* @return JWT 토큰 응답
* @throws Exception 인증 실패 시 예외 발생
*/
@PostMapping("/login")
@Operation(summary = "로그인 및 JWT 토큰 발급", description = "사용자 이름과 비밀번호를 입력하여 JWT 토큰을 발급받습니다.")
public AuthResponse login(@RequestBody AuthRequest authRequest) throws Exception {
try {
// 사용자 인증 수행
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(authRequest.username(), authRequest.password())
);
} catch (AuthenticationException e) {
throw new RuntimeException("Invalid username or password", e); // 인증 실패 시 예외 처리
}
// 인증 성공 시 JWT 토큰 생성 및 반환
String token = jwtUtil.generateToken(authRequest.username());
return new AuthResponse(token); // JWT 토큰을 담은 응답 객체 반환
}
/**
* JWT 토큰의 유효성을 확인하는 엔드포인트
*
* @return 간단한 메시지 (JWT가 유효한 경우)
*/
@GetMapping("/validate-token")
@Operation(summary = "JWT 토큰 유효성 확인", description = "발급받은 JWT 토큰이 유효한지 확인하고, 사용자 정보를 반환합니다.")
public ResponseEntity<UserInfoResponse> validateToken(@AuthenticationPrincipal UserPrincipal userPrincipal) {
Integer userId = userPrincipal.getUser().getId();
String username = userPrincipal.getUser().getName();
return ResponseEntity.ok(new UserInfoResponse(userId, username));
}
}
'Spring > 인증_인가' 카테고리의 다른 글
[Spring] JWT를 이용한 인증,인가 동작 원리 (0) | 2024.11.11 |
---|