이 문서는 가리사니 개발자 포럼에 올렸던 글의 백업 파일입니다. 오래된 문서가 많아 현재 상황과 맞지 않을 수 있습니다.
Spring Security OAuth 통합 로그인 시리즈
서론
2강에서 일반 로그인을 구현했다면 이번장에선 OAuth를 연동해보겠습니다. 스프링에서 제공해주는 OAuth 클라이언트는 “스프링 소셜”, “스프링 OAuth SSO”, “OAuth2 매뉴얼”이 있는데 “OAuth2 매뉴얼” 을 이용하여 구현합니다. 왜 이 방법을 선택했는지에 대해서는 2장 강의를 참고해주시기 바랍니다.
구조 web.configuration.security.oauth2 부분을 구현해 보겠습니다.
의존성
<!-- 스프링 시큐리티 oauth -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
OAuth 프로퍼티 설정 먼저 각 개발자 사이트에서 로그인앱을 만들고 clientId / clientSecret 을 발급받습니다. 페이스북과 네이버는 아래 주소를 그대로 입력하시면 되며 나머지 사이트들도 API 문서를 보시면 쉽게 accessTokenUri / userAuthorizationUri / userInfoUri 를 찾을 수 있습니다.
# 페이스북
facebook.client.clientId: 발급받은 클라이언트 아이디
facebook.client.clientSecret: 발급받은 시크릿 키
facebook.client.accessTokenUri: https://graph.facebook.com/oauth/access_token
facebook.client.userAuthorizationUri: https://www.facebook.com/dialog/oauth
facebook.client.tokenName: oauth_token
facebook.client.authenticationScheme: query
facebook.client.clientAuthenticationScheme: form
# 스코프는 제가 임시로 넣어봤습니다. 이런식으로 권한을 추가할 수 있습니다.
facebook.client.scope: public_profile,email,user_birthday
facebook.resource.userInfoUri: https://graph.facebook.com/me
# 네이버
naver.client.clientId: 발급받은 클라이언트 아이디
naver.client.clientSecret: 발급받은 시크릿 키
naver.client.accessTokenUri: https://nid.naver.com/oauth2.0/token
naver.client.userAuthorizationUri: https://nid.naver.com/oauth2.0/authorize
naver.client.tokenName: oauth_token
naver.client.authenticationScheme: query
naver.client.clientAuthenticationScheme: form
naver.resource.userInfoUri: https://openapi.naver.com/v1/nid/me
이 강의에선 yaml 을 설명하지 않아 프로퍼티로 하였습니다. 스프링 yaml 사용
OAuth2 Client 설정 참고 : [https://spring.io/guides/tutorials/spring-boot-oauth2/#_social_login_manual registration.setOrder(-100); 에 대한 설명을 위 주소에서 참고하세요.
@Configuration
@EnableOAuth2Client
public class OAuth2ClientConfig
{
@Autowired
OAuth2ClientContext oauth2ClientContext;
// 앞서 전장에서 만들었던 계정 서비스입니다.
@Autowired
AccountService accountService;
@Bean
@ConfigurationProperties("facebook.client")
AuthorizationCodeResourceDetails facebook()
{
return new AuthorizationCodeResourceDetails();
}
@Bean
@ConfigurationProperties("facebook.resource")
ResourceServerProperties facebookResource()
{
return new ResourceServerProperties();
}
@Bean
@ConfigurationProperties("naver.client")
AuthorizationCodeResourceDetails naver()
{
return new AuthorizationCodeResourceDetails();
}
@Bean
@ConfigurationProperties("naver.resource")
ResourceServerProperties naverResource()
{
return new ResourceServerProperties();
}
@Bean
FilterRegistrationBean oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter)
{
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(filter);
// 스프링 사이트에 의하면 다른 필터보다 우선순위를 올리기위해 -100을 주었다고 나옵니다.
registration.setOrder(-100);
return registration;
}
@Bean("sso.filter")
Filter ssoFilter()
{
List<Filter> filters = new ArrayList<>();
// 페이스북
OAuth2ClientAuthenticationProcessingFilter facebook
= new OAuth2ClientAuthenticationProcessingFilter("/sign-in/facebook");
facebook.setRestTemplate(new OAuth2RestTemplate(facebook(), oauth2ClientContext));
facebook.setTokenServices(new UserTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId()));
facebook.setAuthenticationSuccessHandler(new OAuth2SuccessHandler("facebook", accountService));
filters.add(facebook);
// 네이버
OAuth2ClientAuthenticationProcessingFilter naver
= new OAuth2ClientAuthenticationProcessingFilter("/sign-in/naver");
naver.setRestTemplate(new OAuth2RestTemplate(naver(), oauth2ClientContext));
naver.setTokenServices(new UserTokenServices(naverResource().getUserInfoUri(), naver().getClientId()));
naver.setAuthenticationSuccessHandler(new OAuth2SuccessHandler("naver", accountService));
filters.add(naver);
CompositeFilter filter = new CompositeFilter();
filter.setFilters(filters);
return filter;
}
}
위 주소에서 설정한 “/sign-in/facebook”, “/sign-in/naver” 는 각각 OAuth 제공 개발자 사이트에 가서 리다이렉트 허용설정을 해주셔야합니다. (http를 포함하여 전체로 쓰셔야합니다.) 추가설명 @EnableOAuth2Client 를 사용하여 OAuth2 Client 사용을 알립니다. 각종 필터와 필터 등록 빈을 만듭니다.
UserTokenServices
public class UserTokenServices extends UserInfoTokenServices
{
public UserTokenServices(String userInfoEndpointUrl, String clientId)
{
super(userInfoEndpointUrl, clientId);
setAuthoritiesExtractor(new OAuth2AuthoritiesExtractor());
}
public static class OAuth2AuthoritiesExtractor implements AuthoritiesExtractor
{
@Override
public List<GrantedAuthority> extractAuthorities(Map<String, Object> map)
{
return AuthorityUtils.createAuthorityList(RoleType.OAUTH.toString());
}
}
}
별다른 의미는 없지만 UserInfoTokenServices에서 ROLE 기본값이 ROLE_USER 인 관계로 OAuth로 로그인 했다는 것을 알리기위해 OAUTH : RoleType.OAUTH.toString() 로 바꾸었습니다.
OAuth2SuccessHandler 인증이 성공된 경우!
public class OAuth2SuccessHandler implements AuthenticationSuccessHandler
{
final String type;
final AccountService accountService;
public OAuth2SuccessHandler(String type, AccountService accountService)
{
this.type = type;
this.accountService = accountService;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse res, Authentication auth)
throws IOException, ServletException
{
// 연결되어 있는 계정이 있는지 확인합니다.
// 이전 강의 AccountService.getAccountByOAuthId 참고!!
Account account = accountService.getAccountByOAuthId(type, auth.getName());
// 연결되어 있는 계정이 있는경우.
if (account != null)
{
// 기존 인증을 바꿉니다.
// 이전 강의의 일반 로그인과 동일한 방식으로 로그인한 것으로 간주하여 처리합니다.
// 기존 인증이 날아가기 때문에 OAUTH ROLE은 증발하며, USER ROLE 이 적용됩니다.
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken
(
new UserDetails(account), null, UserDetails.getAuthorities(account.getRoles()))
);
res.sendRedirect("/");
}
// 연결된 계정이 없는경우
else
{
// 회원가입 페이지로 보냅니다.
// ROLE 은 OAUTH 상태입니다.
res.sendRedirect("/sign-up/oauth");
// 특별히 추가정보를 받아서 가입해야할 일이없다면,
// 즉석으로 계정을 생성한 후 성공처리 해준다면 사용자 입장에서 좋을 것 같습니다.
}
}
}
접속 확인
주소/sign-in/facebook 으로 접속합니다. 자동으로 리다이렉트 되고 인증이 성공된 경우 OAuth2SuccessHandler.onAuthenticationSuccess 에 따라 작동하게 됩니다.
Authentication 확인 해보기
@RequestMapping(path="/test", produces="text/plain")
@ResponseBody
String test(Authentication authentication)
{
return authentication;
}
/test 에 로그인 전 / 후 OAuth 만 인증된 경우 확인 해보시기 바랍니다.!!
추신
하지만 로그인에 너무 많은 문제점들이 있어서!!! 좀 더 정리를 해서 구현해야할 것 같습니다.!! 이제 ROLE 이 OAUTH 일때 OAUTH 추가 회원가입만 만드시면 됩니다. 사이트에서 수집하는 개인정보가 ID 정도만으로도 만족된다면, 즉석에서 게정을 만들어 준 후 접속되게 하는 방법도 괜찮을 것 같습니다.!!