-> 코드의 흐름 알아보는 글
스프링은 인증과 인가기능을 가진 프레임워크인 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);
}
}