개발놀이터
스프링 시큐리티 + JWT 인증 레이어 추가하기 (4) : 주요 클래스 (SecurityConfig와 OAuth2 Handler) 본문
스프링 시큐리티 + JWT 인증 레이어 추가하기 (4) : 주요 클래스 (SecurityConfig와 OAuth2 Handler)
마늘냄새폴폴 2023. 5. 20. 05:25https://coding-review.tistory.com/384
이전 포스팅과 이어집니다.
package com.hello.capston.oauth;
import com.hello.capston.jwt.JwtAuthenticationFilter;
import com.hello.capston.jwt.JwtTokenProvider;
import com.hello.capston.jwt.OAuth2LoginSuccessHandler;
import com.hello.capston.jwt.gitbefore.RedisTokenRepository;
import com.hello.capston.jwt.service.JwtService;
import com.hello.capston.jwtDeprecated.JwtUtil;
import com.hello.capston.principal.PrincipalDetailService;
import com.hello.capston.repository.MemberRepository;
import com.hello.capston.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomOAuth2UserService customOAuth2UserService;
private final PrincipalDetailService principalDetailService;
private final JwtTokenProvider jwtTokenProvider;
private final RedisTemplate redisTemplate;
private final MemberRepository memberRepository;
private final UserRepository userRepository;
private final RedisTokenRepository redisTokenRepository;
@Bean
public BCryptPasswordEncoder encodePWD() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(principalDetailService).passwordEncoder(encodePWD());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().headers().frameOptions().disable()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.authorizeRequests().antMatchers(
"/", "/css/**", "/image/**", "/js/**", "/h2-console/**", "/login", "/item_list/**",
"/item_detail/**", "/social_login", "/join", "/login_id_duplicate", "/sendEmail", "/checkNumber",
"/find_by_detail_category/**", "/item_list_popular/**", "/inquiry/**", "/custom/login", "/custom/logout",
"/oauth2/authorization/google", "/oauth2/authorization/naver", "/oauth2/authorization/kakao", "/erase/authToken/authentication"
).permitAll()
.antMatchers("/item_upload", "/detail_upload/**").hasAnyRole("MANAGER", "ADMIN")
.antMatchers("/admin/**", "/coupon_upload").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, redisTemplate, redisTokenRepository, memberRepository, userRepository), UsernamePasswordAuthenticationFilter.class)
.oauth2Login().loginPage("/login")
.and()
.logout().logoutSuccessUrl("/")
.and()
.oauth2Login().userInfoEndpoint().userService(customOAuth2UserService)
.and()
.successHandler(new OAuth2LoginSuccessHandler(jwtTokenProvider, redisTemplate, redisTokenRepository, userRepository));
//중복 로그인
http.sessionManagement()
.maximumSessions(1) //세션 최대 허용 수
.maxSessionsPreventsLogin(false); // false 이면 중복 로그인하면 이전 로그인이 풀린다.
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
좀 복잡하긴 한데... 스프링 시큐리티의 기본적인 메서드는 넘어가도록 하겠습니다.
구조는 이렇습니다.
- 인증이 필요없는 URL 설정
- ROLE_MANAGER와 ROLE_ADMIN만 입장할 수 있는 URL
- ROLE_ADMIN만 입장할 수 있는 URL
- UsernamePasswordAuthenticationFilter앞에 우리가 만든 JwtAuthenticationFilter 끼워넣기
- OAuth는 자체적으로 커스텀하게 만든 CustomOAuth2UserService 장착
- OAuth로그인이 끝나면 SuccessHandler를 실행
4번 같은 경우는 꼭 UsernamePasswordAuthenticationFilter 앞에 우리가 커스텀하게 만든 JwtFilter가 있어야합니다.
왜냐하면 스프링 시큐리티같은 경우는 여러개의 필터 중 하나라도 작동해서 인증에 성공하면 다음 필터는 작동되지 않기 때문입니다.
우리는 로그인과 인증에 JWT를 사용하기 때문에 JWT 필터를 맨 앞에 둬야합니다.
또한 6번같은 경우는 OAuth는 기본적으로 폼 기반의 로그인 (Username과 Password를 넘기는)과는 다른 필터를 사용합니다.
그리고 인증하는 과정도 폼 기반 로그인과 많이 다릅니다. 때문에 일반적인 폼 형태의 로그인은 컨트롤러를 통해 쿠키와 Redis 객체를 생성해주면 됐지만 OAuth는 컨트롤러를 타지 않고 인증을 거칩니다.
때문에 OAuth가 모두 끝났을 때 쿠키와 Redis 객체를 생성해주고 저장해야합니다.
이런 우리에게 아주 꿀맛같은 인터페이스가 있습니다. 이걸 찾아내는데 굉장히 많은 삽질을 했는데...
바로 AuthenticationSuccessHandler입니다. (스프링 짱짱... 내가 원하는 인터페이스 웬만한건 다있어 대박)
package com.hello.capston.jwt;
import com.hello.capston.entity.User;
import com.hello.capston.jwt.dto.UserResponseDto;
import com.hello.capston.jwt.gitbefore.RedisAndSession;
import com.hello.capston.jwt.gitbefore.RedisTokenRepository;
import com.hello.capston.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.ResponseCookie;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
@Slf4j
@RequiredArgsConstructor
public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler {
private final JwtTokenProvider jwtTokenProvider;
private final RedisTemplate redisTemplate;
private final RedisTokenRepository redisTokenRepository;
private final UserRepository userRepository;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
UserResponseDto tokenInfo = jwtTokenProvider.generateToken(authentication);
// NEW!
String sessionId = request.getSession(true).getId();
RedisAndSession redisAndSession = RedisAndSession.builder().sessionId(sessionId).refreshToken(tokenInfo.getRefreshToken()).build();
redisTokenRepository.save(redisAndSession);
Cookie sessionCookie = new Cookie("SESSION-TOKEN", sessionId);
sessionCookie.setHttpOnly(true);
ResponseCookie responseSessionCookie = ResponseCookie
.from("SESSION-TOKEN", sessionId)
.maxAge(Duration.ofDays(7))
.httpOnly(true)
.path("/")
.sameSite("Lax")
.build();
response.setHeader("Set-Cookie", responseSessionCookie.toString());
response.addCookie(sessionCookie);
User principal = (User) authentication.getPrincipal();
User user = userRepository.findByEmail(principal.getEmail()).map(entity -> entity.updateSessionId(sessionId)).orElse(principal);
userRepository.save(user);
// NEW!
redisTemplate.opsForValue()
.set("RT:" + authentication.getName(), tokenInfo, tokenInfo.getRefreshTokenExpirationTime(), TimeUnit.MILLISECONDS);
SecurityContextHolder.getContext().setAuthentication(authentication);
Cookie cookie = new Cookie("AUTH-TOKEN", tokenInfo.getAccessToken());
cookie.setHttpOnly(true);
ResponseCookie responseCookie =
ResponseCookie.from("AUTH-TOKEN", tokenInfo.getAccessToken())
.sameSite("Lax")
.httpOnly(true)
.maxAge(Duration.ofMinutes(30))
.path("/")
.build();
response.addCookie(cookie);
response.setHeader("Set-Cookie", responseCookie.toString());
response.sendRedirect("/");
}
}
뭐가 많이 긴데 그냥 Access Token 쿠키와 Session Id 쿠키, 그리고 Redis 객체를 생성해주는 코드입니다. 그냥 Controller에서 하던 것을 핸들러에서 처리한다 이렇게 생각하시면 됩니다.
마치며
정말 우여곡절이 많았습니다. 우선 제가 참고한 코드를 제 프로젝트에 걸맞게 커스터마이징 하는 과정에서 많은 삽질이 있었습니다.
아마 이 기능을 만드실 때 기본적인 JWT만들고 쿠키로 간단하게 만드실 분은 상관없겠지만 스프링 시큐리티에 대한 전반적인 이해를 하고 만드시는 것을 추천드립니다.
생각보다 스프링 시큐리티와 JWT를 결합하는 것이 만만치않더군요. 폼 기반 로그인은 어떤 필터를 타고 어떤 과정을 거쳐서 인증이 되는지, OAuth는 어떤 필터를 거치고 어떤 과정으로 인증이 되는지 이에 대한 학습이 조금 부족하다면 만드시는데 큰 어려움이 있을 것이라고 예상합니다.
아래 제가 참고한 깃헙 링크와 블로그 포스팅 링크를 달아두도록 하겠습니다.
출처
https://wildeveloperetrain.tistory.com/57
=> 이 블로그를 참고했습니다.
https://wildeveloperetrain.tistory.com/50
=> 이 블로그에서 스프링 시큐리티에 대한 전반적인 이해를 권장합니다.
깃허브
https://github.com/JianChoi-Kor/Login
=> 이분 깃헙을 기반으로 만들었습니다. 제 깃헙 보다는 이분것을 기반으로 만드는 것을 추천드립니다. 제건 너무 클래스도 많고 난잡해서...
https://github.com/garlicpollpoll/capston
=> 혹시 몰라서 제 깃헙 주소도 적어두도록 하겠습니다. 패키지는 capston > jwt 에 모아놨습니다. 포스팅에 있던 컨트롤러는 capston > controller > login > LoginController.java 입니다. 또한 SecurityConfig는 capston > oauth > SecurityConfig 여기에 있습니다.
'Spring > Spring Security' 카테고리의 다른 글
CAS 인증서버 구축하기 (1) : CAS 서버 가져오기 (1) | 2024.02.25 |
---|---|
SSO (부제 : Spring Security CAS) (0) | 2024.02.25 |
스프링 시큐리티 + JWT 인증 레이어 추가하기 (2) : 주요 클래스 (JwtProvider와 JwtFilter) (0) | 2023.05.20 |
스프링 시큐리티 + JWT 인증 레이어 추가하기 (1) : 개요 (0) | 2023.05.20 |
스프링 시큐리티 동작 원리 (0) | 2023.05.17 |