스프링 부트와 AWS로 구현하는 웹 서비스_5

HyeBin, Park·2021년 8월 11일
0
post-thumbnail

스프링 부트와 AWS로 혼자 구현하는 웹 서비스
https://github.com/HYEBPARK/springboot-webservice

5장. 스프링 시큐리티와 OAuth2.0으로 로그인 기능 구현하기


5.1 스프링 시큐리티와 스프링 시큐리티 Oauth2 클라이언트

📍 Spring Security

  • AuthenticationAuthorization 또는 권한부여 기능을 가진 프레임워크
  • 스프링기반의 애플리케이션에서 보안을 위한 표준
  • Spring Boot 2.0
    • 신규 기능은 새 oauth2 라이브러리에서만 지원
    • 스프링 부트용 라이브러리 출시
    • client 인증 정보만 입력, 그 전의 값들은 enum으로 대체

📍 Google 서비스 등록

  • 구글 서비스에 신규 서비스를 생성하여 cliendId와 clientSecret을 발급
    • https://console.cloud.google.com 에 이동
    • 프로젝트 생성
    • 사용자 인증 정보 만들기 *승인된 리디렉션 URI
    • 인증 정보 (clientId와 clientSecret) 확인
  • 승인된 리디렉션 URI
    • 서비스에서 파라미터로 인증 정보를 주었을때 인증이 성공하면 구글에서 리다이렉트할 URL
      • 리다이렉트 : 기존 URL을 다른 URL과 연결 지어 다른 페이지로 넘어간다.
    • springboot 2 버전의 시큐리티에서는 기본적으로 {도메인}/login/oauth2/code/{소셜서비스코드}
      로 리다이렉트 URL을 지원한다.
    • 사용자가 별도의 리다이텍트 URL을 지원하는 Controller를 만들 필요가 없다.
  • application-oauth.properties 파일 생성
  spring.security.oatuh2.client.registration.google.client-id = cliendId
  sprint.security.oauth2.client.registration.google.client-secret = clientSecret
  spring.security.oauth2.client.registration.google.scope = profile,email 

// scope의 기본값은 openid, profile, email 인데 profile과 email로 등록한 이유는
// openid라는 scope는 Open Id Provider 라고 인식을 하게 되는데
// OpenId Provider인 구글과는 다르게 naver, kakao는 그렇지 않아 각각 OAuth2Service를 만들어야 하기 때문

📍 Google 로그인 연동하기

- 사용자 정보를 담당할 도메인 -> User class

	@Enumerated(EnumType.STRING)
	@Column(nullable = false)
	private Role role;
  • @Enumerated(EnumType.STRING)
    • JPA로 저장할때 Enum값을 어떤 형태로 저장할지 결정
    • 기본적으로 int형으로 저장됨-> db에서 무슨 코드를 의미하는지 알 수 없어서 string으로 선언
    • Role은 Enum 클래스

- 각 사용자의 권한을 관리하는 Enum 클래스 Role

     GUSET("ROLE_GUEST", "손님"),
     USER("ROLE_USER" , "일반사용자");
  • 권한 코드에 항상 ROLE_이 앞에 있어야 한다.

- User의 CRUD를 책임지는 UserRpository

	Optional<User> findByEmail(String email);
  • findByEmail : 소셜 로그인으로 반환되는 값 중 email을 통해 이미 생성된 사용자인지 판단하기 위한 메소드

📍 스프링 시큐리티 설정

- build.gradle에 의존성 추가

	implementation('org.springframework.boot:spring-boot-starter-oauth2-client')
	// 소셜 로그인 등 클라이언트 입장에서 소셜 기능 구현시 필요한 의존성
  • spring-security-oauth2-client와 spring-security-oauth2-jose를 기본으로 관리해준다.
    • jose(Javascript Object Signing and Encryption)
      => JWT(JSON Web Tokens)의 권한을 안전하게 전송하는 프레임워크
      => JWT의 리소스 접근 권한 정보의 암호화/복호화 및 일정 기능을 제공한다.

- SecurityConfig 클래스

  @RequiredArgsConstructor
  @EnableWebSecurity // Spring Security 설정 활성화 
  public class SecurityConfig extends WebSecurityConfigurerAdapter {
        private final CustomOAuth2UserService customOAuth2UserService;
 	   @Override
      protected  void configure(HttpSecurity http) throws Exception{
          http		
                  // h2-console 화면을 사용하기 위해서 disable
                  .csrf().disable()
                  .headers().frameOptions().disable() 
                  .and()
                      .authorizeRequests() // URL별 권한 관리를 설정하는 옵션의 시작점
                      .antMatchers("/","/css/**","/images/**",
                              "/js/**","/h2-console/**").permitAll() // 전체 열람 권한
                      .antMatchers("/api/v1/**").hasRole(Role.USER.name()) // USER 권한만 가능
                      .anyRequest().authenticated() // 로그인한 사용자들만 허용
                  .and()
                      .logout()
                          .logoutSuccessUrl("/") // 로그아웃 성공 시 / 주소로 이동
                  .and()
                      .oauth2Login() // oauth2 로그인 기능에 대한 여러설정의 진입점
                          .userInfoEndpoint() // 성공 이후 사용자 정보를 가져올때 설정 담당
                              .userService(customOAuth2UserService);
      }
  }
  • antMatchers
    • 권한 관리 대상을 지정하는 옵션
    • URL, HTTP 메소드별로 관리 가능
  • anyRequest
    • 설정된 값들 이외 나머지 URL
  • authenticated()
    • 모두 인증된(로그인한) 사용자들에게만 허용
  • .userService(customOAuth2UserService)
    • 소셜 로그인 성공시 후속 조치를 할 UserService인터페이스의 구현체 등록
    • 소셜서비스들에서 사용자 정보를 가져온 상태에서 추가로 진행하고자 하는 기능 명시 가능

