-> 코드의 흐름 알아보는 글
스프링은 인증과 인가기능을 가진 프레임워크인 Spring Security를 제공한다.
그 중 소셜로그인을 위해 제공하는 프로토콜이 OAuth이다.
OAuth(Open Authorization)는 토큰 기반의 인증 및 권한을 위한 표준 프로토콜입니다. OAuth와 같은 인증 프로토콜을 통해 유저의 정보를 페이스북, 구글, 카카오 등의 서비스에서 제공받을 수 있고 이 정보를 기반으로 어플리케이션 사용자에게 로그인이나 다른 여러 기능들을 손쉽게 제공할 수 있습니다.
oauth를 통해 네이버, 카카오, 구글, 로컬 로그인을 진행한다.
차근차근 코드를 분석하면서 진행하겠지만 그래도 이해가 어려운 부분이 많다.
이 세개를 인코딩한 문자열이 나온다. 그 문자열을 디코딩하면 위 3가지 정보가 나온다. 토큰안에 정보를 들고다니는거다.
/**
 * @package : com.checkmate.backend.oauth.token
 * @name: AuthToken.java
 * @date : 2022/05/22 5:13 오후
 * @author : jifrozen
 * @version : 1.0.0
 * @description : Jwt 패키지를 생성, 토큰 생성과 토큰의 유효성 검증 담당
 * @modified :
 **/
@Slf4j
@RequiredArgsConstructor
public class AuthToken {
	private static final String AUTHORITIES_KEY = "role";
	@Getter
	private final String token;
	private final Key key;
	AuthToken(String id, Date expiry, Key key) {
		this.key = key;
		this.token = createAuthToken(id, expiry);
	}
	AuthToken(String id, String role, Date expiry, Key key) {
		this.key = key;
		this.token = createAuthToken(id, role, expiry);
	}
	//토큰 생성
	private String createAuthToken(String id, Date expiry) {
		return Jwts.builder()
			.setSubject(id)
			.signWith(key, SignatureAlgorithm.HS256)
			.setExpiration(expiry)
			.compact();
	}
	private String createAuthToken(String id, String role, Date expiry) {
		return Jwts.builder()
			.setSubject(id)
			.claim(AUTHORITIES_KEY, role)
			.signWith(key, SignatureAlgorithm.HS256)
			.setExpiration(expiry)
			.compact();
	}
	// 검증 부분
	public boolean validate() {
		return this.getTokenClaims() != null;
	}
	public Claims getTokenClaims() {
		try {
			return Jwts.parserBuilder()
				.setSigningKey(key)
				.build()
				.parseClaimsJws(token)
				.getBody();
		} catch (SecurityException e) {
			log.info("Invalid JWT signature.");
		} catch (MalformedJwtException e) {
			log.info("Invalid JWT token.");
		} catch (ExpiredJwtException e) {
			log.info("Expired JWT token.");
		} catch (UnsupportedJwtException e) {
			log.info("Unsupported JWT token.");
		} catch (IllegalArgumentException e) {
			log.info("JWT token compact of handler are invalid.");
		}
		return null;
	}
	public Claims getExpiredTokenClaims() {
		try {
			Jwts.parserBuilder()
				.setSigningKey(key)
				.build()
				.parseClaimsJws(token)
				.getBody();
		} catch (ExpiredJwtException e) {
			log.info("expired JWT token.");
			return e.getClaims();
		}
		return null;
	}
}@Slf4j
public class AuthTokenProvider {
	private static final String AUTHORITIES_KEY = "role";
	private final Key key;
	public AuthTokenProvider(String secret) {
		// 스트링인 yml 파일의 secret키를 인코딩 된 byte 배열로 변환후 다시 SecretKey로 변환해 주는 hmacShaKeyFor
		this.key = Keys.hmacShaKeyFor(secret.getBytes());
	}
	public AuthToken createAuthToken(String id, Date expiry) {
		return new AuthToken(id, expiry, key);
	}
	public AuthToken createAuthToken(String id, String role, Date expiry) {
		return new AuthToken(id, role, expiry, key);
	}
	// String -> token
	public AuthToken convertAuthToken(String token) {
		return new AuthToken(token, key);
	}
	// token 값에서 사용자의 정보를 꺼내는 함수
	public Authentication getAuthentication(AuthToken authToken) {
		if (authToken.validate()) {
			Claims claims = authToken.getTokenClaims();
			Collection<? extends GrantedAuthority> authorities =
				Arrays.stream(new String[] {claims.get(AUTHORITIES_KEY).toString()})
					.map(SimpleGrantedAuthority::new)
					.collect(Collectors.toList());
			log.debug("claims subject := [{}]", claims.getSubject());
			User principal = new User(claims.getSubject(), "", authorities);
			return new UsernamePasswordAuthenticationToken(principal, authToken, authorities);
		} else {
			throw new TokenValidFailedException();
		}
	}
}/**
 * @package : com.checkmate.backend.oauth.config
 * @name: JwtConfig.java
 * @date : 2022/05/22 5:38 오후
 * @author : jifrozen
 * @version : 1.0.0
 * @description : Jwt을 사용하기 위한 설정
 * @modified :
 **/
