Skip to content

Commit 1600e1c

Browse files
authored
1-1단계 - OAuth 2.0 Login, 1-2단계 - 리팩터링 & OAuth 2.0 Resource 연동 리뷰 요청드립니다. (#13)
* feat: GithubLoginRedirectFilter 추가 * feat: GithubAuthenticationFilter 추가 * feat: 새로운 멤버인 경우 회원가입 로직 추가 * feat: 두 AuthentiactionFilter를 OAuth2AuthenticationFilter로 통합 * feat: 두 LoginRedirectFilter를 OAuth2LoginRedirectFilter로 통합 * feat: OAuth2AuthenticationFilter 로직 메서드로 추출 * feat: 클라이언트 관련 값 삭제 * fix: 불필요 주석 제거 * fix: 프로퍼티 값을 Map으로 받도록 수정 * fix: 네이밍 변경 (OAuth2Client -> ClientRegistration) * fix: Factory 클래스를 OAuth2ClientRepository 클래스로 변경 * OAuth2ClientRepository 클래스는 클라이언트 정보만 가져오도록 수정 * RequestMatcher는 필터에서 사용하도록 수정 * fix: OAuth2AuthenticationProvider에서 회원가입 로직 제거 * fix: CLIENT_ID 수정 * fix: 회원가입 로직 제외 * fix: 불필요 주석 제거
1 parent ff089ca commit 1600e1c

16 files changed

+552
-28
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package nextstep.app;
2+
3+
import nextstep.security.authentication.ClientRegistration;
4+
import org.springframework.boot.context.properties.ConfigurationProperties;
5+
import org.springframework.stereotype.Component;
6+
7+
import java.util.Map;
8+
9+
@Component
10+
@ConfigurationProperties(prefix = "oauth2")
11+
public class OAuth2Property {
12+
13+
private Map<String, ClientRegistration> clients;
14+
15+
public Map<String, ClientRegistration> getClients() {
16+
return clients;
17+
}
18+
19+
public void setClients(Map<String, ClientRegistration> clients) {
20+
this.clients = clients;
21+
}
22+
23+
public ClientRegistration getClient(String key) {
24+
return clients.get(key);
25+
}
26+
}

src/main/java/nextstep/app/SecurityConfig.java

Lines changed: 18 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
package nextstep.app;
22

3-
import nextstep.app.domain.Member;
43
import nextstep.app.domain.MemberRepository;
4+
import nextstep.app.domain.MemberService;
55
import nextstep.security.access.AnyRequestMatcher;
66
import nextstep.security.access.MvcRequestMatcher;
77
import nextstep.security.access.RequestMatcherEntry;
88
import nextstep.security.access.hierarchicalroles.RoleHierarchy;
99
import nextstep.security.access.hierarchicalroles.RoleHierarchyImpl;
10-
import nextstep.security.authentication.AuthenticationException;
11-
import nextstep.security.authentication.BasicAuthenticationFilter;
12-
import nextstep.security.authentication.UsernamePasswordAuthenticationFilter;
10+
import nextstep.security.authentication.*;
1311
import nextstep.security.authorization.*;
1412
import nextstep.security.config.DefaultSecurityFilterChain;
1513
import nextstep.security.config.DelegatingFilterProxy;
1614
import nextstep.security.config.FilterChainProxy;
1715
import nextstep.security.config.SecurityFilterChain;
1816
import nextstep.security.context.SecurityContextHolderFilter;
19-
import nextstep.security.userdetails.UserDetails;
2017
import nextstep.security.userdetails.UserDetailsService;
2118
import org.springframework.context.annotation.Bean;
2219
import org.springframework.context.annotation.Configuration;
@@ -25,16 +22,18 @@
2522

2623
import java.util.ArrayList;
2724
import java.util.List;
28-
import java.util.Set;
25+
import java.util.Map;
2926

3027
@EnableAspectJAutoProxy
3128
@Configuration
3229
public class SecurityConfig {
3330

3431
private final MemberRepository memberRepository;
32+
private final OAuth2Property oAuth2Property;
3533

36-
public SecurityConfig(MemberRepository memberRepository) {
34+
public SecurityConfig(MemberRepository memberRepository, OAuth2Property oAuth2Property) {
3735
this.memberRepository = memberRepository;
36+
this.oAuth2Property = oAuth2Property;
3837
}
3938

4039
@Bean
@@ -57,6 +56,8 @@ public SecurityFilterChain securityFilterChain() {
5756
return new DefaultSecurityFilterChain(
5857
List.of(
5958
new SecurityContextHolderFilter(),
59+
new OAuth2LoginRedirectFilter(oAuth2ClientRepository()),
60+
new OAuth2AuthenticationFilter(userDetailsService(), oAuth2ClientRepository()),
6061
new UsernamePasswordAuthenticationFilter(userDetailsService()),
6162
new BasicAuthenticationFilter(userDetailsService()),
6263
new AuthorizationFilter(requestAuthorizationManager())
@@ -82,27 +83,16 @@ public RequestAuthorizationManager requestAuthorizationManager() {
8283
}
8384

8485
@Bean
85-
public UserDetailsService userDetailsService() {
86-
return username -> {
87-
Member member = memberRepository.findByEmail(username)
88-
.orElseThrow(() -> new AuthenticationException("존재하지 않는 사용자입니다."));
89-
90-
return new UserDetails() {
91-
@Override
92-
public String getUsername() {
93-
return member.getEmail();
94-
}
95-
96-
@Override
97-
public String getPassword() {
98-
return member.getPassword();
99-
}
86+
public OAuth2ClientRepository oAuth2ClientRepository() {
87+
return new OAuth2ClientRepository(
88+
Map.of(
89+
"google", oAuth2Property.getClient("google"),
90+
"github", oAuth2Property.getClient("github")
91+
));
92+
}
10093

101-
@Override
102-
public Set<String> getAuthorities() {
103-
return member.getRoles();
104-
}
105-
};
106-
};
94+
@Bean
95+
public UserDetailsService userDetailsService() {
96+
return new MemberService(memberRepository);
10797
}
10898
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package nextstep.app.domain;
2+
3+
import nextstep.security.authentication.AuthenticationException;
4+
import nextstep.security.userdetails.UserDetails;
5+
import nextstep.security.userdetails.UserDetailsService;
6+
7+
import java.util.Set;
8+
9+
public class MemberService implements UserDetailsService {
10+
11+
private final MemberRepository memberRepository;
12+
13+
public MemberService(MemberRepository memberRepository) {
14+
this.memberRepository = memberRepository;
15+
}
16+
17+
@Override
18+
public UserDetails loadUserByUsername(String username) {
19+
Member member = memberRepository.findByEmail(username)
20+
.orElseThrow(() -> new AuthenticationException("존재하지 않는 사용자입니다."));
21+
22+
return new UserDetails() {
23+
@Override
24+
public String getUsername() {
25+
return member.getEmail();
26+
}
27+
28+
@Override
29+
public String getPassword() {
30+
return member.getPassword();
31+
}
32+
33+
@Override
34+
public Set<String> getAuthorities() {
35+
return member.getRoles();
36+
}
37+
};
38+
}
39+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package nextstep.security.access;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import org.springframework.http.HttpMethod;
5+
6+
import java.util.regex.Matcher;
7+
import java.util.regex.Pattern;
8+
9+
public class RegexRequestMatcher implements RequestMatcher {
10+
11+
private HttpMethod method;
12+
private final Pattern pattern;
13+
14+
public RegexRequestMatcher(HttpMethod method, String pattern) {
15+
this.method = method;
16+
this.pattern = Pattern.compile(pattern);
17+
}
18+
19+
@Override
20+
public boolean matches(HttpServletRequest request) {
21+
if (this.method != null && !this.method.name().equals(request.getMethod())) {
22+
return false;
23+
}
24+
25+
Matcher matcher = pattern.matcher(request.getRequestURI());
26+
return matcher.matches();
27+
}
28+
29+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package nextstep.security.authentication;
2+
3+
public class AccessTokenResponseDTO {
4+
private String access_token;
5+
private String scope;
6+
private String token_type;
7+
8+
public String getAccess_token() {
9+
return access_token;
10+
}
11+
12+
public String getScope() {
13+
return scope;
14+
}
15+
16+
public String getToken_type() {
17+
return token_type;
18+
}
19+
20+
public String getToken() {
21+
return token_type + " " + access_token;
22+
}
23+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package nextstep.security.authentication;
2+
3+
import org.springframework.util.LinkedMultiValueMap;
4+
import org.springframework.util.MultiValueMap;
5+
6+
public class ClientRegistration {
7+
8+
private String clientId;
9+
private String clientSecret;
10+
private String tokenUri;
11+
private String userInfoUri;
12+
private String grantType;
13+
private String redirectUri;
14+
private String loginRedirectUri;
15+
16+
public String getUserInfoUri() {
17+
return userInfoUri;
18+
}
19+
20+
public void setUserInfoUri(String userInfoUri) {
21+
this.userInfoUri = userInfoUri;
22+
}
23+
24+
public String getTokenUri() {
25+
return tokenUri;
26+
}
27+
28+
public void setTokenUri(String tokenUri) {
29+
this.tokenUri = tokenUri;
30+
}
31+
32+
public String getClientSecret() {
33+
return clientSecret;
34+
}
35+
36+
public void setClientSecret(String clientSecret) {
37+
this.clientSecret = clientSecret;
38+
}
39+
40+
public String getClientId() {
41+
return clientId;
42+
}
43+
44+
public void setClientId(String clientId) {
45+
this.clientId = clientId;
46+
}
47+
48+
public String getRedirectUri() {
49+
return redirectUri;
50+
}
51+
52+
public void setRedirectUri(String redirectUri) {
53+
this.redirectUri = redirectUri;
54+
}
55+
56+
public String getGrantType() {
57+
return grantType;
58+
}
59+
60+
public void setGrantType(String grantType) {
61+
this.grantType = grantType;
62+
}
63+
64+
public String getLoginRedirectUri() {
65+
return loginRedirectUri;
66+
}
67+
68+
public void setLoginRedirectUri(String loginRedirectUri) {
69+
this.loginRedirectUri = loginRedirectUri;
70+
}
71+
72+
public MultiValueMap<String, String> getParamsForToken(String code) {
73+
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
74+
params.add("code", code);
75+
params.add("client_id", clientId);
76+
params.add("client_secret", clientSecret);
77+
params.add("grant_type", grantType);
78+
params.add("redirect_uri", redirectUri);
79+
return params;
80+
}
81+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package nextstep.security.authentication;
2+
3+
import jakarta.servlet.FilterChain;
4+
import jakarta.servlet.ServletException;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpServletResponse;
7+
import nextstep.security.access.RegexRequestMatcher;
8+
import nextstep.security.context.HttpSessionSecurityContextRepository;
9+
import nextstep.security.context.SecurityContext;
10+
import nextstep.security.context.SecurityContextHolder;
11+
import nextstep.security.userdetails.UserDetailsService;
12+
import org.springframework.http.HttpEntity;
13+
import org.springframework.http.HttpHeaders;
14+
import org.springframework.http.HttpMethod;
15+
import org.springframework.http.ResponseEntity;
16+
import org.springframework.util.MultiValueMap;
17+
import org.springframework.web.client.RestTemplate;
18+
import org.springframework.web.filter.OncePerRequestFilter;
19+
20+
import java.io.IOException;
21+
import java.util.List;
22+
23+
public class OAuth2AuthenticationFilter extends OncePerRequestFilter {
24+
25+
private final RestTemplate restTemplate = new RestTemplate();
26+
private static final RegexRequestMatcher OAUTH2_AUTHENTICATION_REQUEST_MATCHER = new RegexRequestMatcher(HttpMethod.GET, "/login/oauth2/code/.*");
27+
private final HttpSessionSecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
28+
private final AuthenticationManager authenticationManager;
29+
private final OAuth2ClientRepository oAuth2ClientRepository;
30+
31+
public OAuth2AuthenticationFilter(UserDetailsService userDetailsService, OAuth2ClientRepository oAuth2ClientRepository) {
32+
this.authenticationManager = new ProviderManager(
33+
List.of(new OAuth2AuthenticationProvider(userDetailsService)));
34+
this.oAuth2ClientRepository = oAuth2ClientRepository;
35+
}
36+
37+
@Override
38+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
39+
40+
if (!OAUTH2_AUTHENTICATION_REQUEST_MATCHER.matches(request)) {
41+
filterChain.doFilter(request, response);
42+
return;
43+
}
44+
45+
String clientRegistrationId = extractRegistrationId(request);
46+
ClientRegistration clientRegistration = oAuth2ClientRepository.findByRegistrationId(clientRegistrationId);
47+
if (clientRegistration == null) {
48+
throw new AuthenticationException();
49+
}
50+
51+
String code = request.getParameter("code");
52+
String token = getAccessToken(clientRegistration, code);
53+
UserProfile userProfile = getUserProfile(token, clientRegistration);
54+
55+
UsernamePasswordAuthenticationToken unauthenticated = UsernamePasswordAuthenticationToken.unauthenticated(userProfile.getEmail(), null);
56+
Authentication authenticated = authenticationManager.authenticate(unauthenticated);
57+
saveSecurityContext(request, response, authenticated);
58+
59+
response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
60+
response.sendRedirect("/");
61+
}
62+
63+
private static String extractRegistrationId(HttpServletRequest request) {
64+
return request.getRequestURI().substring("/login/oauth2/code/".length());
65+
}
66+
67+
private String getAccessToken(ClientRegistration clientRegistration, String code) {
68+
String tokenUri = clientRegistration.getTokenUri();
69+
MultiValueMap<String, String> paramsForToken = clientRegistration.getParamsForToken(code);
70+
AccessTokenResponseDTO tokenResponse = restTemplate.postForObject(tokenUri, paramsForToken, AccessTokenResponseDTO.class);
71+
return tokenResponse.getToken();
72+
}
73+
74+
private UserProfile getUserProfile(String token, ClientRegistration clientRegistration) {
75+
HttpHeaders headers = new HttpHeaders();
76+
headers.set("Authorization", token);
77+
HttpEntity<Object> httpEntity = new HttpEntity<>(headers);
78+
String userInfoUri = clientRegistration.getUserInfoUri();
79+
ResponseEntity<UserProfile> userProfileDTOResponseEntity = restTemplate.exchange(userInfoUri, HttpMethod.GET, httpEntity, UserProfile.class);
80+
return userProfileDTOResponseEntity.getBody();
81+
}
82+
83+
private void saveSecurityContext(HttpServletRequest request, HttpServletResponse response, Authentication authenticated) {
84+
SecurityContext context = SecurityContextHolder.createEmptyContext();
85+
context.setAuthentication(authenticated);
86+
SecurityContextHolder.setContext(context);
87+
securityContextRepository.saveContext(context, request, response);
88+
}
89+
}

0 commit comments

Comments
 (0)