[Spring] Spring Security를 이용한 우당탕탕 인증/인가 개발기(1) - 회원가입(일반/Oauth2.0)

    Intro

    원래 계획했던 일반(Id + Password) + Oauth 회원가입/로그인 로직과 이를 이용한 Jwt Token 활용에 대해서 1차적으로 완성을 하였다. Spring Security를 이용해서 진행을 함에 있어서, 내가 생각했던 것에 비해 복잡하게 구성되어 있었던 것 같고, 이런 뻘짓, 저런 뻘짓 등을 해가며 구현하였다. 또한, 아직 프론트와의 연결을 진행하지 못했기 때문에 얼른 연결해서 테스트를 하고 디버깅을 빠르게 해야할 것 같다. 대략 1달이라는 시간을 소비하였는데, 물론 모든 시간을 코딩에만 집중하지 못하고 연말이어서 이런 일, 저런 일 등 내 시간을 빼앗는 일들도 많았다. 지금부터 할 포스팅에서는 내가 공부하며 구현했던 부분들을 단계적으로 정리해보고자 한다.


    일반 회원가입/로그인에 Spring Security를 이용하게 된 배경

    가장 먼저 일반 회원가입을 구현함에 있어서 생각보다 크게 막혔던 부분은, Spring Security를 이용하는 부분이었다.

    처음에는, 단순히 email과 password를 받고, 이를 암호화 한 후 DB에 저장. 추후 POST 요청으로 email과 password를 받고, DB에 저장된 password를 decode해서 비교하면 끝나게 되지 않을까? 였다.

    하지만, 조사해 본 결과 Spring security의 기능을 이용하는 것이 여러가지 이점을 가지고 있고, 이를 일반화 한 메소드를 제공하는 것에는 이유가 있다고 생각을 했다. 크게 3가지 정도의 이유를 생각해 보았다.

    • 보안적인 이점 : 인증, 권한 부여, 세션 관리 등을 프레임워크를 통해 처리할 수 있음
    • Filter기반 보안 사용 : 스프링 filter에 대한 지식이 부족했고, 이에 관련된 부분을 공부하고 싶었음
    • Oauth의 경우 Spring security를 이용할 텐데, 일반 회원가입 또한 같이 가져가는 것이 더 옳다고 생각했음

    물론, 내 생각이 100% 정답은 아니지만, 스프링을 공부하고 사용하는 입장에서 Spring Security에 대해서 알아야지 나중에 내가 필요한 부분들을 추가로 적용할 수 있을 것이라 생각했다.


    #1 Member Entity 작성

    [Member]

    기본 생성자의 경우, Spring Data JPA를 사용하기 떄문에 protected로 생성하였다. 또한 Setter의 위험성을 본 적이 있기 때문에 Getter만 세팅하였다.

    password 컬럼을 확인해보면 nullable로 되어 있는데, Oauth2.0을 통해 회원가입한 유저의 경우 비밀번호를 따로 받지 않기 때문이다. 또한 Oauth2.0 유저의 정보를 확인하기 위해 Oauth에서 제공하는 id와 어떤 provider(Google, Kakao 등)로 가입하였는지 구분을 위해 컬럼을 추가했다.

    이번 프로젝트의 경우 토큰을 사용할 것이고, accessToken과 refreshToken 2개를 사용할 예정이기 때문에 refreshToken을 보관하기 위해 컬럼을 생성했다.

    role의 경우 Spring Security에서 권한을 정의하기 위해 지정하였다.

    role의 경우, 이런 식으로 되어 있다.

    #2 이메일과 패스워드를 받아서 회원가입 기능 구현

    일단 일반 회원의 이메일과 패스워드를 받아 이를 DB에 저장하는 회원가입 기능부터 구현을 하기로 결정했다.

    [MemberController]

    이런식으로, 회원가입을 하는 controller을 받아준다. SignUpReq라는 Dto를 통해서 request body값을 받고 있으며, 이메일 형식을 파악하기 위해서 @Valid 어노테이션을 사용했다.

    위 쪽, Post 요청은 실질적인 회원가입, Get 요청은 프론트측에서 이메일 중복확인을 위해서 사용한다.

    [SignUpReq]

    @Pattern 어노테이션과 정규식을 활용하여 이메일 형식이 올바른지 파악하고, 비밀번호와 비밀번호 재확인 부분을 받게 된다.

    [SignUpService]

    SignUpReq를 인자로 받는 회원가입 서비스 로직이다. 유효한 비밀번호인지 확인하고, 비밀번호와 재확인 비밀번호가 같은지부터 파악한다. 그 이후 보안적인 이유 때문에 비밀번호를 암호화하여 DB에 집어넣게 된다.

    여기서 한 가지 고민을 했었다. "프론트 측에서도 이메일 패스워드 등을 검증해서 넘겨주는데, 백엔드에서도 처리를 해야 할까?" 라는 부분이었는데, 같이 프로젝트를 진행하는 프론트분과 이야기를 나누었을 때, 프론트와 백엔드 모두에서 더블체크를 하는 것이 옳은 것 같다. 라는 이야기를 듣고 공감하게 되었다. 프론트 페이지를 통한 것이 아니라, 직접 주소와 body값을 알아내서 보낼 요청을 보낼 수도 있는 위험성이 존재할 것이기 때문이었다.

    (또한, 글을 작성하며 내가 놓친 부분을 파악할 수 있었는데, 중복 이메일의 경우 프론트측에서만 파악하고 백엔드 측에서는 검증을 따로 하지 않고 있다. 이 부분은 수정을 진행해야겠다.)

    Swagger을 통해서 실행 해 보았을 때, 저장이 잘 되는 것을 확인할 수 있었다.

    로그인의 경우, 토큰을 적용할 생각이었기 떄문에 토큰을 적용한 이후에 구현을 진행하였다.

    #3 Oauth2.0을 통한 회원가입 기능 구현

    먼저 Spring Security를 통한 Oauth2.0 회원가입/로그인의 경우 다른 포스팅에서 작성하였기 때문에, 따로 이야기하지 않겠다.

    Spring Security에서 oauth2.0을 위한 부분을 설정해 주었다.

    [CustomOAuth2User]

    DefaultOAuth2User 을 상속받고, 나에게 필요한 부분을 추가로 적용했다.

    [OauthAttribute]

    @Getter
    public class OauthAttribute {
        private Map<String, Object> attributes;
        private String nameAttributeKey;
        private Long memberId;
        private String name;
        private String email;
        private String oauthProvider;
        private Integer oauthId;
    
        @Builder
        public OauthAttribute(Map<String, Object> attributes,
                              String nameAttributeKey,
                              Long memberId,
                              String name,
                              String email,
                              String oauthProvider,
                              Integer oauthId) {
            this.attributes = attributes;
            this.nameAttributeKey = nameAttributeKey;
            this.memberId = memberId;
            this.name = name;
            this.email = email;
            this.oauthProvider = oauthProvider;
            this.oauthId = oauthId;
        }
    
        public static OauthAttribute of(String userNameAttributeName,
                                        Map<String, Object> attributes) {
            return of42(userNameAttributeName, attributes);
        }
    
        private static OauthAttribute of42(String userNameAttributeName,
                                           Map<String, Object> attributes) {
            return OauthAttribute.builder()
                    .name((String) attributes.get("login"))
                    .email((String) attributes.get("email"))
                    .oauthProvider("42Seoul")
                    .oauthId((Integer) attributes.get("id"))
                    .attributes(attributes)
                    .nameAttributeKey(userNameAttributeName)
                    .build();
        }
    
        public OauthAttribute updateMemberId(Long memberId) {
            this.memberId = memberId;
            return this;
        }
    
        public Member toEntity() {
            return Member.builder()
                    .name(name)
                    .email(email)
                    .oauthId(oauthId)
                    .oauthProvider("42Seoul")
                    .role(MemberRole.MEMBER)
                    .build();
        }
    }

    Oauth를 통해 얻어온 부분을 바탕으로 내 Oauth 속성을 설정해 주는 부분이다.

    중간에 of 메소드를 확인해보면 그냥 return of42() 라고 되어 있는데, 현재는 42Seoul로그인만 지원을 하기 때문이다. 만약 다른 provider을 사용하게 된다면, 추가로 provider을 받아서 그에 따라 return 메소드를 다르게 지정하려고 한다.

    [customOauth2UserService]

    OAuth2UserService 인터페이스를 받아서 구현을 진행했다.

    기본적으로 인터페이스에 구현되어 있는 loadUser 부분을 작성해 주어야 한다.

    아마 위에서 설명하였듯이, oauth 시스템을 확장하게 된다면 registrationId를 받고 이를 of 메소드에 인자로 넣어 return 메소드를 다르게 지정할 예정이다.

    OauthAttribute로 속성들을 지정한 후에, db상에 저장하게 된다. 만약 이미 있는 유저라면 update를 하고 그게 아니라면 toEntity() 메소드를 통해 유저를 생성한다.

    마지막에, 그냥 OAuth2User가 아닌, 내가 custom해서 memberId와 oauthProvider을 지니고 있는 CustomOAuth2User을 리턴한다.

    OAuth2SuccessHandler의 경우에는 별도로 작성하지 않아도 되지만, 나의 경우 토큰 설정을 위해서 따로 작성을 진행하고 Config에도 설정을 하였다.

    실제로 적용을 해보면 DB상에도 정상적으로 저장되는 것을 확인할 수 있었다.

     

    아마 간단하고, 기본적인 부분이었겠지만 이미 짜여져 있는 구조를 몰랐었기 때문에 쉽게 진행하기 어려웠던 것 같다.

    다음은 나를 지금도 괴롭히고 있는 token 부분으로 넘어가보려고 한다.

    댓글