@Configuration
public class JwtConfig {
	@Value("${jwt.secret}")
	private String secret;
	@Bean
	public AuthTokenProvider jwtProvider() {
		return new AuthTokenProvider(secret);
	}
}Filter base class that aims to guarantee a single execution per request dispatch, on any servlet container. It provides a doFilterInternal method with HttpServletRequest and HttpServletResponse arguments.
요청 당 한번의 실행을 보장 → 자원 낭비를 줄이기 위해 통일한 request 안에서 한번만 필터링 해주는 filter이다.
따라서 여기서는 request안에 토큰 값을 받아 유효한지 확인해주는 작업을 한다.
출처
/**
 * @package : com.checkmate.backend.oauth.filter
 * @name: TokenAuthenticationFilter.java
 * @date : 2022/05/22 5:38 오후
 * @author : jifrozen
 * @version : 1.0.0
 * @description : Generating Token / Validating JWT 과정에서 첫번째 필터 OncePerRequestFilter -> 토큰을 유효성 검사
 * @modified :
 **/
@Slf4j
@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
	private final AuthTokenProvider tokenProvider;
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
		FilterChain filterChain) throws ServletException, IOException {
		String tokenStr = HeaderUtil.getAccessToken(request);
		AuthToken token = tokenProvider.convertAuthToken(tokenStr);
		if (token.validate()) {
			Authentication authentication = tokenProvider.getAuthentication(token);
			SecurityContextHolder.getContext().setAuthentication(authentication);
		}
		//다음 필터로 이동
		filterChain.doFilter(request, response);
	}
}로그인은 지공해주는 provider를 enum타입으로 선언
@Getter
public enum ProviderType {
	GOOGLE,
	NAVER,
	KAKAO,
	LOCAL;
}주체의 인가를 enum타입으로 정의
@Getter
@AllArgsConstructor
public enum RoleType {
	USER("ROLE_USER", "일반 사용자 권한"),
	ADMIN("ROLE_ADMIN", "관리자 권한"),
	GUEST("GUEST", "게스트 권한");
	private final String code;
	private final String displayName;
	public static RoleType of(String code) {
		return Arrays.stream(RoleType.values())
			.filter(r -> r.getCode().equals(code))
			.findAny()
			.orElse(GUEST);
	}
}@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "USER")
public class User {
	@JsonIgnore
	@Id
	@Column(name = "USER_SEQ")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long userSeq;
	@Column(name = "USER_ID", length = 64, unique = true)
	@NotNull
	@Size(max = 64)
	private String userId;
	@Column(name = "USERNAME", length = 100)
	@NotNull
	@Size(max = 100)
	private String username;
	@JsonIgnore
	@Column(name = "PASSWORD", length = 128)
	@NotNull
	@Size(max = 128)
	private String password;
	@Column(name = "PROVIDER_TYPE", length = 20)
	@Enumerated(EnumType.STRING)
	@NotNull
	private ProviderType providerType;
	@Column(name = "ROLE_TYPE", length = 20)
	@Enumerated(EnumType.STRING)
	@NotNull
	private RoleType roleType;
	@Column(name = "CREATED_AT")
	@NotNull
	private LocalDateTime createdAt;
	@Column(name = "MODIFIED_AT")
	@NotNull
	private LocalDateTime modifiedAt;
	public User(
		@NotNull @Size(max = 64) String userId,
		@NotNull @Size(max = 100) String username,
		@NotNull @Size(max = 128) String password,
		@NotNull ProviderType providerType,
		@NotNull RoleType roleType,
		@NotNull LocalDateTime createdAt,
		@NotNull LocalDateTime modifiedAt
	) {
		this.userId = userId;
		this.username = username;
		this.password = password;
		this.providerType = providerType;
		this.roleType = roleType;
		this.createdAt = createdAt;
		this.modifiedAt = modifiedAt;
	}
}@Repository
public interface UserRepository extends JpaRepository<User, Long> {
   User findByUserId(String userId);
}
인증된 스프링 시큐리티의 principal(주제 정보)를 나타낸다.
OAuth2User → 소셜로그인
UserDetauls → 로컬 로그인
oidcUser → openID
/**
 *@package: com.checkmate.backend.oauth.entity
 *@name:UserPrincipal.java
 *@date: 2022/05/22 6:20 오후
 *@author: jifrozen
 *@version: 1.0.0
 *@description: Spring Security에서 소셜 로그인 / 로컬 로그인 정보 담고있음
 *@modified:
 **/
