Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OAuth 추가 #71

Merged
merged 7 commits into from
Jul 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public String getPassword() {

@Override
public String getUsername() {
return null;
return userEntity.getUserId();
}

@Override
Expand Down
28 changes: 27 additions & 1 deletion src/main/java/com/sirius/spurt/common/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
import com.sirius.spurt.common.filter.ExceptionHandlerFilter;
import com.sirius.spurt.common.jwt.AuthorizationFilter;
import com.sirius.spurt.common.jwt.JwtUtils;
import com.sirius.spurt.common.oauth.handler.OAuth2AuthenticationFailHandler;
import com.sirius.spurt.common.oauth.handler.OAuth2AuthenticationSuccessHandler;
import com.sirius.spurt.common.oauth.service.OAuth2Service;
import com.sirius.spurt.store.repository.database.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.configurers.AbstractHttpConfigurer;
Expand All @@ -24,6 +28,7 @@ public class SecurityConfig {
private final UserRepository userRepository;
private final JwtUtils jwtUtils;
private final ObjectMapper objectMapper;
private final OAuth2Service oAuth2Service;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
Expand All @@ -36,14 +41,25 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.authorizeHttpRequests(
(requests) ->
requests
.requestMatchers("/oauth2/**")
.permitAll()
.requestMatchers("/v1/question/random")
.permitAll()
.requestMatchers("/v1/jobgroup", HttpMethod.POST.name())
.permitAll()
.requestMatchers("/error")
.permitAll()
.anyRequest()
.authenticated())
.addFilterBefore(authorizationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(exceptionHandlerFilter(), LogoutFilter.class);
.addFilterBefore(exceptionHandlerFilter(), LogoutFilter.class)
.oauth2Login(
oauth2LoginConfigurer ->
oauth2LoginConfigurer
.successHandler(oAuth2AuthenticationSuccessHandler())
.failureHandler(oAuth2AuthenticationFailHandler())
.userInfoEndpoint(
userInfoEndpoint -> userInfoEndpoint.userService(oAuth2Service)));

return http.build();
}
Expand All @@ -57,4 +73,14 @@ public AuthorizationFilter authorizationFilter() {
public ExceptionHandlerFilter exceptionHandlerFilter() {
return new ExceptionHandlerFilter(objectMapper);
}

@Bean
public OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler() {
return new OAuth2AuthenticationSuccessHandler(jwtUtils);
}

@Bean
public OAuth2AuthenticationFailHandler oAuth2AuthenticationFailHandler() {
return new OAuth2AuthenticationFailHandler(objectMapper);
}
}
2 changes: 2 additions & 0 deletions src/main/java/com/sirius/spurt/common/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.sirius.spurt.common.config;

import com.sirius.spurt.common.jwt.JwtUtils;
import com.sirius.spurt.common.resolver.LoginUserResolver;
import com.sirius.spurt.common.resolver.NonLoginUserResolver;
import java.util.List;
import lombok.RequiredArgsConstructor;
Expand All @@ -15,6 +16,7 @@ public class WebConfig implements WebMvcConfigurer {

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginUserResolver(jwtUtils));
resolvers.add(new NonLoginUserResolver(jwtUtils));
}
}
19 changes: 12 additions & 7 deletions src/main/java/com/sirius/spurt/common/jwt/AuthorizationFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import static com.sirius.spurt.common.jwt.JwtUtils.REFRESH_TOKEN_NAME;
import static com.sirius.spurt.common.jwt.JwtUtils.TOKEN_TYPE;
import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.POST;

import com.sirius.spurt.common.auth.PrincipalDetails;
import com.sirius.spurt.common.oauth.user.OAuthUser;
import com.sirius.spurt.common.validator.TokenValidator;
import com.sirius.spurt.common.validator.UserValidator;
import com.sirius.spurt.store.repository.database.entity.UserEntity;
Expand Down Expand Up @@ -36,7 +38,10 @@ public class AuthorizationFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;

