package com.sparta.springauth.jwt;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.springauth.dto.LoginRequestDto;
import com.sparta.springauth.entity.UserRoleEnum;
import com.sparta.springauth.security.UserDetailsImpl;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.io.IOException;
@Slf4j(topic = "로그인 및 JWT 생성")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final JwtUtil jwtUtil;
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
setFilterProcessesUrl("/api/user/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
log.info("로그인 시도");
try {
LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);
// 인증 처리를 하는 메서드
// 여기서 select 이루어짐 → 확인하면서 비밀번호도 확인하고 실패하면 unsuccessfulAuthentication 메서드로
return getAuthenticationManager().authenticate(
// 인증 객체 Token을 넣어줘야 함
new UsernamePasswordAuthenticationToken(
requestDto.getUsername(),
requestDto.getPassword(),
null
)
);
} catch (IOException e) {
log.error(e.getMessage());
throw new RuntimeException(e.getMessage());
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
log.info("로그인 성공 및 JWT 생성");
String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();
String token = jwtUtil.createToken(username, role);
jwtUtil.addJwtToCookie(token, response);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
log.info("로그인 실패");
response.setStatus(401);
}
}
📌 첫번째 매서드
attemptAuthentication
: 로그인 시도
getAuthenticationManager - UsernamePasswordAuthenticationFilter 을 상속받아서 사용할 수 있음
AuthenticationManager 가 .authenticate 라는 메서드를 가지고 있다.
📌 두번째 메서드
successfulAuthentication
: 로그인 성공했을 때 수행되는 메서드 + JWT 생성
마지막에 Authentication 인증 객체를 받아온다. Authentication 여기 안에
UserDetails 가 들어있다. (전에 UserDetails와 UserDetailsService 를 AuthenticationManager 가 사용한다고 했었음!)
authResult.getPrincipal() 이렇게 코드로 직접 장성! ← ProductController 의 @AuthenticationPrincipal 사용했었음!
📌 세번째 메서드
unsuccessfulAuthentication
: 로그인 실패했을 때
⭐️UsernamePasswordAuthenticationFilter 중요!
여기 들어있는 기능들을 사용하기 위해 상속받아서 JwtAuthenticationFilter
UsernamePasswordAuthenticationFilter 역할?
사용자가 username 이랑 password 를 보내면 인증객체 즉,
UsernamePasswordAuthenticationToken 을 만든 다음에
AuthenticationManager 를 통해서 확인까지!!!
직접 custom 해서 만들어 보자. 직접하는 이유?
우리는 JWT 까지 생성해줘야 하기 때문
UsernamePasswordAuthenticationFilter 이 것을 직접 사용하면
Session 방식이기 때문에!
UsernamePasswordAuthenticationFilter 를 상속받으면?
setFilterProcessesUrl 이라는 메서드를 호출할 수 있다.
package com.sparta.springauth.jwt;
import com.sparta.springauth.security.UserDetailsServiceImpl;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Slf4j(topic = "JWT 검증 및 인가")
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsServiceImpl userDetailsService;
public JwtAuthorizationFilter(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {
String tokenValue = jwtUtil.getTokenFromRequest(req);
if (StringUtils.hasText(tokenValue)) {
// JWT 토큰 substring
tokenValue = jwtUtil.substringToken(tokenValue);
log.info(tokenValue);
if (!jwtUtil.validateToken(tokenValue)) {
log.error("Token Error");
return;
}
Claims info = jwtUtil.getUserInfoFromToken(tokenValue);
try {
setAuthentication(info.getSubject());
} catch (Exception e) {
log.error(e.getMessage());
return;
}
}
filterChain.doFilter(req, res);
}
// 인증 처리
public void setAuthentication(String username) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = createAuthentication(username);
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
// 인증 객체 생성
private Authentication createAuthentication(String username) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
}
📌 첫번째 메서드
doFilterInternal
OncePerRequestFilter 상속 받으면 HttpServletRequest, HttpServletResponse를 받아올 수 있다.
jwtUtil.getTokenFromRequest(req); → Cookie 에서 JWT 를 가지고 있는 Cookie 가지고 오는 코드
setAuthentication() → 인증처리하는 메서드
setAuthentication(info.getSubject()); → 사용자 정보(username) 가져올 수 있음
강사님이 Token 만들 때 Subject에 user 이름을 넣었었다?
📌 두번째 메서드
setAuthentication
: 인증처리
원래는 Authentication Maneger 가 Authentication 인증 객체를 만들고
SecurityContextHolder 에 넣어주고 이런 역할들을 내부적으로 원래는 다 해주는데 지금은 우리가 직접 Token 검증하고 그 다음에 인가되었다는 인가처리를 해줘야 한다. → 그래서 우리가 직접 인증 객체 만들고 SecurityContextHolder 에 직접 넣어줘야 한다.
📌 세번째 메서드
createAuthentication
: 인증 객체 생성
userDetailsService.loadUserByUsername(username);-> 해당 user 가 있는지 없는지 확인/ 여기에서 사용하려고!
⭐️ 인가 되었어!
→ 너는 인증된 사용자!
다음 Filter로 넘어가도 문제없이 DispatcherServlet 통해서
Controller 까지 넘어갈 수 있어
Filter 단에서 JWT 방식으로 인증, 인가를 전부 처리하게끔 구현!!!
package com.sparta.springauth.config;
import com.sparta.springauth.jwt.JwtAuthorizationFilter;
import com.sparta.springauth.jwt.JwtAuthenticationFilter;
import com.sparta.springauth.jwt.JwtUtil;
import com.sparta.springauth.security.UserDetailsServiceImpl;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
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.method.configuration.EnableGlobalMethodSecurity;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
public class WebSecurityConfig {
private final JwtUtil jwtUtil;
private final UserDetailsServiceImpl userDetailsService;
private final AuthenticationConfiguration authenticationConfiguration;
public WebSecurityConfig(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService, AuthenticationConfiguration authenticationConfiguration) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
this.authenticationConfiguration = authenticationConfiguration;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
return filter;
}
@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter() {
return new JwtAuthorizationFilter(jwtUtil, userDetailsService);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
http.csrf((csrf) -> csrf.disable());
// 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
http.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
http.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
.requestMatchers("/api/user/**").permitAll() // '/api/user/'로 시작하는 요청 모두 접근 허가
.anyRequest().authenticated() // 그 외 모든 요청 인증처리
);
http.formLogin((formLogin) ->
formLogin
.loginPage("/api/user/login-page").permitAll()
);
// 필터 관리
http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
⭐️ 로그인 방식에서 JWT 방식으로 바꾸는 방법!
Spring Security 에서 기본적으로 제공하는 Session 방식이 아니라 우리가 배운 JWT 를 어떻게 Security 에 적용할 수 있는지?
이전에는 로그인 처리를 하고 JWT 를 생성해서 반환하는 걸 Controller 단에서 했었음→service→repository
📍Filter 구현
→ 인증, 인가 처리와 비즈니스 로직 처리를 분리한다.
UserController 쪽에서 로그인을 구현하지 않고
Filter 단에서만 처리한다!
그 다음 메인페이지 호출됨
Token 검증 다 잘 이루어지고
인증처리에 인증 객체 생성돼서 SecurityContextHolder 에 잘 담겨서
HomeController 까지 왔다..
main 페이지에 가기 위해 만들어 놓은 Controller
전체 코드 좀 공유해주실 수 있나요