@Getter
@Setter
@AllArgsConstructor
@RequiredArgsConstructor
public class UserPrincipal implements OAuth2User, UserDetails, OidcUser {
   private final String userId;
   private final String password;
   private final ProviderType providerType;
   private final RoleType roleType;
   private final Collection<GrantedAuthority> authorities;
   private Map<String, Object> attributes;
   public static UserPrincipal create(User user) {
      return new UserPrincipal(
         user.getUserId(),
         user.getPassword(),
         user.getProviderType(),
         RoleType.USER,
         Collections.singletonList(new SimpleGrantedAuthority(RoleType.USER.getCode()))
      );
   }
   public static UserPrincipal create(User user, Map<String, Object> attributes) {
      UserPrincipal userPrincipal =create(user);
      userPrincipal.setAttributes(attributes);
      return userPrincipal;
   }
   @Override
   public Map<String, Object> getAttributes() {
      return attributes;
   }
   @Override
   public Collection<? extends GrantedAuthority> getAuthorities() {
      return authorities;
   }
   @Override
   public String getName() {
      return userId;
   }
   @Override
   public String getUsername() {
      return userId;
   }
   @Override
   public boolean isAccountNonExpired() {
      return true;
   }
   @Override
   public boolean isAccountNonLocked() {
      return true;
   }
   @Override
   public boolean isCredentialsNonExpired() {
      return true;
   }
   @Override
   public boolean isEnabled() {
      return true;
   }
   @Override
   public Map<String, Object> getClaims() {
      return null;
   }
   @Override
   public OidcUserInfo getUserInfo() {
      return null;
   }
   @Override
   public OidcIdToken getIdToken() {
      return null;
   }
}loadUser 메서드는 서드파티(구글, 카카오, 네이버,,)에 사용자 정보를 요청할 수 있는 accessToken을 얻고 나서 실행된다. 이 때 token같은 정보들이 OAuth2UserRequest안에 들어있다.
출처 - https://yelimkim98.tistory.com/49?category=958402
/**
 * @package : com.checkmate.backend.oauth.service
 * @name: CustomOAuth2UserService.java
 * @date : 2022/05/22 6:24 오후
 * @author : jifrozen
 * @version : 1.0.0
 * @description : 소셜로그인 처리하는 Service
 * @modified :
 **/
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
	private final UserRepository userRepository;
	@Override
	public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
		OAuth2User user = super.loadUser(userRequest);
		try {
			return this.process(userRequest, user);
		} catch (AuthenticationException ex) {
			throw ex;
		} catch (Exception ex) {
			ex.printStackTrace();
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause());
		}
	}
	private OAuth2User process(OAuth2UserRequest userRequest, OAuth2User user) {
		// naver google kakao 구분 짓는 코드
		ProviderType providerType = ProviderType.valueOf(
			userRequest.getClientRegistration().getRegistrationId().toUpperCase());
		OAuth2UserInfo userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(providerType, user.getAttributes());
		// 회원가입 된 User 정보 찾음
		User savedUser = userRepository.findByUserId(userInfo.getId());
		if (savedUser != null) {//회원가입 o
			if (providerType != savedUser.getProviderType()) {
				throw new OAuthProviderMissMatchException(
					"Looks like you're signed up with " + providerType +
						" account. Please use your " + savedUser.getProviderType() + " account to login."
				);
			}
			updateUser(savedUser, userInfo);
		} else {//회원가입 x
			savedUser = createUser(userInfo, providerType);
		}
		return UserPrincipal.create(savedUser, user.getAttributes());
	}
	//첫 로그인시 회원가입
	private User createUser(OAuth2UserInfo userInfo, ProviderType providerType) {
		LocalDateTime now = LocalDateTime.now();
		User user = new User(
			userInfo.getId(),
			userInfo.getName(),
			"No_Password",
			providerType,
			RoleType.USER,
			now,
			now
		);
		return userRepository.saveAndFlush(user);
	}
	private User updateUser(User user, OAuth2UserInfo userInfo) {
		if (userInfo.getName() != null && !user.getUsername().equals(userInfo.getName())) {
			user.setUsername(userInfo.getName());
		}
		return user;
	}
}/**
 *@package: com.checkmate.backend.oauth.info
 *@name:OAuth2UserInfoFactory.java
 *@date: 2022/05/23 12:59 오전
 *@author: jifrozen
 *@version: 1.0.0
 *@description: ProviderType에 따라 UserInfo 나눔
 *@modified:
 **/
