[aws] 스프링 시큐리티와 OAuth2.0
스프링 시큐리티는 인증, 인가 기능을 가진 프레임워크
이며 스프링 기반의 애플리케이션의 보안이라면 거의 표준입니다.
인터셉터와 필터 기반의 보안 기능을 직접 구현하는 것보다 스프링 시큐리티를 통해 구현하는 것을 적극 권장합니다.
스프링 시큐리티와 스프링 시큐리티 Oauth2 클라이언트
많은 서비스에서 로그인 기능을 id/password 보다 sns로그인 기능을 사용합니다.
직접 구현할 경우 배보다 배꼽이 커진다.
라고 합니다.
- 로그인 시 보안
- 비밀번호 찾기, 변경
- 회원정보 변경
- 회원가입 시 인증 위의 모든 사항을 구현해야 하는데 이것들을 모두 sns에 맡겨버리게 되니 sns로그인을 쓰는게 개발 시간 향상에 도움이 됩니다.
1. 구글 서비스 등록
- 구글 클라우드 콘솔에서 프로젝트를 하나 만듭니다.
- 해당 프로젝트의 API및 서비스 창에서 OAuth 웹 클라이언트로 사용자 인증 정보를 만듭니다. 참고사이트
- 승인된 리디렉션 적기
- 승인된 리디렉션은 구글에서 인증 후 자신의 웹 서버로 다시 요청을 보낼 url이므로 꼭 적으셔야 합니다.
- 스프링 시큐리티엔 이미 구현해 놓은 상태입니다.
{
"web": {
"client_id": secret,
"project_id": secret,
"auth_uri": secret,
"token_uri": secret,
"auth_provider_x509_cert_url": secret,
"client_secret": secret,
"redirect_uris": [
"http://localhost:8080/login/oauth2/code/google"
]
}
}
2.사용자 정보를 담당하는 도메인 만들기
@Getter
@NoArgsConstructor
@Entity
public class User extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String email;
@Column
private String picture;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
}
3.사용자 정보중 역할을 담당하는 Enum 만들기
ROLE_GUEST
스프링 시큐리티에서는 권한 코드에 항상ROLE_
이 있어야 합니다.@Getter @RequiredArgsConstructor public enum Role { GUEST("ROLE_GUEST", "손님"), USER("ROLE_USER", "일반 사용자"); private final String key; private final String title; }
4.필요한 의존성
- spring-boot-starter-oauth2-client
- 소셜 로그인 기능을 구현 하려는 클라이언트에게 필요한 의존성
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
- 소셜 로그인 기능을 구현 하려는 클라이언트에게 필요한 의존성
5.SecurityConfig
- Spring Security의 버전이 높아서 기존 책에서 하던대로 메소드 체이닝 불가
- 대신 람다활용
- Spring Security 설정 활성화
@EnableWebSecurity
- csrf 해제
http.csrf(AbstractHttpConfigurer::disable) .headers(headersConfigurer -> { headersConfigurer.frameOptions(FrameOptionsConfig::disable); })
- url에 대한 접근 제한 시작
authorizeHttpRequests
- logout에 대한 설정
logout
- oauth2Login
-
userInfoEndpoint
: 로그인 성공후 사용자 정보를 어디에 담아올지 결정 ```java @RequiredArgsConstructor @Configuration @EnableWebSecurity public class SecurityConfig {private final CustomOAuth2UserService customOAuth2UserService;
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable) .headers(headersConfigurer -> { headersConfigurer.frameOptions(FrameOptionsConfig::disable); }) .authorizeHttpRequests(matcherRegistry -> { matcherRegistry.requestMatchers(“/”, “/css/”, “/images/”, “/js/”, “/h2-console/” ).permitAll() .requestMatchers(“/api/v1/**”).hasRole(Role.USER.name()) .anyRequest().authenticated() }) .logout(logoutConfigurer -> logoutConfigurer.logoutSuccessUrl(“/”)) .oauth2Login(oAuth2LoginConfigurer -> oAuth2LoginConfigurer.userInfoEndpoint( endpointConfig -> endpointConfig.userService(customOAuth2UserService)) ); return http.build(); }
-
}
### 6.로그인 성공 후, 후속 로직을 이어갈 `OAuth2UserService` 작성
> 구글 로그인 이후 가져온 사용자의 정보를 바탕으로 가입 및 정보수정, 세션 저장 등의 기능을 지원
#### 하는 기능
- OAuthAttributes 생성
- 유저 저장
- User user = saveOrUpdate(attributes);
- 세션 저장
- httpSession.setAttribute("user", new SessionUser(user));
- DefaultOAuth2User 생성및 반환
- return new DefaultOAuth2User
#### 소스 코드
- 소스 코드에 주석으로 상세 설명
```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);
// userRequest.getClientRegistration().getRegistrationId();
// 현재 로그인 진행중인 서비스를 구분하는 코드
String registrationId = userRequest.getClientRegistration().getRegistrationId();
// userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()
// OAuth2로 로그인 진행시 키가 되는 값
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName();
//위의 서비스 구분 코드, OAuth2 키값, OAuth2의 attribute를 담은 객체
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName,
oAuth2User.getAttributes());
// 새로운 SessionUser 객체를 생성해 session에 저장
// SessionUser는 세션에 사용자 정보를 저장하기 위한 Dto
User user = saveOrUpdate(attributes);
httpSession.setAttribute("user", new SessionUser(user));
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),
attributes.getAttributes(), attributes.getNameAttributeKey()
);
}
private User saveOrUpdate(OAuthAttributes attributes) {
return (User) userRepository.findByEmail(attributes.getEmail())
.map(entity -> entity.update(attributes.getName(), attributes.getPicture()))
.orElse(attributes.toEntity());
}
}
현재 로그인 진행중인 서비스를 구분하는 코드
userRequest.getClientRegistration().getRegistrationId();
OAuth2로 로그인 진행시 키가 되는 값
userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
SessionUser를 사용하는 이유
User 클래스는 직렬화를 구현하지 못하기 때문에 User 클래스를 session.setAttribute
에 그대로 사용할 수 없습니다.
User 클래스가 엔티티이기 때문입니다. 엔티티는 다른 객체와 연관관계가 있고 이 연관관계까지 직렬화 해야하는데 아주 많은 관계가 있을 경우 성능 이슈를 만듭니다.
그러므로 직렬화 가능을 가진 세션Dto를 하나 더 만드는 것입니다.
403
글 등록시 403
권한 거부 됩니다.
7. 어노테이션 기반으로 개선하기
같은 코드가 반복되는 것은 개선해야합니다.
1. IndexController
의 session
부분 어노테이션으로 개선
- Annotation 생성 ```java @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface LoginUser {
}
- `LoginUserArgumentResolver`라는 `HandlerMethodArgumentResolver` 구현체 생성
```java
@RequiredArgsConstructor
@Component
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
private final HttpSession httpSession;
// 파라미터가 유효한 값인지 검증
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 파라미터에 `LoginUser` 라는 어노테이션이 붙어있어야 합니다.
boolean isLoginUserAnnotation = parameter.getParameterAnnotation(LoginUser.class) != null;
// 파라미터의 타입이 `SessionUser`타입이어야 합니다.
boolean isUserClass = SessionUser.class.equals(parameter.getParameterType());
return isLoginUserAnnotation && isUserClass;
}
// 파라미터에 전달할 객체 생성
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// 세션에서 "user"란 이름의 객체를 가져옵니다.
return httpSession.getAttribute("user");
}
}
WebConfig
라는WebMvcConfigurer
구현체 생성@RequiredArgsConstructor @Configuration public class WebConfig implements WebMvcConfigurer { private final LoginUserArgumentResolver loginUserArgumentResolver; @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(loginUserArgumentResolver); } }
- IndexController 변경전
@GetMapping("/") public String index(Model model){ model.addAttribute("posts", postsService.findAllDesc()); SessionUser user = (SessionUser) httpSession.getAttribute("user"); if(user != null){ model.addAttribute("username", user.getName()); } return "index"; }
- IndexController 변경후
@GetMapping("/") public String index(Model model, @LoginUser SessionUser user){ model.addAttribute("posts", postsService.findAllDesc()); if(user != null){ model.addAttribute("username", user.getName()); } return "index"; }
댓글남기기