- CustomOAuth2UserService 클래스 생성 - 로그인 후 가져온 사용자의 정보를 기반으로 기능 지원

	String registrationId = userRequest.getClientRegistration().getRegistrationId();
        String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
                .getUserInfoEndpoint().getUserNameAttributeName();
        OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, 
        	oAuth2User.getAttributes());
        User user = saveOrUpdate(attributes);
        httpSession.setAttribute("user", new SessionUser(user));
  • registrationId
    • 현재 로그인 진행 중인 서비스를 구분 ex) google, naver ...
  • userNameAttributeName
    • OAuth2 로그인 진행 시 키가 되는 필드값, Primarty Key와 같은 의미
    • 구글은 기본적인 코드를 지원"sub" , naver, kakao등은 기본지원 x
  • OAuthAttributes
    • OAuth2UserService를 통해 가져온 OAuth2User의 attribute를 담은 클래스
  • SessionUser
    • 세션에 사용자 정보를 저장하기 위한 Dto 클래스
    • Dto(Data Transfer Object) : 클라이언트와 서버의 서비스 계층 사이에서 교환되는 데이터를 담는 그릇

- OAuthAttributes 클래스 생성 - Dto

 public User toEntity(){
        return User.builder()
                .name(name)
                .email(email)
                .picture(picture)
                .role(Role.GUEST)
                .build();
    }
  • toEntity()
    • User 엔티티를 생성
    • OAuthAttributes에서 엔티티를 처음 생성하는 시점은 처음 가입할 때
    • 가입할때의 기본권한은 GUEST

SessionUser 클래스 생성

@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();
    }
}
  • Serializable (직렬화) : 객체는 바이트형이 아니라서 스트림을 통해 파일에 저장하거나 네트워크로 전송할 수 없어서 객체를 스트림을 통해 입출력하기위해서 바이트 배열로 변환하는 것
  • 인증된 사용자 정보만 필요하기 때문에 name, email ,picture만 필드로 선언

📍 로그인 테스트

- 화면에 로그인 버튼 추가

		{{#userName}}
                    Logged in as : <span id="user">{{userName}}</span>
                    <a href="/logout" class="btn btn-info active" role = "button">Logout</a>
                {{/userName}}
                {{^userName}}
                    <a href="/oauth2/authorization/google" class="btn btn-success active" role="button">Google Login</a>
                {{/userName}}
  • {{#userName}}
    • 머스테치는 if문 제공 x , true/false 여부만 판단 => 최종값을 넘겨줘야한다.
    • userName이 있다면 userName을 노출시키도록 구성
  • a href = "/logout"
    • spring security에서 기본적으로 제공하는 logout URL => Controller를 따로 만들어줄 필요없음
  • {{^userName}}
    • userName이 없다면 로그인 버튼을 노출시키도록 구성
    • 머스테치는 해당 값이 존재하지 않는 경우 ^ 사용
  • a href = "/oauth2/authorization/google"
    • spring security에서 기본적으로 제공하는 login URL => Controller를 따로 만들어줄 필요없음

- IndexController에서 userName을 model에 저장 : index.mustache에서 userName을 사용

@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";
    }
  • (SessionUser) httpSession.getAttribute("user")
    • CustomeOAuth2UserService에서 로그인 성공시 세션에 SessionUser를 저장하도록 구성
      => 로그인 성공시 httpSession.getAttribute("user")에서 값을 가져올 수 있다.
  • if(user!=null)
    • 세션에 저장된 값이 있을 때만 model에 userName으로 등록
    • 세션에 저장된 값이 없으면 로그인 버튼만 보이게된다.

📍 status\":403,\error\

  • 403error는 권한 거부에러, 처음 로그인시 모두 권한은 GUEST로 줬다.
  • 게시글 등록은 로그인을 한 USER만 가능하기 때문에
  • UPDATE user SET role = 'USER'; 로 권한을 바꿔주어야한다.
  • 로그아웃한 후 다시 로그인하여 세션 정보를 최신 정보로 갱신하면 글쓰기 가능

🤦‍♀️ 계속 로그인을 실패했었는데 오타를 잘 확인하자 .. emil-> x emil->o ..

💡 참조

0개의 댓글