public class OAuth2UserInfoFactory {
   public static OAuth2UserInfo getOAuth2UserInfo(ProviderType providerType, Map<String, Object> attributes) {
      switch (providerType) {
         caseGOOGLE:
            return new GoogleOAuth2UserInfo(attributes);
         caseNAVER:
            return new NaverOAuth2UserInfo(attributes);
         caseKAKAO:
            return new KakaoOAuth2UserInfo(attributes);
         default:
            throw new IllegalArgumentException("Invalid Provider Type.");
      }
   }
}
/**
 *@package: com.checkmate.backend.oauth.service
 *@name:CustomUserDetailService.java
 *@date: 2022/05/23 1:26 오전
 *@author: jifrozen
 *@version: 1.0.0
 *@description: local 로그인을 위함 유저의 정보를 가져옴
 *@modified:
 **/
@Service
@RequiredArgsConstructor
public class CustomUserDetailService implements UserDetailsService {
   private final UserRepository userRepository;
   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      User user = userRepository.findByUserId(username);
      if (user == null) {
         throw new UsernameNotFoundException("Can not find username.");
      }
      return UserPrincipal.create(user);
   }
}
에러가 발생하게되면, user는 client로 redirect되며 이때 에러 메세지는 Query String으로 전달
/**
 * @package : com.checkmate.backend.oauth.handler
 * @name: OAuth2AuthenticationFailureHandler.java
 * @date : 2022/05/23 1:26 오전
 * @author : jifrozen
 * @version : 1.0.0
 * @description : OAuth2 authentication 수행중에 어떠한 error라도 발생하게되면, OAuth2AuthenticationFailureHandler가 invoke
 * @modified :
 **/
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
   private final OAuth2AuthorizationRequestBasedOnCookieRepository authorizationRequestRepository;
   @Override
   public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
      AuthenticationException exception) throws
      IOException, ServletException {
      String targetUrl = CookieUtil.getCookie(request,REDIRECT_URI_PARAM_COOKIE_NAME)
         .map(Cookie::getValue)
         .orElse(("/"));
      exception.printStackTrace();
      targetUrl = UriComponentsBuilder.fromUriString(targetUrl)
         .queryParam("error", exception.getLocalizedMessage())
         .build().toUriString();
      authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
      getRedirectStrategy().sendRedirect(request, response, targetUrl);
   }
}OAuth2AuthenticationSuccessHandler/**
 *@package: com.checkmate.backend.oauth.handler
 *@name:OAuth2AuthenticationFailureHandler.java
 *@date: 2022/05/23 1:29 오전
 *@author: jifrozen
 *@version: 1.0.0
 *@description: OAuth2AuthenticationSuccessHandler는 login 성공시 invoked
 *@modified:
 **/
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
   private final AuthTokenProvider tokenProvider;
   private final AppProperties appProperties;
   private final UserRefreshTokenRepository userRefreshTokenRepository;
   private final OAuth2AuthorizationRequestBasedOnCookieRepository authorizationRequestRepository;
   @Override
   public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication authentication) throws
      IOException, ServletException {
      String targetUrl = determineTargetUrl(request, response, authentication);
      if (response.isCommitted()) {
         logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
         return;
      }
      clearAuthenticationAttributes(request, response);
      getRedirectStrategy().sendRedirect(request, response, targetUrl);
   }
   protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response,
      Authentication authentication) {
      Optional<String> redirectUri = CookieUtil.getCookie(request,REDIRECT_URI_PARAM_COOKIE_NAME)
         .map(Cookie::getValue);
      if (redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) {
         throw new IllegalArgumentException(
            "Sorry! We've got an Unauthorized Redirect URI and can't proceed with the authentication");
      }
      String targetUrl = redirectUri.orElse(getDefaultTargetUrl());
      OAuth2AuthenticationToken authToken = (OAuth2AuthenticationToken)authentication;
      ProviderType providerType = ProviderType.valueOf(authToken.getAuthorizedClientRegistrationId().toUpperCase());
      OidcUser user = ((OidcUser)authentication.getPrincipal());
      OAuth2UserInfo userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(providerType, user.getAttributes());
      Collection<? extends GrantedAuthority> authorities = ((OidcUser)authentication.getPrincipal()).getAuthorities();
      RoleType roleType = hasAuthority(authorities, RoleType.ADMIN.getCode()) ? RoleType.ADMIN: RoleType.USER;
      Date now = new Date();
      AuthToken accessToken = tokenProvider.createAuthToken(
         userInfo.getId(),
         roleType.getCode(),
         new Date(now.getTime() + appProperties.getAuth().getTokenExpiry())
      );
      // refresh 토큰 설정
      long refreshTokenExpiry = appProperties.getAuth().getRefreshTokenExpiry();
      AuthToken refreshToken = tokenProvider.createAuthToken(
         appProperties.getAuth().getTokenSecret(),
         new Date(now.getTime() + refreshTokenExpiry)
      );
      // DB 저장
      UserRefreshToken userRefreshToken = userRefreshTokenRepository.findByUserId(userInfo.getId());
      if (userRefreshToken != null) {
         userRefreshToken.setRefreshToken(refreshToken.getToken());
      } else {
         userRefreshToken = new UserRefreshToken(userInfo.getId(), refreshToken.getToken());
         userRefreshTokenRepository.saveAndFlush(userRefreshToken);
      }
      int cookieMaxAge = (int)refreshTokenExpiry / 60;
      CookieUtil.deleteCookie(request, response,REFRESH_TOKEN);
      CookieUtil.addCookie(response,REFRESH_TOKEN, refreshToken.getToken(), cookieMaxAge);
      return UriComponentsBuilder.fromUriString(targetUrl)
         .queryParam("token", accessToken.getToken())
         .build().toUriString();
   }
   protected void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) {
      super.clearAuthenticationAttributes(request);
      authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
   }
   private boolean hasAuthority(Collection<? extends GrantedAuthority> authorities, String authority) {
      if (authorities == null) {
         return false;
      }
      for (GrantedAuthority grantedAuthority : authorities) {
         if (authority.equals(grantedAuthority.getAuthority())) {
            return true;
         }
      }
      return false;
   }
   private boolean isAuthorizedRedirectUri(String uri) {
      URI clientRedirectUri = URI.create(uri);
      return appProperties.getOauth2().getAuthorizedRedirectUris()
         .stream()
         .anyMatch(authorizedRedirectUri -> {
            // Only validate host and port. Let the clients use different paths if they want to
            URI authorizedURI = URI.create(authorizedRedirectUri);
            if (authorizedURI.getHost().equalsIgnoreCase(clientRedirectUri.getHost())
               && authorizedURI.getPort() == clientRedirectUri.getPort()) {
               return true;
            }
            return false;
         });
   }
}기존에 Exception 처리를 위해 ExceptionAdvice를 만들어줬다. 하지만 Spring Security에서 발생하는 오류는 Exception 처리보다 앞단에서 일어나기 때문에 따로 처리해줘야한다.
@Slf4j
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
   @Override
   public void commence(
      HttpServletRequest request,
      HttpServletResponse response,
      AuthenticationException authException
   ) throws IOException, ServletException {
      authException.printStackTrace();
log.info("Responding with unauthorized error. Message := {}", authException.getMessage());
      response.sendError(
         HttpServletResponse.SC_UNAUTHORIZED,
         authException.getLocalizedMessage()
      );
   }
}application.yml 파일에서 JWT Configuartion을 binding하는 POJO 클래스
/**
 *@package: com.checkmate.backend.oauth.config
 *@name:AppProperties.java
 *@date: 2022/05/23 1:47 오전
 *@author: jifrozen
 *@version: 1.0.0
 *@description: 정의한 토큰 정보를 자바 코드에서 가져다 쓰기 위함
 *@modified:
 **/
