Skip to content

Commit

Permalink
[feat] #5 JWT 인증을 통한 임시 로그인 기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
dogsub committed Jan 12, 2025
1 parent 47eaaac commit c88b51d
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ public ResponseEntity<GlobalResponseDto<MemberResponseDto>> createMember(@Valid
})
@PostMapping("/login")
public ResponseEntity<GlobalResponseDto<String>> login(@RequestBody LoginRequestDto loginRequest) {
String message = memberService.login(loginRequest);
return ResponseEntity.ok(GlobalResponseDto.success(message));
// 로그인 로직 → 토큰 발급
String token = memberService.login(loginRequest);

// 토큰을 응답 바디로 전달
return ResponseEntity.ok(GlobalResponseDto.success(token));
}

// 모든 회원 조회 API
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,62 @@
package com.wedit.weditapp.domain.member.service;

import com.wedit.weditapp.domain.member.domain.Member;
import com.wedit.weditapp.domain.member.domain.repository.MemberRepository;
import com.wedit.weditapp.domain.member.dto.LoginRequestDto;
import com.wedit.weditapp.domain.member.dto.MemberRequestDto;
import com.wedit.weditapp.global.error.ErrorCode;
import com.wedit.weditapp.global.error.exception.CommonException;
import com.wedit.weditapp.global.security.jwt.JwtProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
private final JwtProvider jwtProvider; // JWT 발급을 위해

// [회원가입 관련] - 이미 존재하는 이메일인지 검사 + Member 엔티티 생성 + DB저장 및 반환
public Member createMember(MemberRequestDto requestDto) {
if (memberRepository.findByEmail(requestDto.getEmail()).isPresent()) {
throw new CommonException(ErrorCode.MEMBER_ALREADY_EXISTS);
}

Member member = Member.createUser(
requestDto.getEmail(),
requestDto.getPassword(),
requestDto.getName()
);

return memberRepository.save(member);
}

public String login(LoginRequestDto loginRequest) {
// 이메일로 회원 조회
Member member = memberRepository.findByEmail(loginRequest.getEmail())
.orElseThrow(() -> new CommonException(ErrorCode.USER_NOT_FOUND));

// 비밀번호 검증 (임시: 평문 비교)
if (!member.getPassword().equals(loginRequest.getPassword())) {
throw new CommonException(ErrorCode.LOGIN_FAIL);
}

// JWT Access Token 생성
return jwtProvider.createAccessToken(member.getEmail());
}

// [모든 회원 조회]
public List<Member> findAllMembers() {
return memberRepository.findAll();
}

// [단일 회원 조회]
public Member findMemberById(Long userId) {
return memberRepository.findById(userId)
.orElseThrow(() -> new CommonException(ErrorCode.USER_NOT_FOUND)
);
}

}
26 changes: 24 additions & 2 deletions src/main/java/com/wedit/weditapp/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@

import java.util.Arrays;

import com.wedit.weditapp.global.security.jwt.JwtAuthenticationFilter;
import com.wedit.weditapp.global.security.jwt.JwtProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
Expand All @@ -22,13 +28,19 @@
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.cors(withDefaults())
.csrf(AbstractHttpConfigurer::disable)

// 세션 관련 정책 추가 : 세션 방식 사용 X (오직 JWT만 사용)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

.headers(
headersConfigurer ->
headersConfigurer
Expand All @@ -39,10 +51,13 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.authorizeHttpRequests(
authorize ->
authorize
.requestMatchers( "/v3/api-docs/**", "/swagger-ui/**").permitAll() // swagger-ui 접근 허용
.anyRequest().permitAll()
.requestMatchers( "/v3/api-docs/**", "/swagger-ui/**", "/api/members/login", "/api/members/signup").permitAll() // swagger-ui 접근 허용 + 로그인과 회원가입까지만 허용
.anyRequest().authenticated() // 그외에는 Access Token을 가진 유저만 접근 가능
);

// JWT 인증 필터 등록
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

Expand All @@ -60,4 +75,11 @@ public CorsConfigurationSource corsConfigurationSource() {
source.registerCorsConfiguration("/**", configuration);
return source;
}

// 회원가입 시 사용
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.wedit.weditapp.global.security.jwt;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;


@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtProvider jwtProvider;

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {

// 헤더에서 토큰 추출
String token = resolveToken(request);

// 토큰 검증
if (token != null && jwtProvider.validateToken(token)) {
// 토큰에서 사용자 정보 추출
String email = jwtProvider.getEmail(token);

// 임시로 Role없이 Authentication 객체 생성 -> 추후 추가 예정
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(email, null, null);

// SecurityContext에 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
}

// 다음 필터로 진행
filterChain.doFilter(request, response);
}

// "Authorization: Bearer <token>" 헤더에서 token만 파싱
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.wedit.weditapp.global.security.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;

@Slf4j
@Component
public class JwtProvider {

@Value("${jwt.secretKey}")
private String secretKey;

@Value("${jwt.access.expiration}")
private long accessTokenExpiry; // 만료 시간 : 3600000 (1시간)

private Key key; // 실제 사용할 HMAC용 key 객체

// Bean 생성 직후 secretKey를 Key 객체로 변환
@PostConstruct
protected void init() {
this.key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
}


// JWT Access Token 생성
/*
* - subject로 email (또는 userId 등) 지정
* - 만료시간 설정
* - 서명 알고리즘: HS256
*/
public String createAccessToken(String email) {
Date now = new Date();
Date expiry = new Date(now.getTime() + accessTokenExpiry);

return Jwts.builder()
.subject(email)
.issuedAt(now)
.expiration(expiry)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}

// JWT Access Token 유효성 검증
public boolean validateToken(String token) {
try {
// 토큰 파싱 -> 에러 없으면 유효
Jwts.parser()
.verifyWith((SecretKey) key)
.build()
.parseSignedClaims(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.error("Invalid JWT signature: {}", e.getMessage());
} catch (ExpiredJwtException e) {
log.error("Expired JWT token: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token: {}", e.getMessage());
} catch (IllegalArgumentException e) {
log.error("JWT token compact of handler are invalid: {}", e.getMessage());
}
return false;
}

// 토큰에서 email(Subject) 추출
public String getEmail(String token) {
return Jwts.parser()
.verifyWith((SecretKey) key)
.build()
.parseSignedClaims(token).getPayload()
.getSubject();
}
}

0 comments on commit c88b51d

Please sign in to comment.