private static final List<RequestMatcher> whiteList =
List.of(new AntPathRequestMatcher("/v1/question/random", GET.name()));
List.of(
new AntPathRequestMatcher("/oauth2/**", GET.name()),
new AntPathRequestMatcher("/v1/question/random", GET.name()),
new AntPathRequestMatcher("/v1/jobgroup", POST.name()));

@Override
protected void doFilterInternal(
Expand All @@ -56,16 +61,16 @@ protected void doFilterInternal(
TokenValidator.validateCookie(accessCookie);

String accessToken = accessCookie.replace(TOKEN_TYPE, "");
String userId = jwtUtils.getUserId(accessToken);
if (userId == null) {
OAuthUser oAuthUser = jwtUtils.getOAuthUser(accessToken);
if (oAuthUser == null) {
TokenValidator.validateCookie(refreshCookie);
String refreshToken = refreshCookie.replace(TOKEN_TYPE, "");
userId = jwtUtils.getUserId(refreshToken);
TokenValidator.validateUserId(userId);
jwtUtils.updateTokens(response, userId);
oAuthUser = jwtUtils.getOAuthUser(refreshToken);
TokenValidator.validateOAuthUser(oAuthUser);
jwtUtils.updateTokens(response, oAuthUser.getUserId(), oAuthUser.getEmail());
}

setAuthentication(userId);
setAuthentication(oAuthUser.getUserId());
filterChain.doFilter(request, response);
}

Expand Down
49 changes: 30 additions & 19 deletions src/main/java/com/sirius/spurt/common/jwt/JwtUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import com.sirius.spurt.common.exception.GlobalException;
import com.sirius.spurt.common.jwt.token.AccessToken;
import com.sirius.spurt.common.jwt.token.RefreshToken;
import com.sirius.spurt.common.oauth.user.OAuthUser;
import com.sirius.spurt.store.provider.auth.AuthProvider;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
Expand Down Expand Up @@ -43,40 +45,50 @@ public void init() {
this.key = Keys.hmacShaKeyFor(keyBytes);
}

private AccessToken getAccessToken(String userId) {
private AccessToken getAccessToken(String userId, String email) {
String accessToken =
Jwts.builder()
.setSubject("accessToken")
.claim("userId", userId)
.claim("email", email)
.setExpiration(new Date(getCurrentTimestamp() + ACCESS_TOKEN_EXPIRE_TIME))
.signWith(key, SignatureAlgorithm.HS256)
.compact();

return AccessToken.builder().token(accessToken).expireTime(ACCESS_TOKEN_EXPIRE_TIME).build();
}

private RefreshToken getRefreshToken(String userId) {
private RefreshToken getRefreshToken(String userId, String email) {
String refreshToken =
Jwts.builder()
.setSubject("refreshToken")
.claim("userId", userId)
.claim("email", email)
.setExpiration(new Date(getCurrentTimestamp() + REFRESH_TOKEN_EXPIRE_TIME))
.signWith(key, SignatureAlgorithm.HS256)
.compact();

return RefreshToken.builder().token(refreshToken).expireTime(REFRESH_TOKEN_EXPIRE_TIME).build();
}

public void setAccessToken(HttpServletResponse response, String userId) {
AccessToken accessToken = getAccessToken(userId);
setCookie(response, ACCESS_TOKEN_NAME, accessToken.getToken(), accessToken.getExpireTime());
public void setAccessToken(HttpServletResponse response, String userId, String email) {
AccessToken accessToken = getAccessToken(userId, email);
setCookie(
response,
ACCESS_TOKEN_NAME,
TOKEN_TYPE + accessToken.getToken(),
accessToken.getExpireTime());
}

public void setRefreshToken(HttpServletResponse response, String userId) {
RefreshToken refreshToken = getRefreshToken(userId);
public void setRefreshToken(HttpServletResponse response, String userId, String email) {
RefreshToken refreshToken = getRefreshToken(userId, email);
authProvider.setRefreshToken(
KEY_PREFIX + userId, refreshToken.getToken(), refreshToken.getExpireTime());
setCookie(response, REFRESH_TOKEN_NAME, refreshToken.getToken(), refreshToken.getExpireTime());
setCookie(
response,
REFRESH_TOKEN_NAME,
TOKEN_TYPE + refreshToken.getToken(),
refreshToken.getExpireTime());
}

private void setCookie(HttpServletResponse response, String key, String token, Long expireTime) {
Expand All @@ -95,27 +107,26 @@ private long getCurrentTimestamp() {
return System.currentTimeMillis();
}

public String getUserId(String token) {
public OAuthUser getOAuthUser(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody()
.get("userId")
.toString();
Claims claims =
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
return OAuthUser.builder()
.userId(claims.get("userId").toString())
.email(claims.get("email").toString())
.build();
} catch (Exception e) {
log.error(e.getMessage());
return null;
}
}

public void updateTokens(HttpServletResponse response, String userId) {
public void updateTokens(HttpServletResponse response, String userId, String email) {
if (!authProvider.hasRefreshToken(KEY_PREFIX + userId)) {
throw new GlobalException(AUTHENTICATION_FAILED);
}

setAccessToken(response, userId);
setRefreshToken(response, userId);
setAccessToken(response, userId, email);
setRefreshToken(response, userId, email);
}
}
5 changes: 5 additions & 0 deletions src/main/java/com/sirius/spurt/common/meta/Social.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.sirius.spurt.common.meta;

public enum Social {
GOOGLE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.sirius.spurt.common.oauth.handler;

import static com.sirius.spurt.common.meta.ResultCode.AUTHENTICATION_FAILED;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.sirius.spurt.service.controller.RestResponse;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationFailHandler implements AuthenticationFailureHandler {
private final ObjectMapper objectMapper;

@Override
public void onAuthenticationFailure(
HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
setErrorResponse(response, RestResponse.error(AUTHENTICATION_FAILED));
}

private void setErrorResponse(HttpServletResponse response, RestResponse<?> res)
throws IOException {
response.setCharacterEncoding(UTF_8.name());
response.setContentType(APPLICATION_JSON_VALUE);
response.setStatus(SC_OK);

try {
response.getWriter().write(objectMapper.writeValueAsString(res));
} catch (Exception e) {
log.error(e.getMessage());
} finally {
response.getWriter().close();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.sirius.spurt.common.oauth.handler;

import com.sirius.spurt.common.auth.PrincipalDetails;
import com.sirius.spurt.common.jwt.JwtUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;

@Slf4j
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtUtils jwtUtils;

private final String IS_COMMITTED = "Response has already committed.";

@Value("${redirect-uri.prod}")
private String redirectUri;

@Override
public void onAuthenticationSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
String targetUrl = determineTargetUrl(request, response, authentication);
if (response.isCommitted()) {
log.debug(IS_COMMITTED + targetUrl);
return;
}

getRedirectStrategy().sendRedirect(request, response, targetUrl);
}

@Override
protected String determineTargetUrl(
HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
jwtUtils.setAccessToken(
response,
principalDetails.getUserEntity().getUserId(),
principalDetails.getUserEntity().getEmail());
jwtUtils.setRefreshToken(
response,
principalDetails.getUserEntity().getUserId(),
principalDetails.getUserEntity().getEmail());
return UriComponentsBuilder.fromUriString(redirectUri).build().toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.sirius.spurt.common.oauth.info;

import java.util.Map;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public abstract class OAuth2UserInfo {
protected Map<String, Object> attributes;

public abstract String getUserId();

public abstract String getEmail();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.sirius.spurt.common.oauth.info;

import static com.sirius.spurt.common.meta.ResultCode.UNKNOWN_SOCIAL;
import static com.sirius.spurt.common.meta.Social.GOOGLE;

import com.sirius.spurt.common.exception.GlobalException;
import com.sirius.spurt.common.meta.Social;
import com.sirius.spurt.common.oauth.info.impl.GoogleOAuth2UserInfo;
import java.util.Map;

public class OAuth2UserInfoFactory {
public static OAuth2UserInfo getOAuth2UserInfo(Social social, Map<String, Object> attributes) {
if (GOOGLE.equals(social)) {
return new GoogleOAuth2UserInfo(attributes);
}

throw new GlobalException(UNKNOWN_SOCIAL);
}
}
Loading
Loading