@Getter
@ConfigurationProperties(prefix = "app")
public class AppProperties {
   private final Auth auth = new Auth();
   private final OAuth2 oauth2 = new OAuth2();
   @Getter
   @Setter
   @NoArgsConstructor
   @AllArgsConstructor
   public static class Auth {
      private String tokenSecret;
      private long tokenExpiry;
      private long refreshTokenExpiry;
   }
   public static final class OAuth2 {
      private List<String> authorizedRedirectUris = new ArrayList<>();
      public List<String> getAuthorizedRedirectUris() {
         return authorizedRedirectUris;
      }
      public OAuth2 authorizedRedirectUris(List<String> authorizedRedirectUris) {
         this.authorizedRedirectUris = authorizedRedirectUris;
         return this;
      }
   }
}
@Getter
@Setter
@ConfigurationProperties(prefix = "cors")
public class CorsProperties {
   private String allowedOrigins;
   private String allowedMethods;
   private String allowedHeaders;
   private Long maxAge;
}
/**
 * @package : com.checkmate.backend.oauth.config
 * @name: SecurityConfig.java
 * @date : 2022/05/23 1:51 오전
 * @author : jifrozen
 * @version : 1.0.0
 * @description : Spring security를 위한 설정 파일
 * @modified :
 **/
@Configuration
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	private final CorsProperties corsProperties;
	private final AppProperties appProperties;
	private final AuthTokenProvider tokenProvider;
	private final CustomUserDetailService userDetailsService;
	private final CustomOAuth2UserService oAuth2UserService;
	private final TokenAccessDeniedHandler tokenAccessDeniedHandler;
	private final UserRefreshTokenRepository userRefreshTokenRepository;
	/*
	 * UserDetailsService 설정
	 * */
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsService)
			.passwordEncoder(passwordEncoder());
	}
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
        //h2 오류로 추가
        .headers().frameOptions().sameOrigin().and()
			.cors()//cors 허용
			.and()
			.sessionManagement() // session Creation Policy를 STATELESS로 정의해 session을 사용하지 않겠다 선언
			.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
			.and()
			//REST API 이기 떄문에 사용 x
			.csrf().disable()
			.formLogin().disable()
			.httpBasic().disable()
			.exceptionHandling()
			// 사용자가 authentication 없이 protected resource에 접근하는 경우에 invoked 되는 entry point
			.authenticationEntryPoint(new RestAuthenticationEntryPoint())
			//인가되지않은 사용자 Handler
			.accessDeniedHandler(tokenAccessDeniedHandler)
			.and()
			// 경로 허용 경우 설정
			.authorizeRequests()
			.requestMatchers().permitAll()
            ..antMatchers("/**").permitAll()
			//.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
			.antMatchers("/api/**","**/oauth/**").permitAll()
			//.antMatchers("/api/**").hasAnyAuthority(RoleType.USER.getCode())
			//.antMatchers("/api/**/admin/**").hasAnyAuthority(RoleType.ADMIN.getCode())
			//.anyRequest().authenticated()
			.and()
			//OAuth2 로그인 설정 부분
			.oauth2Login()
			// oauth 로그인시 접근할 end point를 정의합니다
			.authorizationEndpoint()
			.baseUri("/oauth2/authorization")
			.authorizationRequestRepository(oAuth2AuthorizationRequestBasedOnCookieRepository())
			.and()
			.redirectionEndpoint()
			.baseUri("/*/oauth2/code/*")
			.and()
			// 로그인시 사용할 User Service를 정의
			.userInfoEndpoint()
			.userService(oAuth2UserService)
			.and()
			//성공 실패 Handler
			.successHandler(oAuth2AuthenticationSuccessHandler())
			.failureHandler(oAuth2AuthenticationFailureHandler());
		//reqeust 요청이 올때마다 UsernamePasswordAuthenticationFilter 이전에 tokenAuthenticaitonFilter를 수행하도록 정의
		http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
	}
	/*
	 * auth 매니저 설정
	 * */
	@Override
	@Bean(BeanIds.AUTHENTICATION_MANAGER)
	protected AuthenticationManager authenticationManager() throws Exception {
		return super.authenticationManager();
	}
	/*
	 * security 설정 시, 사용할 인코더 설정
	 * */
	@Bean
	public BCryptPasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	/*
	 * 토큰 필터 설정
	 * */
	@Bean
	public TokenAuthenticationFilter tokenAuthenticationFilter() {
		return new TokenAuthenticationFilter(tokenProvider);
	}
	/*
	 * 쿠키 기반 인가 Repository
	 * 인가 응답을 연계 하고 검증할 때 사용.
	 * */
	@Bean
	public OAuth2AuthorizationRequestBasedOnCookieRepository oAuth2AuthorizationRequestBasedOnCookieRepository() {
		return new OAuth2AuthorizationRequestBasedOnCookieRepository();
	}
	/*
	 * Oauth 인증 성공 핸들러
	 * */
	@Bean
	public OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler() {
		return new OAuth2AuthenticationSuccessHandler(
			tokenProvider,
			appProperties,
			userRefreshTokenRepository,
			oAuth2AuthorizationRequestBasedOnCookieRepository()
		);
	}
	/*
	 * Oauth 인증 실패 핸들러
	 * */
	@Bean
	public OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler() {
		return new OAuth2AuthenticationFailureHandler(oAuth2AuthorizationRequestBasedOnCookieRepository());
	}
	/*
	 * Cors 설정
	 * */
	@Bean
	public UrlBasedCorsConfigurationSource corsConfigurationSource() {
		UrlBasedCorsConfigurationSource corsConfigSource = new UrlBasedCorsConfigurationSource();
		CorsConfiguration corsConfig = new CorsConfiguration();
		corsConfig.setAllowedHeaders(Arrays.asList(corsProperties.getAllowedHeaders().split(",")));
		corsConfig.setAllowedMethods(Arrays.asList(corsProperties.getAllowedMethods().split(",")));
		corsConfig.setAllowedOrigins(Arrays.asList(corsProperties.getAllowedOrigins().split(",")));
		corsConfig.setAllowCredentials(true);
		corsConfig.setMaxAge(corsConfig.getMaxAge());
		corsConfigSource.registerCorsConfiguration("/**", corsConfig);
		return corsConfigSource;
	}
}/**
 *@package: com.checkmate.backend.oauth.api.controller
 *@name:AuthController.java
 *@date: 2022/05/23 2:10 오전
 *@author: jifrozen
 *@version: 1.0.0
 *@description: 로그인 처리하는 Controller
 *@modified:
 **/
