728x90
1.JWT
인증과 정보 교환을 위해 사용되는 토큰 기반 인증 방식중 하나로 로그인 기능에 JWT(JSON Web Token)을 사용할거다.
그를 위해 일단은 JWT사용을 위한 의존성을 추가해준다.
<!--JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
2.JwtTokenProvider
JWT를 생성, 검증, 그리고 사용자 정보를 추출하는 역할을 담당하는 클래스인 JwtTokenProvider를 생성해줬다.
@Slf4j
@Component
public class JwtTokenProvider {
private static final String SECRET_KEY = "mySecretKeymySecretKeymySecretKey123";
private static final long EXPIRATION_TIME = 1000 * 60 * 60; // 1시간
private final SecretKey key = Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
// JWT 토큰 생성
public String createToken(String userId) {
return Jwts.builder()
.subject(userId)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(key, Jwts.SIG.HS256)
.compact();
}
// JWT 토큰 검증
public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
log.error("Invalid JWT token: {}", e.getMessage());
return false;
}
}
// JWT에서 사용자 ID 추출
public String getUserIdFromToken(String token) {
return Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
// JWT 만료시간 가져오기
public long getExpiration(String token) {
Claims claims = Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload();
return claims.getExpiration().getTime() - System.currentTimeMillis();
}
}
2-1.상수
비밀키, JWT의 유효 기간, HMAC 알고리즘에서 사용할 비밀 키를 상수로 지정해둔다.
private static final String SECRET_KEY = "mySecretKeymySecretKeymySecretKey123";
private static final long EXPIRATION_TIME = 1000 * 60 * 60; // 1시간
private final SecretKey key = Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
2-2.JWT 토큰 생성
로그인에 성공했을 때, 인증 정보를 기반으로 JWT 토큰을 발급해주는 메소드다.
서명(Signature) 은 토큰의 변조를 방지를 위해 하며 서명이 포함된 JWT는 인증된 발급자가 생성한 것이며, 중간에서 변경되지 않았음을 보장한다.
public String createToken(String userId) {
return Jwts.builder() // JWT 빌더 객체 생성
.subject(userId) // 토큰의 subject(사용자 ID) 설정
.issuedAt(new Date()) // 토큰 생성 시간(발급 시간) 설정
.expiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) // 토큰 만료 시간 설정 (현재 시간 + 1시간)
.signWith(key, Jwts.SIG.HS256) // HMAC SHA-256 알고리즘을 사용하여 토큰에 서명
.compact(); // JWT 문자열 생성 및 반환
}
2-3.JWT 토큰 검증
JWT 토큰을 검증하는 메서드이다.
클라이언트가 서버에 요청을 보낼 때, 제공한 JWT가 유효한지 확인하는 역할을 수행한다.
public boolean validateToken(String token) {
try {
Jwts.parser() // JWT 파서를 생성
.verifyWith(key) // 서명 키를 사용하여 JWT 서명을 검증
.build() // 파서 빌드 완료
.parseSignedClaims(token); // 서명이 포함된 JWT 토큰을 파싱하여 검증
return true; // 토큰이 유효하면 true 반환
} catch (JwtException | IllegalArgumentException e) {
log.error("Invalid JWT token: {}", e.getMessage()); // 검증 실패 시 로그 출력
return false; // 토큰이 유효하지 않으면 false 반환
}
}
2-4.JWT에서 사용자 ID 추출
클라이언트가 API 요청 시 JWT에서 사용자 정보를 가져오는 메소드다.
// JWT에서 사용자 ID 추출
public String getUserIdFromToken(String token) {
return Jwts.parser() // JWT 파서를 생성
.verifyWith(key) // 서명 키를 사용하여 JWT 서명을 검증
.build() // 파서 빌드 완료
.parseSignedClaims(token) // 서명이 포함된 JWT 토큰을 파싱
.getPayload() // 토큰의 페이로드(데이터) 추출
.getSubject(); // subject(사용자 ID) 값을 가져옴
}
3.JwtAuthenticationFilter
Spring Security의 필터 역할을 하는 클래스로 클라이언트가 보낸 요청에 JWT 토큰이 포함되어 있는지 확인하고,
유효한 토큰이면 사용자 인증을 수행하는 역할을 한다.
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
//요청에서 JWT 토큰 추출
String token = getTokenFromRequest(request);
//JWT 검증 및 사용자 정보 확인
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
String userId = jwtTokenProvider.getUserIdFromToken(token);
UserDetails userDetails = new User(userId, "", Collections.emptyList()); //Spring Security User 객체 생성
UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); //인증 객체 생성 및 SecurityContext에 저장
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
log.info("인증 완료: 사용자 ID = {}", userId);
}
//다음 필터로 요청 전달
chain.doFilter(request, response);
}
// 요청 헤더에서 JWT 추출
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7); // "Bearer " 제거 후 토큰 반환
}
return null;
}
}
3-1.getTokenFromRequest 메소드
요청 헤더에서 JWT를 추출하는 메소드이다.
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7); // "Bearer " 제거 후 토큰 반환
}
return null;
}
3-2.doFilterInternal 메소드
JwtAuthenticationFilter 클래스의 실제 필터링 작업을 수행하는 핵심 메소드
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
//요청에서 JWT 토큰 추출
String token = getTokenFromRequest(request);
//JWT 검증 및 사용자 정보 확인
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
String userId = jwtTokenProvider.getUserIdFromToken(token);
UserDetails userDetails = new User(userId, "", Collections.emptyList()); //Spring Security User 객체 생성
UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); //인증 객체 생성 및 SecurityContext에 저장
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
log.info("인증 완료: 사용자 ID = {}", userId);
}
//다음 필터로 요청 전달
chain.doFilter(request, response);
}
4.SecurityConfig 수정
이번에 JWT 를 추가하며 새로운 Bean을 추가하고 추가적으로 여러가지를 수정하였다.
4-1.JwtAuthenticationFilter 빈추가
securityFilterChain에 추가할 JwtAuthenticationFilter 를 빈으로 등록해뒀다.
// JwtAuthenticationFilter를 Bean으로 등록
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter(jwtTokenProvider);
}
4-2. CORS 설정 빈 추가
기존 WebConfig 파일에 있던 CORS 설정을 SecurityConfig 파일로 옮겨서 작성하였다.
// CORS 설정
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:5173")); // 프론트엔드 도메인 허용
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
4.securityFilterChain 수정
엔드포인트 추가와 더불어 authorizeHttpRequests내부를 추가 수정하였고 JWT 기반 인증과정을 추가하였다.
Spring Security에서 기본적으로 제공하는 UsernamePasswordAuthenticationFilter는 폼 로그인처리용 필터고 , JWT 인증은 세션을 사용하지 않기 때문에 별도의 JwtAuthenticationFilter가 필요하기때문에 앞에 추가해서 모든 요청의 JWT를 검사하게 하였다.
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORS
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll() // 로그인,로그아웃
.requestMatchers("/member/register").permitAll() // 회원가입
.requestMatchers("/h2-console/**").permitAll() // H2 콘솔
.requestMatchers("/product/**").permitAll() // 상품
.requestMatchers("/images/**").permitAll() // 이미지
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html" ).permitAll()//swagger
.requestMatchers("/test/public").permitAll() //테스트용
.requestMatchers("/test/protected").authenticated() //테스트용
.anyRequest().authenticated()
)
.csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화
.headers(headers -> headers.frameOptions(frame -> frame.sameOrigin())) // H2 Console설정
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 사용안함
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
5.AuthDTO
5-1.AuthRequestDto
로그인을 위한 DTO로 아이디 비번을 입력받는다.
@Getter
@Setter
public class AuthRequestDto {
private String memberId;
private String password;
}
5-2.AuthResponseDto
로그인 후 발급되는 token 을 위한 DTO
@Getter
@AllArgsConstructor
public class AuthResponseDto {
private String token;
}
6.AuthController
인증을 위한 컨트롤러로 로그인 엔드포인트를 만들었다.
로그아웃은 블랙리스트방식이 아닌 프론트엔드에서 처리하는것을 가정하여 우선 엔드포인트는 제작하지않았다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/auth")
@Tag(name = "인증API", description = "/auth")
public class AuthController {
private final AuthService authService;
//로그인 API
@PostMapping("/login")
public ResponseEntity<AuthResponseDto> login(@RequestBody AuthRequestDto request) {
AuthResponseDto response = authService.login(request);
return ResponseEntity.ok(response);
}
}
7.MemberRepository
findBymemberId 메소드를 JPA를 통해 만들어준 MemberRepository다.
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findBymemberId(String memberId);
}
8.AuthService
Auth의 로그인을 처리하는 비즈니스 로직을 처리해준다.
아이디를 통해 회원이 존재하는지 여부를 찾고 비밀번호가 맞으면 해당 맴버 아이디로 JWT 토큰을 만들어서 반환해준다.
@Service
@RequiredArgsConstructor
public class AuthService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
//로그인 (JWT 반환)
public AuthResponseDto login(AuthRequestDto request) {
Member member = memberRepository.findBymemberId(request.getMemberId())
.orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
if (!passwordEncoder.matches(request.getPassword(), member.getPassword())) {
throw new RuntimeException("비밀번호가 일치하지 않습니다.");
}
String token = jwtTokenProvider.createToken(member.getMemberId());
return new AuthResponseDto(token);
}
}
9.테스트 해보기
9-0.SwaggerConfig
Swagger에서 token을 통한 JWT 인증 테스트를 수행하기 위해 SwaggerConfig를 만들어서 다음과같은 설정을 추가하였다.
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Shopping API")
.version("1.0")
.description("쇼핑몰 API 문서"))
.addSecurityItem(new SecurityRequirement().addList("BearerAuth")) // JWT 인증 설정
.components(new io.swagger.v3.oas.models.Components()
.addSecuritySchemes("BearerAuth",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
}
9-1.TestController
토큰이 잘되는지 테스트 하기위한 엔드포인트를 만들어봤다.
protected의 경우 인증되지않으면 접근이 불가능하게 SecurityConfig에 설정하여 만들었다.
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/public")
public String publicEndpoint() {
return "접근성공";
}
@GetMapping("/protected")
public String protectedEndpoint(@AuthenticationPrincipal UserDetails userDetails) {
return "사용자: " + userDetails.getUsername();
}
}
9-2.비인증 상태에서 접근해보기
우선 public의 경우 인증이 필요없는만큼 접근이 성공적으로 가능했다.
protected도 비인증상태에서 정상적으로 접근이 불가능하다는것을 확인하였다. 이제 토큰을 발급받은뒤 테스트 해보자.
9-3.인증키 발급(로그인)
이제 login 엔드포인트에 미리 회원가입으로 만들어둔 아이디와 비번을 입력하여 토큰을 발급받는다.
Response로 토큰을 정상적으로 발급받는것을 확인했다.
이제 Swagger 우상단의 Authorize에 해당 토큰을 넣어서 인증을 한다.
자물쇠가 잠긴 모양이 되며 인증이 성공되었다. 이제 다시 제한된 엔드포인트에 접근해보자
이제 토큰으로 인증되었기 때문에 protected 엔드포인트에 접근하는것을 성공하였다.
9-4.프론트엔드에서 확인 (닉네임 엔드포인트 추가)
이제 React로 프론트엔드 부분을 작성한후 테스트 해보았다.
닉네임 표시를 위해 /me 로 토큰을 통해 얻은 아이디로 닉네임을 조회할수있는 엔드포인트를 추가했다.
@GetMapping("/me")
public ResponseEntity<Map<String, String>> getMe(@AuthenticationPrincipal UserDetails userDetails) {
String nickname = memberService.getNicknameByMemberId(userDetails.getUsername());
Map<String, String> response = new HashMap<>();
response.put("nickname", nickname);
return ResponseEntity.ok(response);
}
728x90
'BackEnd > SpringBoot' 카테고리의 다른 글
[SpringBoot] 쇼핑몰 상품 엔드포인트제작,예외처리, 로그설정 (0) | 2025.02.24 |
---|---|
[SpringBoot] 쇼핑몰 회원가입 기능 제작 (0) | 2025.02.21 |
[SpringBoot] 쇼핑몰 상품 조회기능 만들기 (0) | 2025.02.20 |