스프링 시큐리티는 스프링 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크다. 즉 인증(Authenticate, 누구인지) 과 인가(Authorize, 어떤것을 할 수 있는지)를 담당하는 프레임워크를 말한다.
스프링 시큐리티에서는 주로 서블릿 필터(filter)와 이들로 구성된 필터체인으로의 구성된 위임모델을 사용한다. 그리고 보안과 관련해서 체계적으로 많은 옵션을 제공해주기 때문에 개발자 입장에서는 일일이 보안관련 로직을 작성하지 않아도 된다는 장점이 있다.
이 이후는 추후에 알아보도록 하고 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 5장
을 읽고 실습한 내용을 정리할 것이다.
스프링 부트 1.5 에서 OAuth2 연동 방법이 2.0에서는 크게 변경되었지만, 설정 방법에 크게 차이가 없는 경우를 자주 본다고 한다. 이유 - spring-security-oauth2-autoconfigure
라이브러리 덕분이다.
이 책에서는 스프링 부트 2 방식인 Spring Security Oauth2 Client 라이브러리를 사용하여 진행한다.
먼저 구글 서비스에 신규 서비스를 생성해야 한다. 여기서 발급된 인증 정보를 통해 로그인 기능과 소셜 서비스 기능을 사용할 수가 있다.
구글 클라우드 플랫폼 바로가기
여기서 프로젝트 선택을 누른 후 새 프로젝트를 클릭한다.
여기서 프로젝트 이름을 원하는대로 짓는다.
그 다음 API 및 서비스 탭의 사용자 인증 정보에서 사용자 인증 정보 만들기 탭으로 이동한 후
OAuth 2.0클라이언트 ID를 만들어준다.
설정은 아래와 같이 한다.
승인된 리디렉션URI는 스프링 부트 2버전의 시큐리티에서 기본적으로 {도메인}/login/oauth2/code/{소셜서비스코드}로 Redirect URL을 지원하고 있다.
추후에 AWS 서버에 배포하게 될때 주소를 추가하게 될텐데 이후에 다시 포스팅하도록 하겠다.
생성된 클라이언트로 들어가게 되면 클라이언드 ID와 비밀코드를 프로젝트에 설정한다.
소스에는 없지만
application-oauth.properties 파일을 하나 생성하고 아래와 같이 등록한다.
application-oauth.properties
spring.security.oauth2.client.registration.google.client-id=클라이언트 ID
spring.security.oauth2.client.registration.google.client-secret=클라이언트 비밀 코드
spring.security.oauth2.client.registration.google.scope=profile,email
application.properties에 소스 추가
spring.profiles.include=oauth
oauth로 한 이유는 스프링 부트에서는 properties이름을 application-xxx.properties로 만들면 xxx라는 이름의 profile이 생성되어 이를 통해 관리를 할 수가 있다.
아래에는 자바 소스코드이다. 여기서부터는 주석으로 설명을 대체하겠다.😅
SecurityConfig.java
@RequiredArgsConstructor
@EnableWebSecurity // Spring security활성화 어노테이션
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomOAuth2UserService customOAuth2UserService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.headers().frameOptions().disable()
// 여기까지가 h2-console 화면을 사용하기 위해 해당 옵션들을 disable
.and()
.authorizeRequests()
//URL별 권한 관리를 설정하는 옵션 시작점, authorizeRequests가 선언되어야 아래의 antMatchers옵션 사용가능
.antMatchers("/", "a주소", "b주소",
"c주소", "d주소").permitAll()
// "/"등 지정된 URL들은 permitAll() 옵션을 통해 전체 열람 권한을 주었음
.antMatchers("!주소").hasRole(Role.USER.name())
// 권한 관리 대상을 지정하는 옵션, URL, HTTP,메소드별로 관리 가능
// !주소를 가진 API는 USER권한을 가진 사람만 가능하도록 하였음
.anyRequest().authenticated()
// anyRequest는 설정된 값들 이외 나머지 URL들을 나타냄
// 여기서는 authenticated를 추가하여 나머지 URL들은 모두 인증된 사용자들에게만 허용하게 한다.
// 인증된 사용자 -> 로그인한 사용자
.and()
.logout()
.logoutSuccessUrl("/") // 로그아웃 기능에 대한 여러 설정의 진입점
// 로그아웃에 성공하면 / 주소로 이동된다.
.and()
.oauth2Login()
// oauth2로그인 기능에 대한 여러 설정의 진입점
.userInfoEndpoint()
// Oauth2 로그인 성공 이후 사용자 정보를 가져올 때의 설정들을 담당
.userService(customOAuth2UserService);
// 소셜 로그인 성공 시 후속 조치를 진행할 UserService 인터페이스의 구현체를 등록
// 리소스 서버(소셜 서비스)에서 사용자 정보를 가져온 상태에서 추가로 진행하고자 하는 기능을 명시할 수 있다.
}
}
CustomOAuth2UserService.java
@RequiredArgsConstructor
@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final HttpSession httpSession;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
// 현재 로그인 진행중인 서비스를 구분하는 코드
String registrationId = userRequest.getClientRegistration().getRegistrationId();
// OAuth2 로그인 진행 시 키가 되는 필드값을 이야기함. Primary key같은 의미
// 구글의 경우 기본적으로 코드를 지원하지만 네이버, 카카오 등은 기본 지원하지 않는다.
// 구글의 기본코드는 "sub"
// 이후 네이버, 구글 로그인 동시 지원할때 사용
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName();
//OAuth2UserService를 통해 가져온 OAuth2User의 attribute를 담을 클래스
// 향후 다른 소셜 로그인도 이 클래스 사용함
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
User user = saveOrUpdate(attributes);
httpSession.setAttribute("user", new SessionUser(user));
//SessionUser는 세션에 정보를 저장하기 위한 DTO클래스
return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),
attributes.getAttributes(), attributes.getNameAttributeKey());
}
private User saveOrUpdate(OAuthAttributes attributes){
User user = userRepository.findByEmail(attributes.getEmail())
.map(entity -> entity.update(attributes.getName(), attributes.getPicture()))
.orElse(attributes.toEntity());
return userRepository.save(user);
}
}
OAuthAttributes.java
@Getter
public class OAuthAttributes {
private Map<String, Object> attributes;
private String nameAttributeKey;
private String name;
private String email;
private String picture;
@Builder
public OAuthAttributes(Map<String, Object> attributes, String nameAttributeKey, String name, String email, String picture) {
this.attributes = attributes;
this.nameAttributeKey = nameAttributeKey;
this.name = name;
this.email = email;
this.picture = picture;
}
public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes){
// OAuth2에서 반환하는 사용자 정보는 Map이기 때문에 값 하나하나를 변환해야 한다.
return ofGoogle(userNameAttributeName, attributes);
}
private static OAuthAttributes ofGoogle(String userNameAttributeName, Map<String, Object> attributes){
return OAuthAttributes.builder()
.name((String) attributes.get("name"))
.email((String) attributes.get("email"))
.picture((String) attributes.get("picture"))
.attributes(attributes)
.nameAttributeKey(userNameAttributeName)
.build();
}
public User toEntity(){
//User 엔티티 생성
/*
OauthAttributes에서 엔티티를 생성하는 시점은 처음 가입할 때
가입할 때의 기본 권한을 Guest로 주기 위해 role 값에는 GUEST enum을 사용
*/
return User.builder()
.name(name)
.email(email)
.picture(picture)
.role(Role.GUEST)
.build();
}
}
SessionUser.java
@Getter
public class SessionUser implements Serializable {
private String name;
private String email;
private String picture;
public SessionUser(User user) {
// 인증된 사용자 정보만 필요하므로 세개만 필드로 선언함
this.name = user.getName();
this.email = user.getEmail();
this.picture = user.getPicture();
}
}
이렇게 해서 모든 시큐리티 설정이 끝났다.
스프링이나 스프링 부트에 몰두해서 돌아보니 많이 부족한 부분이 있는것 같다.😱
스프링 시큐리티 더 나아가서는 스프링 배치, 스프링 Data JPA 등등 넓게 다방면으로 알고 공부 많이 해야겠다고 생각한다. 이상 이번 포스팅은 마친다.
이후에 스프링 시큐리티 개념을 다시한번 정리하도록 하겠다.😀
깃허브 소스