@Tag(name = "OAuth2", description = "로그인 관련 API")
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {
   private final static longTHREE_DAYS_MSEC= 259200000;
   private final static StringREFRESH_TOKEN= "refresh_token";
   private final AppProperties appProperties;
   private final AuthTokenProvider tokenProvider;
   private final AuthenticationManager authenticationManager;
   private final UserRefreshTokenRepository userRefreshTokenRepository;
   private final ResponseService responseService;
   @PostMapping("/login")
   public SingleResult<String> login(
      HttpServletRequest request,
      HttpServletResponse response,
      @RequestBody AuthReqModel authReqModel
   ) {
      Authentication authentication = authenticationManager.authenticate(
         new UsernamePasswordAuthenticationToken(
            authReqModel.getId(),
            authReqModel.getPassword()
         )
      );
      String userId = authReqModel.getId();
      SecurityContextHolder.getContext().setAuthentication(authentication);
      Date now = new Date();
      AuthToken accessToken = tokenProvider.createAuthToken(
         userId,
         ((UserPrincipal)authentication.getPrincipal()).getRoleType().getCode(),
         new Date(now.getTime() + appProperties.getAuth().getTokenExpiry())
      );
      long refreshTokenExpiry = appProperties.getAuth().getRefreshTokenExpiry();
      AuthToken refreshToken = tokenProvider.createAuthToken(
         appProperties.getAuth().getTokenSecret(),
         new Date(now.getTime() + refreshTokenExpiry)
      );
      // userId refresh token 으로 DB 확인
      UserRefreshToken userRefreshToken = userRefreshTokenRepository.findByUserId(userId);
      if (userRefreshToken == null) {
         // 없는 경우 새로 등록
         userRefreshToken = new UserRefreshToken(userId, refreshToken.getToken());
         userRefreshTokenRepository.saveAndFlush(userRefreshToken);
      } else {
         // DB에 refresh 토큰 업데이트
         userRefreshToken.setRefreshToken(refreshToken.getToken());
      }
      int cookieMaxAge = (int)refreshTokenExpiry / 60;
      CookieUtil.deleteCookie(request, response,REFRESH_TOKEN);
      CookieUtil.addCookie(response,REFRESH_TOKEN, refreshToken.getToken(), cookieMaxAge);
      return responseService.getSingleResult(accessToken.getToken());
   }
   @GetMapping("/refresh")
   public SingleResult<String> refreshToken(HttpServletRequest request, HttpServletResponse response) {
      // access token 확인
      String accessToken = HeaderUtil.getAccessToken(request);
      AuthToken authToken = tokenProvider.convertAuthToken(accessToken);
      if (!authToken.validate()) {
         throw new TokenValidFailedException();
      }
      // expired access token 인지 확인
      Claims claims = authToken.getExpiredTokenClaims();
      if (claims == null) {
         throw new TokenValidFailedException("유효한 access token.");
      }
      String userId = claims.getSubject();
      RoleType roleType = RoleType.of(claims.get("role", String.class));
      // refresh token
      String refreshToken = CookieUtil.getCookie(request,REFRESH_TOKEN)
         .map(Cookie::getValue)
         .orElse((null));
      AuthToken authRefreshToken = tokenProvider.convertAuthToken(refreshToken);
      if (authRefreshToken.validate()) {
         throw new TokenValidFailedException("refresh Token 유효하지 않음.");
      }
      // userId refresh token 으로 DB 확인
      UserRefreshToken userRefreshToken = userRefreshTokenRepository.findByUserIdAndRefreshToken(userId,
         refreshToken);
      if (userRefreshToken == null) {
         throw new TokenValidFailedException("refresh Token 유효하지 않음.");
      }
      Date now = new Date();
      AuthToken newAccessToken = tokenProvider.createAuthToken(
         userId,
         roleType.getCode(),
         new Date(now.getTime() + appProperties.getAuth().getTokenExpiry())
      );
      long validTime = authRefreshToken.getTokenClaims().getExpiration().getTime() - now.getTime();
      // refresh 토큰 기간이 3일 이하로 남은 경우, refresh 토큰 갱신
      if (validTime <=THREE_DAYS_MSEC) {
         // refresh 토큰 설정
         long refreshTokenExpiry = appProperties.getAuth().getRefreshTokenExpiry();
         authRefreshToken = tokenProvider.createAuthToken(
            appProperties.getAuth().getTokenSecret(),
            new Date(now.getTime() + refreshTokenExpiry)
         );
         // DB에 refresh 토큰 업데이트
         userRefreshToken.setRefreshToken(authRefreshToken.getToken());
         int cookieMaxAge = (int)refreshTokenExpiry / 60;
         CookieUtil.deleteCookie(request, response,REFRESH_TOKEN);
         CookieUtil.addCookie(response,REFRESH_TOKEN, authRefreshToken.getToken(), cookieMaxAge);
      }
      return responseService.getSingleResult(newAccessToken.getToken());
   }
}
/**
 *@package: com.checkmate.backend.oauth.api.controller
 *@name:UserController.java
 *@date: 2022/05/23 2:15 오전
 *@author: jifrozen
 *@version: 1.0.0
 *@description: 사용자 관리 Controller
 *@modified:
 **/
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
   private final UserService userService;
   private final ResponseService responseService;
   @GetMapping
   public SingleResult<User> getUser() {
      org.springframework.security.core.userdetails.User principal = (org.springframework.security.core.userdetails.User)SecurityContextHolder
         .getContext().getAuthentication().getPrincipal();
      User user = userService.getUser(principal.getUsername());
      return responseService.getSingleResult(user);
   }
   	//회원가입 로직 추가 필요
	@Operation(summary = "회원가입", description = "회원가입 요청")
	@PostMapping("/join")
	public SingleResult<User> join(@RequestBody @Parameter(description = "회원가입 정보", required = true) UserDto user) {
		log.info("회원가입 - {}", user);
		User signedUser = userService.signUpUser(user);
		return responseService.getSingleResult(signedUser);
	}
}