[특별 30% 쿠폰할인!!] 스프링부트를 이용한 웹 프로그래밍: 웹사이트 이렇게 만드는 거예요!
블스님이 선물하는 할인쿠폰
스프링부트를 이용한 웹 프로그래밍: 웹사이트 이렇게 만드는 거예요!
www.inflearn.com
이번 시간에는 지난 시간에 이어서 spring security를 적용하겠습니다.
1. filter 패키지 생성
2. LoginRequest 클래스 생성
package com.office.jwtex.jwt.filter;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LoginRequest {
private String id;
private String pw;
}
3. UsernamePasswordAuthenticationFilter를 상속한 JwtUsernamePasswordAuthenticationFilter 클래스 생성
package com.office.jwtex.jwt.filter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.office.jwtex.jwt.JwtService;
import com.office.jwtex.jwt.token.TokenGenerateAndSaveResult;
import com.office.jwtex.jwt.token.TokenResponse;
import com.office.jwtex.member.MemberConstant;
import com.office.jwtex.member.MemberDto;
import com.office.jwtex.member.MemberMapper;
import com.office.jwtex.member.response.SignInResponse;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class JwtUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JwtService jwtService;
private final MemberMapper memberMapper;
public JwtUsernamePasswordAuthenticationFilter(
AuthenticationManager authenticationManager,
JwtService jwtService,
MemberMapper memberMapper) {
this.authenticationManager = authenticationManager;
this.jwtService = jwtService;
this.memberMapper = memberMapper;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
log.info("attemptAuthentication()");
try {
// JSON 요청에서 username과 password를 추출
ObjectMapper objectMapper = new ObjectMapper();
LoginRequest loginRequest = objectMapper.readValue(request.getInputStream(), LoginRequest.class);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginRequest.getId(), loginRequest.getPw());
return authenticationManager.authenticate(authenticationToken);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Failed to parse authentication request", e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
log.info("successfulAuthentication()");
// 인증 성공 시 사용자 정보를 가져옴
UserDetails userDetails = (UserDetails) authResult.getPrincipal();
String username = userDetails.getUsername();
MemberDto memberDto = memberMapper.selectMemberById(username);
// JWT 생성 및 저장
if (memberDto != null) {
TokenGenerateAndSaveResult tokenGenerateAndSaveResult = jwtService.generateTokenAndSave(memberDto);
// 쿠키 설정
response.addHeader(HttpHeaders.SET_COOKIE, tokenGenerateAndSaveResult.getResponseCookie().toString());
// 응답 데이터 작성
Map<String, Object> responseData = new HashMap<>();
responseData.put("signInResponse", SignInResponse.builder()
.isSuccess(true)
.message(MemberConstant.SIGNIN_SUCCESS)
.userId(username)
.build());
responseData.put("tokenResponse", TokenResponse.builder()
.accessToken(tokenGenerateAndSaveResult.getAccessToken())
.refreshToken(tokenGenerateAndSaveResult.getRefreshToken())
.build());
// 응답 헤더 설정
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_OK);
// JSON 변환 및 출력
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter().write(objectMapper.writeValueAsString(responseData));
}
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
log.info("unsuccessfulAuthentication()");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write(new ObjectMapper().writeValueAsString("Authentication failed: " + failed.getMessage()));
}
}
4. UserDetailsService 인터페이스를 구현한 CustomUserDetailsService 클래스 생성
package com.office.jwtex.jwt.filter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.office.jwtex.member.MemberDto;
import com.office.jwtex.member.MemberMapper;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final MemberMapper memberMapper;
public CustomUserDetailsService(MemberMapper memberMapper) {
this.memberMapper = memberMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("loadUserByUsername()");
MemberDto memberDto = memberMapper.selectMemberById(username);
if (memberDto != null)
return User.builder()
.username(memberDto.getId())
.password(memberDto.getPw())
.roles("USER")
.build();
return null;
}
}
5. OncePerRequestFilter 추상클래스를 상속한 JwtAuthenticationFilter 클래스 생성
package com.office.jwtex.jwt.filter;
import java.io.IOException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.filter.OncePerRequestFilter;
import com.office.jwtex.jwt.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final CustomUserDetailsService customUserDetailsService;
public JwtAuthenticationFilter(
JwtUtil jwtUtil,
CustomUserDetailsService customUserDetailsService) {
this.jwtUtil = jwtUtil;
this.customUserDetailsService = customUserDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
log.info("doFilterInternal()");
String authorizationHeader = request.getHeader("Authorization");
String jwtToken = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer "))
jwtToken = authorizationHeader.substring(7); // Remove "Bearer "
if (jwtToken != null && SecurityContextHolder.getContext().getAuthentication() == null) {
if (jwtUtil.validateToken(jwtToken)) {
String username = jwtUtil.getUsername(jwtToken);
UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
if (username.equals(userDetails.getUsername())) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(
customUserDetailsService,
null,
userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
} else {
log.info("Invalid JWT token");
}
}
filterChain.doFilter(request, response);
}
}
6. JwtService 클래스에 JwtUtil getter 추가
public JwtUtil getJwtUtil() {
return jwtUtil;
}
7. SecurityConfig에 JwtUsernamePasswordAuthenticationFilter와 JwtAuthenticationFilter 필터 추가
package com.office.jwtex.config;
import java.util.Arrays;
import java.util.Collections;
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.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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 com.office.jwtex.jwt.JwtService;
import com.office.jwtex.jwt.filter.CustomUserDetailsService;
import com.office.jwtex.jwt.filter.JwtAuthenticationFilter;
import com.office.jwtex.jwt.filter.JwtUsernamePasswordAuthenticationFilter;
import com.office.jwtex.member.MemberMapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final AuthenticationConfiguration authenticationConfiguration;
private final JwtService jwtService;
private final CustomUserDetailsService customUserDetailsService;
private final MemberMapper memberMapper;
public SecurityConfig(
AuthenticationConfiguration authenticationConfiguration,
JwtService jwtService,
CustomUserDetailsService customUserDetailsService,
MemberMapper memberMapper) {
this.authenticationConfiguration = authenticationConfiguration;
this.jwtService = jwtService;
this.customUserDetailsService = customUserDetailsService;
this.memberMapper = memberMapper;
}
@Bean AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean PasswordEncoder passwordEncoder() {
log.info("passwordEncoder()");
return new BCryptPasswordEncoder();
}
@Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors
.configurationSource(new CorsConfigurationSource() {
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Arrays.asList(
"http://localhost:3000",
"http://localhost:3001",
"http://localhost:3002"));
corsConfiguration.setAllowedMethods(Arrays.asList(
"GET",
"POST",
"PUT",
"DELETE"));
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setAllowedHeaders(Collections.singletonList("*"));
corsConfiguration.setMaxAge(3600L);
corsConfiguration.setExposedHeaders(Collections.singletonList("Authorization"));
return corsConfiguration;
}
}))
.csrf(csrf -> csrf
.disable());
http
.formLogin(auth -> auth
.disable())
.httpBasic(auth -> auth
.disable());
http
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/",
"/member/signup",
"/member/signout").permitAll()
.anyRequest().authenticated());
http
.addFilterBefore(new JwtAuthenticationFilter(jwtService.getJwtUtil(), customUserDetailsService), JwtUsernamePasswordAuthenticationFilter.class);
JwtUsernamePasswordAuthenticationFilter jwtUsernamePasswordAuthenticationFilter =
new JwtUsernamePasswordAuthenticationFilter(authenticationManager(authenticationConfiguration), jwtService, memberMapper);
jwtUsernamePasswordAuthenticationFilter.setFilterProcessesUrl("/member/signin");
http
.addFilterAt(jwtUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
http
.sessionManagement(auth -> auth
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}
이번 시간에는 인증에 사용되는 JWT 필터를 만들고 Spring Security에 적용했습니다.
'spring boot' 카테고리의 다른 글
react에서 구글로그인 구현-III(feat. spring) (0) | 2024.12.30 |
---|---|
react에서 구글로그인 구현-II(feat. @react-oauth/google) (0) | 2024.12.30 |
JWT를 이용한 인증-II(feat. react, spring boot) (1) | 2024.12.25 |
JWT를 이용한 인증-I(feat. react) (0) | 2024.12.25 |
Log4j2를 이용한 로깅(logging) 방법 - V (0) | 2024.12.12 |