Skip to content

Commit

Permalink
Merge pull request #71 from Nexters/feature/oauth
Browse files Browse the repository at this point in the history
OAuth 추가
  • Loading branch information
emost22 authored Jul 14, 2024
2 parents b3a5a2b + a48a180 commit 1cdc87d
Show file tree
Hide file tree
Showing 18 changed files with 456 additions and 54 deletions.
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

0 comments on commit 1cdc87d

Please sign in to comment.