Skip to content

Commit f6e7d4d

Browse files
sazzeoim.joy
andauthored
[임지영] 1단계 OAuth 2.0 Login, 리팩터링 & OAuth 2.0 Resource 연동 (#12)
* feat: 1-1 미션 페어 프로그래밍 * feat: antMatcher 클래스 추가 * feat: OAuth2RedirectFilter 추가 * feat: Oauth2 프로퍼티용 객체 추가 * refactor: 클래스 이름 변경 * feat: OAuth2RegistrationRepository 생성 * feat: EnableConfigurationProperties 설정 추가, test 이름 변경 * feat: application-test.yml 작성 후 OAuth2PropertiesRedirectFilterTest 테스트 통과 * feat: redirectFilter에 벤더사 설정이 없으면 404 반환한다 * feat: google oauth2 테스트 코드 추가 * feat: include 에 oauth2 추가 * feat: AntRequestMatcher 검증에 AntPathMatcher 사용하도록 변경 * feat: Registration 프로퍼티에 token-uri 추가 * feat: matchRedirectUrl redirectUrl 판단하도록 메소드 추가 * feat: /login/oauth/access_token WireMock 테스트 완료 * feat: OAuth2Parameter 클래스 추가 * feat: accessToken 가져오는 부분 클래스로 변경 * feat: OAuth2 용 UserDetailsService 추가 * feat: vendor 사 반환 코드 추가 * feat: vendor 사 List로 받을 수 있도록 변경 * feat: OAuth2UserDetailsAuthenticationToken 추가 * feat: oauth2UserDetailsServiceResolver 가 user 정보 가져오도록 변경 * feat: yml 파일 속성 추가 * feat: config 수정 * refactor: test 클래스명 변경 * feat: 인증 성공 처리용 OAuth2AuthenticationSuccessHandler 추가 * feat: OAuth2UserDetail 에 getAuthorities 추가 * feat: MemberService 추가 * feat: OAuth2AuthenticationSuccessHandlerImpl에 member 저장하는 코드 추가 * fix: OAuth2 인증후 로그인 유저처럼 동작하는지 확인하는 테스트 코드로 수정 * feat: google 용 OAuth2 서비스 추가 * feat: google 용 설정 추가 * fix: google 용 stub 객체 분리, 깨지는 테스트 수정 * refactor: @value 주입 생성자로 받도록 수정 * fix: 필요없는 코드 제거 및 수정 * feat: response-type 제거, OAuth2Parameter에 default 값 추가 * fix: OAuth2AccessTokenRequestProvider 인터페이스 삭제 후 구현체만 남김 * feat: UrlUtils 클래스 추가 * feat: ClientRegistrationRepository 인터페이스로 변경 * feat: getRedirectUrl 가져오는 부분 기본 url 쓰도록 수정 * feat: AbstractOAuth2UserDetailService 추가해 템플릿 메소드 패턴으로 중복 제거 * refactor: 안쓰는 import문 정리 --------- Co-authored-by: im.joy <[email protected]>
1 parent ff089ca commit f6e7d4d

34 files changed

+1138
-16
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,5 @@ out/
3535

3636
### VS Code ###
3737
.vscode/
38+
39+
application-*.yml
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package nextstep.app;
2+
3+
import jakarta.annotation.Nullable;
4+
import nextstep.security.properties.ClientRegistrationRepository;
5+
import nextstep.security.properties.OAuth2Properties;
6+
import nextstep.security.properties.Registration;
7+
import org.springframework.stereotype.Component;
8+
9+
@Component
10+
public class InMemoryClientRegistrationRepository implements ClientRegistrationRepository {
11+
12+
private final OAuth2Properties oAuth2Properties;
13+
14+
public InMemoryClientRegistrationRepository(final OAuth2Properties oAuth2Properties) {
15+
this.oAuth2Properties = oAuth2Properties;
16+
}
17+
18+
@Override
19+
@Nullable
20+
public Registration findRegistrationById(final String id) {
21+
return oAuth2Properties.getRegistration(id);
22+
}
23+
24+
}

src/main/java/nextstep/app/SecurityApplication.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package nextstep.app;
22

3+
import nextstep.security.properties.OAuth2Properties;
34
import org.springframework.boot.SpringApplication;
45
import org.springframework.boot.autoconfigure.SpringBootApplication;
6+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
7+
58

69
@SpringBootApplication
10+
@EnableConfigurationProperties(OAuth2Properties.class)
711
public class SecurityApplication {
812

913
public static void main(String[] args) {

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

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package nextstep.app;
22

3+
import nextstep.app.application.MemberService;
34
import nextstep.app.domain.Member;
45
import nextstep.app.domain.MemberRepository;
6+
import nextstep.app.oauth2.OAuth2AuthenticationSuccessHandlerImpl;
57
import nextstep.security.access.AnyRequestMatcher;
68
import nextstep.security.access.MvcRequestMatcher;
79
import nextstep.security.access.RequestMatcherEntry;
@@ -15,7 +17,13 @@
1517
import nextstep.security.config.DelegatingFilterProxy;
1618
import nextstep.security.config.FilterChainProxy;
1719
import nextstep.security.config.SecurityFilterChain;
20+
import nextstep.security.context.HttpSessionSecurityContextRepository;
1821
import nextstep.security.context.SecurityContextHolderFilter;
22+
import nextstep.security.oauth2.OAuth2AuthenticationFilter;
23+
import nextstep.security.oauth2.OAuth2AuthenticationSuccessHandler;
24+
import nextstep.security.oauth2.OAuth2RedirectFilter;
25+
import nextstep.security.oauth2.userdetails.OAuth2UserDetailsService;
26+
import nextstep.security.properties.ClientRegistrationRepository;
1927
import nextstep.security.userdetails.UserDetails;
2028
import nextstep.security.userdetails.UserDetailsService;
2129
import org.springframework.context.annotation.Bean;
@@ -32,17 +40,26 @@
3240
public class SecurityConfig {
3341

3442
private final MemberRepository memberRepository;
43+
private final ClientRegistrationRepository clientRegistrationRepository;
44+
private final List<OAuth2UserDetailsService> oAuth2UserDetailsServices;
3545

36-
public SecurityConfig(MemberRepository memberRepository) {
46+
private final MemberService memberService;
47+
48+
public SecurityConfig(final MemberRepository memberRepository,
49+
final InMemoryClientRegistrationRepository clientRegistrationRepository,
50+
final List<OAuth2UserDetailsService> oAuth2UserDetailsServices,
51+
final MemberService memberService) {
3752
this.memberRepository = memberRepository;
53+
this.clientRegistrationRepository = clientRegistrationRepository;
54+
this.oAuth2UserDetailsServices = oAuth2UserDetailsServices;
55+
this.memberService = memberService;
3856
}
3957

4058
@Bean
4159
public DelegatingFilterProxy delegatingFilterProxy() {
4260
return new DelegatingFilterProxy(filterChainProxy(List.of(securityFilterChain())));
4361
}
4462

45-
@Bean
4663
public FilterChainProxy filterChainProxy(List<SecurityFilterChain> securityFilterChains) {
4764
return new FilterChainProxy(securityFilterChains);
4865
}
@@ -54,21 +71,25 @@ public SecuredMethodInterceptor securedMethodInterceptor() {
5471

5572
@Bean
5673
public SecurityFilterChain securityFilterChain() {
57-
return new DefaultSecurityFilterChain(
58-
List.of(
59-
new SecurityContextHolderFilter(),
60-
new UsernamePasswordAuthenticationFilter(userDetailsService()),
61-
new BasicAuthenticationFilter(userDetailsService()),
62-
new AuthorizationFilter(requestAuthorizationManager())
63-
)
64-
);
74+
return new DefaultSecurityFilterChain(List.of(new SecurityContextHolderFilter(httpSessionSecurityContextRepository()),
75+
new UsernamePasswordAuthenticationFilter(userDetailsService()),
76+
new BasicAuthenticationFilter(userDetailsService()),
77+
new OAuth2RedirectFilter(clientRegistrationRepository),
78+
new OAuth2AuthenticationFilter(
79+
clientRegistrationRepository,
80+
oAuth2AuthenticationSuccessHandler(),
81+
oAuth2UserDetailsServices),
82+
new AuthorizationFilter(requestAuthorizationManager())));
83+
}
84+
85+
@Bean
86+
public HttpSessionSecurityContextRepository httpSessionSecurityContextRepository() {
87+
return new HttpSessionSecurityContextRepository();
6588
}
6689

6790
@Bean
6891
public RoleHierarchy roleHierarchy() {
69-
return RoleHierarchyImpl.with()
70-
.role("ADMIN").implies("USER")
71-
.build();
92+
return RoleHierarchyImpl.with().role("ADMIN").implies("USER").build();
7293
}
7394

7495
@Bean
@@ -84,8 +105,7 @@ public RequestAuthorizationManager requestAuthorizationManager() {
84105
@Bean
85106
public UserDetailsService userDetailsService() {
86107
return username -> {
87-
Member member = memberRepository.findByEmail(username)
88-
.orElseThrow(() -> new AuthenticationException("존재하지 않는 사용자입니다."));
108+
Member member = memberRepository.findByEmail(username).orElseThrow(() -> new AuthenticationException("존재하지 않는 사용자입니다."));
89109

90110
return new UserDetails() {
91111
@Override
@@ -105,4 +125,11 @@ public Set<String> getAuthorities() {
105125
};
106126
};
107127
}
128+
129+
@Bean
130+
public OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler() {
131+
return new OAuth2AuthenticationSuccessHandlerImpl(
132+
httpSessionSecurityContextRepository(),
133+
memberService);
134+
}
108135
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package nextstep.app.application;
2+
3+
import nextstep.app.domain.MemberRepository;
4+
import nextstep.app.payload.Oauth2MemberSaveDto;
5+
import org.springframework.stereotype.Service;
6+
7+
@Service
8+
public class MemberService {
9+
private final MemberRepository memberRepository;
10+
11+
public MemberService(final MemberRepository memberRepository) {
12+
this.memberRepository = memberRepository;
13+
}
14+
15+
public void saveOauth2Member(Oauth2MemberSaveDto dto) {
16+
var member = memberRepository.findByEmail(dto.email());
17+
if (member.isPresent()) {
18+
return;
19+
}
20+
memberRepository.save(dto.toMember());
21+
}
22+
23+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package nextstep.app.config;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.web.client.RestTemplate;
6+
7+
@Configuration
8+
public class WebConfig {
9+
@Bean
10+
public RestTemplate restTemplate() {
11+
return new RestTemplate();
12+
}
13+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package nextstep.app.oauth2;
2+
3+
import nextstep.security.oauth2.userdetails.AbstractOAuth2UserDetailsService;
4+
import nextstep.security.oauth2.userdetails.OAuth2UserDetails;
5+
import org.springframework.beans.factory.annotation.Value;
6+
import org.springframework.stereotype.Component;
7+
8+
import java.util.Map;
9+
import java.util.Set;
10+
11+
@Component
12+
public class GithubOAuth2UserDetailService extends AbstractOAuth2UserDetailsService {
13+
private final String userUrl;
14+
15+
public GithubOAuth2UserDetailService(
16+
@Value("${app.oauth2.github.user-url}") final String userUrl
17+
) {
18+
this.userUrl = userUrl;
19+
}
20+
21+
@Override
22+
protected OAuth2UserDetails createOauth2UserDetails(final Map<String, Object> userResponse) {
23+
var email = (String) userResponse.get("email");
24+
var name = (String) userResponse.get("name");
25+
var avatarUrl = (String) userResponse.get("avatar_url");
26+
27+
return new OAuth2UserDetailsImpl(email, Map.of("name", name,
28+
"avatarUrl", avatarUrl),
29+
Set.of("USER"));
30+
}
31+
32+
@Override
33+
protected String getUserUrl() {
34+
return this.userUrl;
35+
}
36+
37+
@Override
38+
public String getVendor() {
39+
return "github";
40+
}
41+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package nextstep.app.oauth2;
2+
3+
import nextstep.security.oauth2.userdetails.AbstractOAuth2UserDetailsService;
4+
import nextstep.security.oauth2.userdetails.OAuth2UserDetails;
5+
import org.springframework.beans.factory.annotation.Value;
6+
import org.springframework.stereotype.Component;
7+
8+
import java.util.Map;
9+
import java.util.Set;
10+
11+
@Component
12+
public class GoogleOAuth2UserDetailService extends AbstractOAuth2UserDetailsService {
13+
private final String userUrl;
14+
15+
public GoogleOAuth2UserDetailService(@Value("${app.oauth2.google.user-url}") final String userUrl) {
16+
this.userUrl = userUrl;
17+
}
18+
19+
@Override
20+
protected OAuth2UserDetails createOauth2UserDetails(final Map<String, Object> userResponse) {
21+
var email = (String) userResponse.get("email");
22+
var name = (String) userResponse.get("name");
23+
var picture = (String) userResponse.get("picture");
24+
25+
return new OAuth2UserDetailsImpl(email, Map.of("name", name, "avatarUrl", picture), Set.of("USER"));
26+
27+
}
28+
29+
@Override
30+
protected String getUserUrl() {
31+
return this.userUrl;
32+
}
33+
34+
@Override
35+
public String getVendor() {
36+
return "google";
37+
}
38+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package nextstep.app.oauth2;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import jakarta.servlet.http.HttpServletResponse;
5+
import nextstep.app.application.MemberService;
6+
import nextstep.app.payload.Oauth2MemberSaveDto;
7+
import nextstep.security.authentication.Authentication;
8+
import nextstep.security.authentication.OAuth2UserDetailsAuthenticationToken;
9+
import nextstep.security.context.HttpSessionSecurityContextRepository;
10+
import nextstep.security.context.SecurityContext;
11+
import nextstep.security.context.SecurityContextHolder;
12+
import nextstep.security.oauth2.OAuth2AuthenticationSuccessHandler;
13+
14+
import java.io.IOException;
15+
public class OAuth2AuthenticationSuccessHandlerImpl implements OAuth2AuthenticationSuccessHandler {
16+
private final HttpSessionSecurityContextRepository securityContextRepository;
17+
private final MemberService memberService;
18+
19+
public OAuth2AuthenticationSuccessHandlerImpl(final HttpSessionSecurityContextRepository securityContextRepository,
20+
final MemberService memberService) {
21+
this.securityContextRepository = securityContextRepository;
22+
this.memberService = memberService;
23+
}
24+
25+
@Override
26+
public void onSuccess(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) throws IOException {
27+
if (authentication instanceof OAuth2UserDetailsAuthenticationToken authToken) {
28+
var otherDetails = authToken.getOAuth2OtherDetails();
29+
memberService.saveOauth2Member(new Oauth2MemberSaveDto(
30+
(String) authToken.getPrincipal(),
31+
otherDetails.get("name"),
32+
otherDetails.get("avatar_url")
33+
));
34+
} else {
35+
throw new IllegalArgumentException("유효한 토큰타입이 아닙니다.");
36+
}
37+
38+
SecurityContext context = new SecurityContext(authentication);
39+
SecurityContextHolder.setContext(context);
40+
securityContextRepository.saveContext(context, request, response);
41+
response.sendRedirect("/");
42+
}
43+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package nextstep.app.oauth2;
2+
3+
import nextstep.security.oauth2.userdetails.OAuth2UserDetails;
4+
5+
import java.util.Map;
6+
import java.util.Set;
7+
8+
public record OAuth2UserDetailsImpl(String userName, Map<String, String> details, Set<String> authorities) implements OAuth2UserDetails {
9+
10+
@Override
11+
public String getUsername() {
12+
return this.userName;
13+
}
14+
15+
@Override
16+
public Map<String, String> getOthers() {
17+
return this.details;
18+
}
19+
20+
@Override
21+
public Set<String> getAuthorities() {
22+
return this.authorities;
23+
}
24+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package nextstep.app.payload;
2+
3+
import nextstep.app.domain.Member;
4+
5+
import java.util.Set;
6+
7+
public record Oauth2MemberSaveDto(
8+
String email,
9+
String name,
10+
String imageUrl) {
11+
12+
public Member toMember() {
13+
return new Member(
14+
email,
15+
null,
16+
name,
17+
imageUrl,
18+
Set.of("USER")
19+
);
20+
}
21+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package nextstep.security.access;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import org.springframework.http.HttpMethod;
5+
import org.springframework.util.AntPathMatcher;
6+
7+
public class AntRequestMatcher implements RequestMatcher {
8+
private final HttpMethod method;
9+
private final String pattern;
10+
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
11+
12+
public AntRequestMatcher(HttpMethod method, String pattern) {
13+
this.method = method;
14+
this.pattern = pattern;
15+
}
16+
17+
@Override
18+
public boolean matches(HttpServletRequest request) {
19+
if (this.method != null && !this.method.name().equals(request.getMethod())) {
20+
return false;
21+
}
22+
String requestURI = request.getRequestURI();
23+
return antPathMatcher.match(pattern, requestURI);
24+
}
25+
}

0 commit comments

Comments
 (0)