Skip to content

Commit 6c78f0a

Browse files
authoredNov 16, 2023
bug: OAuth2SuccessHandler에서 admin의 요청만을 처리하도록 수정 (#256) (#257)
* Revert "refactor: 인증 로직 변경 (#252) (#253)" This reverts commit d78d5f7. * refactor: OAuth2SuccessHandler에서 admin의 요청만을 처리하도록 수정 (#256) - cookie를 이용해 JWT 토큰 전달 * docs: admin과 관련된 ErrorCode 추가 (#256) * style: 줄바꿈 추가 (#256) * feat: 일반 로그인 api 추가 (#256) - controller, service, dto 추가 * feat: RoleType에 ADMIN 추가 (#256) * feat: DTO의 필드로 사용되는 Enum 추가 (#256) * feat: 토큰 필터에 admin에 대한 처리 추가 (#256) * feat: 토큰 서비스에 admin에 대한 처리 추가 (#256)
1 parent 8a952fe commit 6c78f0a

15 files changed

+288
-12
lines changed
 

‎build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ dependencies {
2929

3030
// security
3131
implementation 'org.springframework.boot:spring-boot-starter-security'
32+
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
3233
implementation 'io.jsonwebtoken:jjwt:0.9.1'
3334
implementation 'javax.xml.bind:jaxb-api:2.3.1'
3435

‎src/main/java/com/newfit/reservation/common/auth/config/WebSecurityConfig.java

+18
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
import com.fasterxml.jackson.databind.ObjectMapper;
44
import com.newfit.reservation.common.auth.jwt.TokenAuthenticationFilter;
55
import com.newfit.reservation.common.auth.jwt.TokenProvider;
6+
import com.newfit.reservation.common.auth.oauth.OAuth2AuthorizationRequestCookieRepository;
7+
import com.newfit.reservation.common.auth.oauth.OAuth2UserCustomService;
8+
import com.newfit.reservation.common.auth.oauth.handler.OAuth2FailureHandler;
9+
import com.newfit.reservation.common.auth.oauth.handler.OAuth2SuccessHandler;
610
import com.newfit.reservation.common.exception.CustomExceptionHandlingFilter;
711
import com.newfit.reservation.domains.auth.repository.RefreshTokenRepository;
812
import com.newfit.reservation.domains.user.repository.UserRepository;
@@ -30,6 +34,9 @@ public class WebSecurityConfig {
3034
private final TokenProvider tokenProvider;
3135
private final UserRepository userRepository;
3236
private final RefreshTokenRepository refreshTokenRepository;
37+
private final OAuth2UserCustomService oAuth2UserCustomService;
38+
private final OAuth2SuccessHandler oAuth2SuccessHandler;
39+
private final OAuth2FailureHandler oAuth2FailureHandler;
3340
private final ObjectMapper objectMapper;
3441

3542
// 누구나 접근할 수 있는 URI 패턴을 정의
@@ -60,6 +67,12 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
6067
.anyRequest().permitAll())
6168
.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
6269
.addFilterBefore(customExceptionHandlingFilter(), TokenAuthenticationFilter.class)
70+
.oauth2Login(oauth -> oauth
71+
.userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint.userService(oAuth2UserCustomService))
72+
.successHandler(oAuth2SuccessHandler)
73+
.failureHandler(oAuth2FailureHandler)
74+
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint
75+
.authorizationRequestRepository(oAuth2AuthorizationRequestCookieRepository())))
6376
.logout(logout -> logout
6477
.logoutUrl("/logout")
6578
.addLogoutHandler((request, response, authentication) -> {
@@ -74,6 +87,11 @@ public TokenAuthenticationFilter tokenAuthenticationFilter() {
7487
return new TokenAuthenticationFilter(tokenProvider, refreshTokenRepository, userRepository);
7588
}
7689

90+
@Bean
91+
public OAuth2AuthorizationRequestCookieRepository oAuth2AuthorizationRequestCookieRepository() {
92+
return new OAuth2AuthorizationRequestCookieRepository();
93+
}
94+
7795
@Bean
7896
public CustomExceptionHandlingFilter customExceptionHandlingFilter() {
7997
return new CustomExceptionHandlingFilter(objectMapper);

‎src/main/java/com/newfit/reservation/common/auth/jwt/TokenAuthenticationFilter.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
import com.newfit.reservation.domains.user.repository.UserRepository;
88
import jakarta.servlet.FilterChain;
99
import jakarta.servlet.ServletException;
10+
import jakarta.servlet.http.Cookie;
1011
import jakarta.servlet.http.HttpServletRequest;
1112
import jakarta.servlet.http.HttpServletResponse;
1213
import lombok.RequiredArgsConstructor;
1314
import org.springframework.security.core.Authentication;
1415
import org.springframework.security.core.context.SecurityContextHolder;
1516
import org.springframework.web.filter.OncePerRequestFilter;
1617
import java.io.IOException;
18+
import java.util.Arrays;
1719

1820
import static com.newfit.reservation.common.exception.ErrorCodeType.*;
1921

@@ -33,7 +35,14 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
3335
@Override
3436
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
3537
String token = getToken(request);
36-
if (request.getRequestURI().equals("/login") || token == null) {
38+
if (request.getRequestURI().contains("/v1/admin")) {
39+
Cookie adminCookie = Arrays.stream(request.getCookies()).filter(cookie -> cookie.getName().equals("admin-token")).findAny()
40+
.orElseThrow(() -> new CustomException(ADMIN_COOKIE_NOT_FOUND));
41+
tokenProvider.validAdminToken(adminCookie.getValue(), response);
42+
Authentication authentication = tokenProvider.getAdminAuthentication();
43+
SecurityContextHolder.getContext().setAuthentication(authentication);
44+
filterChain.doFilter(request, response);
45+
} else if (request.getRequestURI().equals("/login") || token == null) {
3746
filterChain.doFilter(request, response);
3847
} else if (request.getRequestURI().equals("/refresh")) {
3948
tokenProvider.validToken(token);

‎src/main/java/com/newfit/reservation/common/auth/jwt/TokenProvider.java

+34-4
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@
88
import com.newfit.reservation.domains.authority.repository.AuthorityRepository;
99
import com.newfit.reservation.domains.user.domain.User;
1010
import com.newfit.reservation.domains.user.repository.UserRepository;
11-
import io.jsonwebtoken.Claims;
12-
import io.jsonwebtoken.Header;
13-
import io.jsonwebtoken.Jwts;
14-
import io.jsonwebtoken.SignatureAlgorithm;
11+
import io.jsonwebtoken.*;
1512
import jakarta.servlet.http.HttpServletRequest;
1613
import jakarta.servlet.http.HttpServletResponse;
1714
import lombok.RequiredArgsConstructor;
@@ -20,6 +17,7 @@
2017
import org.springframework.security.core.Authentication;
2118
import org.springframework.security.core.authority.SimpleGrantedAuthority;
2219
import org.springframework.stereotype.Service;
20+
import java.io.IOException;
2321
import java.time.Duration;
2422
import java.util.*;
2523

@@ -32,6 +30,7 @@ public class TokenProvider { // JWT의 생성 및 검증 로직 담당 클래
3230
private final static Duration ACCESS_TOKEN_DURATION = Duration.ofMinutes(30);
3331
private final static Duration ACCESS_TEMPORARY_TOKEN_DURATION = Duration.ofMinutes(10);
3432
private final static Duration REFRESH_TOKEN_DURATION = Duration.ofDays(7);
33+
private final static Duration ADMIN_TOKEN_DURATION = Duration.ofMinutes(10);
3534

3635
private final JwtProperties jwtProperties;
3736
private final AuthorityRepository authorityRepository;
@@ -88,6 +87,19 @@ public String generateRefreshToken(User user) {
8887
.getToken();
8988
}
9089

90+
public String generateAdminToken() {
91+
Date now = new Date();
92+
Date expiryAt = new Date(now.getTime() + ADMIN_TOKEN_DURATION.toMillis());
93+
return Jwts.builder()
94+
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
95+
.setIssuer(jwtProperties.getIssuer())
96+
.setIssuedAt(now)
97+
.setExpiration(expiryAt)
98+
.setSubject("admin")
99+
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
100+
.compact();
101+
}
102+
91103
public void validAccessToken(String token, HttpServletRequest request, HttpServletResponse response) {
92104
validToken(token);
93105
try {
@@ -97,6 +109,18 @@ public void validAccessToken(String token, HttpServletRequest request, HttpServl
97109
}
98110
}
99111

112+
public void validAdminToken(String token, HttpServletResponse response) throws IOException {
113+
try {
114+
validToken(token);
115+
Claims claims = getClaims(token);
116+
if (!claims.getSubject().equals("admin")) {
117+
throw new CustomException(ADMIN_UNAUTHORIZED_REQUEST);
118+
}
119+
} catch (ExpiredJwtException exception) {
120+
response.sendRedirect("/login");
121+
}
122+
}
123+
100124
private void checkExceptionAndProceed(HttpServletResponse response, CustomException exception, Long userId){
101125
if (!exception.getErrorCodeType().equals(AUTHORITY_ID_LIST_OUTDATED)) {
102126
throw exception;
@@ -173,6 +197,12 @@ public Authentication getAnonymousAuthentication(String token) {
173197
}
174198
}
175199

200+
public Authentication getAdminAuthentication() {
201+
Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority(RoleType.ADMIN.getDescription()));
202+
203+
return new UsernamePasswordAuthenticationToken(new org.springframework.security.core.userdetails.User("admin", "", authorities), null, authorities);
204+
}
205+
176206
private List<Integer> getAuthorityIdList(String token) {
177207
Claims claims = getClaims(token);
178208
return claims.get("authorityIdList", List.class);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.newfit.reservation.common.auth.oauth;
2+
3+
import com.newfit.reservation.domains.auth.domain.OAuthHistory;
4+
import lombok.Getter;
5+
import org.springframework.security.core.GrantedAuthority;
6+
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
7+
8+
import java.util.Collection;
9+
import java.util.Map;
10+
11+
@Getter
12+
public class CustomOAuth2User extends DefaultOAuth2User { // OAuth2 인증이 완료된 사용자 정보를 담는 클래스
13+
private OAuthHistory oAuthHistory;
14+
15+
public CustomOAuth2User(Collection<? extends GrantedAuthority> authorities, Map<String, Object> attributes, String nameAttributeKey, OAuthHistory oAuthHistory) {
16+
super(authorities, attributes, nameAttributeKey);
17+
this.oAuthHistory = oAuthHistory;
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.newfit.reservation.common.auth.oauth;
2+
3+
import com.newfit.reservation.common.auth.util.CookieUtil;
4+
import jakarta.servlet.http.Cookie;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpServletResponse;
7+
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
8+
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
9+
import org.springframework.web.util.WebUtils;
10+
11+
import java.io.IOException;
12+
13+
public class OAuth2AuthorizationRequestCookieRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
14+
private final static String OAUTH2_AUTHORIZATION_REQUEST = "oauth2_authorization_request";
15+
private final static int COOKIE_EXPIRY_SECONDS= 18000;
16+
17+
@Override
18+
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
19+
try {
20+
Cookie cookie = WebUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST);
21+
return CookieUtil.deserialize(cookie, OAuth2AuthorizationRequest.class);
22+
} catch (IOException exception) {
23+
return null;
24+
}
25+
}
26+
27+
@Override
28+
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
29+
if (authorizationRequest == null) {
30+
removeAuthorizationRequestCookies(request, response);
31+
return;
32+
}
33+
CookieUtil.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST, CookieUtil.serialize(authorizationRequest), COOKIE_EXPIRY_SECONDS);
34+
}
35+
36+
@Override
37+
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) {
38+
return this.loadAuthorizationRequest(request);
39+
}
40+
41+
public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) {
42+
CookieUtil.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST);
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.newfit.reservation.common.auth.oauth;
2+
3+
import com.newfit.reservation.domains.auth.domain.OAuthHistory;
4+
import com.newfit.reservation.domains.auth.domain.ProviderType;
5+
import com.newfit.reservation.domains.auth.repository.OAuthHistoryRepository;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
8+
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
9+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
10+
import org.springframework.security.oauth2.core.user.OAuth2User;
11+
import org.springframework.stereotype.Service;
12+
import java.util.Map;
13+
import java.util.Optional;
14+
15+
@Service
16+
@RequiredArgsConstructor
17+
public class OAuth2UserCustomService extends DefaultOAuth2UserService {
18+
private final OAuthHistoryRepository oAuthHistoryRepository;
19+
20+
@Override
21+
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
22+
ProviderType providerType = ProviderType.getProviderType(userRequest.getClientRegistration().getRegistrationId());
23+
OAuth2User oAuth2User = super.loadUser(userRequest);
24+
Map<String, Object> attributes = oAuth2User.getAttributes();
25+
String nameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
26+
27+
// OAuth2 인증을 통해 얻어온 사용자 정보로 OAuthHistory 객체 생성
28+
OAuthHistory oAuthHistory = findOAuthHistory(providerType, String.valueOf(attributes.get(nameAttributeName)));
29+
return new CustomOAuth2User(null, attributes, nameAttributeName, oAuthHistory);
30+
}
31+
32+
private OAuthHistory findOAuthHistory(ProviderType providerType, String attributeName) {
33+
Optional<OAuthHistory> oAuthHistory = oAuthHistoryRepository.findByProviderTypeAndAttributeName(providerType, attributeName);
34+
return oAuthHistory.orElseGet(() -> oAuthHistoryRepository.save(OAuthHistory.createOAuthHistory(providerType, attributeName)));
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.newfit.reservation.common.auth.oauth.handler;
2+
3+
import jakarta.servlet.ServletException;
4+
import jakarta.servlet.http.HttpServletRequest;
5+
import jakarta.servlet.http.HttpServletResponse;
6+
import org.springframework.security.core.AuthenticationException;
7+
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
8+
import org.springframework.stereotype.Component;
9+
10+
import java.io.IOException;
11+
12+
@Component
13+
public class OAuth2FailureHandler implements AuthenticationFailureHandler {
14+
@Override
15+
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
16+
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
17+
response.sendRedirect("/login");
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.newfit.reservation.common.auth.oauth.handler;
2+
3+
import com.newfit.reservation.common.auth.jwt.TokenProvider;
4+
import com.newfit.reservation.common.auth.oauth.CustomOAuth2User;
5+
import com.newfit.reservation.domains.auth.domain.OAuthHistory;
6+
import jakarta.servlet.http.Cookie;
7+
import jakarta.servlet.http.HttpServletRequest;
8+
import jakarta.servlet.http.HttpServletResponse;
9+
import lombok.RequiredArgsConstructor;
10+
import lombok.extern.slf4j.Slf4j;
11+
import org.springframework.beans.factory.annotation.Value;
12+
import org.springframework.security.core.Authentication;
13+
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
14+
import org.springframework.stereotype.Component;
15+
import java.io.IOException;
16+
17+
@Slf4j
18+
@Component
19+
@RequiredArgsConstructor
20+
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
21+
22+
private final TokenProvider tokenProvider;
23+
24+
@Value("${admin.attributeName}")
25+
private String attributeName;
26+
27+
@Override
28+
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
29+
CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();
30+
OAuthHistory oAuthHistory = oAuth2User.getOAuthHistory();
31+
32+
if (oAuthHistory.getAttributeName().equals(attributeName)) {
33+
Cookie cookie = new Cookie("admin-token", tokenProvider.generateAdminToken());
34+
cookie.setPath("/v1/admin");
35+
response.addCookie(cookie);
36+
response.sendRedirect("/v1/admin");
37+
}
38+
else {
39+
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
40+
response.sendRedirect("/login");
41+
}
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.newfit.reservation.common.auth.util;
2+
3+
import jakarta.servlet.http.Cookie;
4+
import jakarta.servlet.http.HttpServletRequest;
5+
import jakarta.servlet.http.HttpServletResponse;
6+
import org.springframework.util.SerializationUtils;
7+
8+
import java.io.*;
9+
import java.util.Base64;
10+
11+
public class CookieUtil { // Cookie 관련 작업 담당 클래스
12+
13+
public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
14+
Cookie cookie = new Cookie(name, value);
15+
cookie.setPath("/");
16+
cookie.setMaxAge(maxAge);
17+
18+
response.addCookie(cookie);
19+
}
20+
21+
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) {
22+
Cookie[] cookies = request.getCookies();
23+
24+
if (cookies == null) {
25+
return;
26+
}
27+
28+
for (Cookie cookie : cookies) {
29+
if (name.equals(cookie.getName())) {
30+
cookie.setValue("");
31+
cookie.setPath("/");
32+
cookie.setMaxAge(0);
33+
response.addCookie(cookie);
34+
}
35+
}
36+
}
37+
38+
public static String serialize(Object obj) {
39+
return Base64.getUrlEncoder()
40+
.encodeToString(SerializationUtils.serialize(obj));
41+
}
42+
43+
public static <T> T deserialize(Cookie cookie, Class<T> cls) throws IOException {
44+
byte[] serializedAuthRequest = Base64.getUrlDecoder().decode(cookie.getValue());
45+
try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedAuthRequest)) {
46+
try (ObjectInputStream ois = new ObjectInputStream(bais)) {
47+
Object objectAuthRequest = ois.readObject();
48+
T cast = cls.cast(objectAuthRequest);
49+
return cast;
50+
} catch (ClassNotFoundException e) {
51+
throw new RuntimeException(e);
52+
}
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)
Please sign in to comment.