요즘은 서비스 업체들이 직접적으로 로그인을 쓰기 보다는 SNS의 로그인 연동을 많이 사용 한다.
로그인 기능을 구현하게 되면 생각보다 많은 리소스가 투자되기 때문이다.
보안, 가입시 절차에 사용되는 이메일/SMS 발송, 비밀번호 찾기/변경, 회원정보 관리 및 변경 등등의 기능을 모두 구현해야 한다.
여기에 덤으로 개인정보 보호법에 의거한 데이터 관리 이슈도 따라온다.
이러한 로그인 관련 핵심 기술에는 인증과 인가가 핵심이 된다.
그래서 스프링은 자신들의 보안 표준으로 스프링 시큐리티를 제공하고 있다.
시큐리티의 버전은 크게 1.5와 2.0 이후를 기준으로 삼으며 설정방법에서 다소 차이를 보인다.
1.5에서는 url 주소를 모두 명시하고 있지만 2.0에서는 클라이언트의 인증 정보만을 요구한다.
그리고 2.0에서는 1.5에서 요구했던 입력값들을 CommonOAuth2Proiver라는 enum으로 대체되었다.
책에서는 2.0을 기준으로 구술을 하고 있는데 스프링에서 1.5에서의 추가적인 지원이 없이 2.0을 제공하기로 발표했기 때문이라고 한다.
우선 구글 로그인을 구현해보자.
구글계정으로 인증정보를 발급받아 로그인 기능과 소셜 서비스 기능을 사용하려 한다.
구글 클라우드 플랫폼으로 접속하자.(https://console.cloud.google.com/)
새로운 프로젝트를 생성해야 한다.
생성 후 좌측 메뉴에서 API 및 서비스 메뉴를 선택한다.
좌측에 사용자 인증 정보 메뉴를 선택하자.
사용자 인증 정보를 만들기를 클릭해서 OAuth 클라이언트 ID를 생성해야 한다.
동의 버튼을 클릭하면 책과는 다른 UI화면이 나온다.
UserType을 결정하라고 하는데 내부/외부 중에 선택을 해야 한다.
일단 내부 사용자로 만들어 보려했는데, 선택자체가 불가능했다.
외부사용자로 선택하도록 한다.
앱정보를 입력하는 화면이 나왔다.
책에 나오는 내용과 또 다른 화면이 나온다.
여기서부터는 해딩을 해야 할 것 같다.
우선 기입가능한 정보들을 최대한 기입해보도록 하자.
저장후 계속을 클릭 해보자.
범위에 관련된 설정을 하는 UI가 나왔다.
우선은 아무것도 선택하지 않고 넘어가 보도록 한다.
테스트계정 등록 메뉴가 나왔다.
사용자 추가 버튼을 클릭해서 내 개인 계정을 등록했다.
테스트계정 생성 결과가 나오고 요약화면으로 넘어가졌다.
이게 정말 제대로 된건지 확인이 필요했다.
우선 googgleing을 해보니, 동의에 관련되서 방금 진행했던 과정을 수행한 다음,
다시 사용자 인증 정보 메뉴에 접근하여 사용자 인증 정보 만들기를 클릭하여
OAuth 클라이언트 ID 생성을 시도해야 한다고 한다.
다시 접근하니 어플리케이션 유형 선택지가 나왔다.
어플리케이션을 선택해 보자.
승인된 자바스크립트 원본 입력 메뉴와 승인된 리디렉션 URI 메뉴가 도출된다.
여기서 우리가 작성해야하는 건 승인된 리디렉션 URI인데
서비스에서 파라미터로 인증 정보를 주었을 때 인증이 성공하면 구글에서 리다이렉트할 URL 정보를 기입하는 곳이다.
스프링 부트는 {도메인}/login/oauth2/code/{소셜서비스코드}로 리다이렉트 URL을 지원하고 있어서 이 주소폼에 맞는 값을 기입하도록 하자.(http://localhost:8080/login/oauth2/code/google)
이렇게 지정하면 스프링 시큐리티에 구현된 컨트롤러를 사용하므로 별도로 생성하지 않고 편하게 사용할 수 있다.
이제 만들기 버튼을 누르면 클라이언트 ID와 보안 비밀번호 생성 알림이 뜬다.
이제 연동을 하기 위해 프로젝트 개발을 시작하자.
구글 서비스에서 생성한 정보를 이제 properties로 생성해야 한다.
그리고 로그인할때 필요한 내용을 담당할 도메인을 설계해야 한다.
생성한다음 클라이언트 아이디, 클라이언트 보안 비밀 코드, 스코프 값을 아래와 같이 입력하자.
책에서는 여기서 scope 설정에 주목하라고 한다.
각 SNS 사별로 지원되는 스코프 기준이 다를 수 있는데, 미지원하는 항목이 포함되느 경우 지원하지 않는 회사의 Provider를 별도로 구현해야하는 문제가 생긴다.
책에서는 이를 미연에 방지하기 위해 기본적으로 모두가 제공하는 email, profile만을 사용하기 위해 scope를 지정한 것으로 명시하고 있다.
이제 application.properties에서 새로 만든 OAuth properties를 인식시키기 위해 코드를 추가하자.
스프링부트에서는 properties의 파일명에 대한 패턴을 제공한다.
application-xxx.properties로 명명하면 xxx에 대한 profile을 통해서 관리가 가능하다.
이제 oatuh.properties를 git에 올라가지 않도록 gitignore 등록을 해야 한다.
왜 형상관리를 하지 않는가에 대한 의문을 가질 수 있는데,
보안에 관련된 코드 내용이고 이를 오픈하는 것은 위험한 행동이다.
github에 이를 노출되지 않게 해야 한다.
gitignore가 정상적으로 수정되었지만 나는 아직 commit 목록에 oauth properties가 노출 되고 있다.
원인은 git에 캐쉬가 문제가 있어서 이를 초기화 해야하는 것으로 보인다.
이동욱님께서는 친절하게 이를 위한 해결책을 제시해 주셨다.
현재의 캐쉬를 삭제하고 다시 추가 후 커밋하는 절차를 진행한다.
git rm -r --cached .
git add .
git commit -m "fixed untracked files"
이제 다시 commit목록을 보면 정상적으로 oauth properties가 노출되지 않는다.
사용자의 정보로는 기본적으로 데이터에 저장할때 각각의 사용자에게 PK값이 할당되야하고 사용자이름, 이메일, 프로필이미지, 권한(role) 정도를 고려해 볼 수 있다.
여기서 권한(role)을 고민해야 한다.
일반적으로 권한별 코드값을 생각할 수 있지만 JAVA8이상 버전에서는 enum이라는 훌륭한 수단을 제공 해준다.
우리는 스피링 시큐리티를 사용하므로 시큐리티에서 제공하는 권한 관련 프로세스를 활용해서 enum을 사용하도록 한다.
enum 클래스로 Role을 먼저 정의해보자.
여기서 주의할 점은 롤 클래스에 key에 해당하는 값의 명명 규칙이다.
스프링 시큐리티에서는 권한 코드를 작성할때 "ROLE_"가 필요하다.
이제 사용자 도메인을 정의하도록 하자.
다른 부분은 일전의 내용과 별다른 차이가 없지만 Role을 선언한 부분은 조금 특별하다.
@Enumerated(EnumType.STRING)라는 어노테이션을 간단히 살펴보면
enum 클래스를 사용하기 위해 선언하는 어노테이션인데 중요한 포인트는 바로
그 뒤에 선언한 EnumType 속성이다.
이 속성은 해당 컬럼필드를 DB에 저장할때 저장되는 값에 대한 방식을 결정하는 요소다.
일반적으로는 String을 쓰도록 권장하고 있다.
그 이유는 ORDINAL로 사용해서 순번으로 저장했을 경우, enum에서 추가적인 내용이 순번 기준 앞으로 생성될 경우, 기존 순번자체가 깨지는 문제가 생긴다.
이러한 문제를 방지하기 위해 일반적으로 String형을 사용할 것을 강력하게 권고하고 있다.
소셜로그인시 반환되는 값중에 email을 가지고 신규사용자인지를 구분하기 위해 사용자테이블에서 데이터를 검색한다.
여기서 조금 신기한 부분이 하나있는데,
어떻게 interface에서 별도의 구현체가 없이 사용이 가능한가이다.
답은 JPA에 있다.
JPA는 JpaRepository 상속 선언시 사용되는 도메인안에서 필드값을 가지고 정해진 메소드 명명 규칙에 의거해서 메소드를 만들면 자체적으로 이를 구현해서 사용한다.
findBy이후 email을 명시하면 도메인에서 email 필드가 있는 경우 findById처럼 email을 대상으로 검색하는 메소드 역할이 수행되는 것이다.
그리고 이를 Optional로 반환한다.
이제 스프링시큐리티 의존성을 bundle.gradle에 선언해야 한다.
시큐리티에 관련된 코드를 이제 작성하기 위해 패키지를 새로 선언한다.
크게 config 설정과 여기서 호출될 userSerivce의 구현으로 구분 할 수 있다.
WebSecurityConfigurerAdapter 를 상속 받아서 configure를 override 하도록 한다.
여기서 마지막에 선언된 userSerive에 호출되는 CustomOAuth2UserService를 구현해야 한다.
소셜 로그인이 성공적으로 진행되서 반환값을 받으면 SNS 별로 OAuth정보를 담을 Dto를 생성한다.
OAuth 정보가 담긴 Dto에서 email을 키값으로 사용자 테이블을 조회하여 일치하는 사용자를 찾아 사용자명, 사진 컬럼을 갱신한다.
만약 일치하는 사용자가 없으면 Guest등급의 신규 사용자를 생성하도록 한다.
조회된 사용자 정보를 sessionUser라는 Dto에 담아 새로운 session을 생성한다.
그리고 DefaultOAuth2User 클래스로 Role의 key값, 사용자 정보, 사용자 로그인진행 Pk값을 담아 반환한다.
로그인 구분코드, 로그인 키값, 사용자 정보를 담는 Dto를 작성한다.
이때 접속하는 SNS별로 객체를 생성할 메소드를 만들 것이다.
우선 구글용 메소드를 작성했다.
구글은 구분코드를 사용하지 않지만 추후에 네이버를 추가할때 사용할 예정이다.
session에 사용자 정보를 적재하기 위한 별도의 Dto를 선언한다.
만약 User를 사용한다면 어떻게 될까?
직렬화(Serialize)란,
자바 시스템 내부에서 사용되는 Object 또는 Data를 외부의 자바 시스템에서도 사용할 수 있도록 byte 형태로 데이터를 변환하는 기술을 말하는데 도메인에 Serializable를 implements 한 다음, ObjectOutputStream 등을 사용해서 구현하면 된다.
이제 화면 호출시 로그인 정보를 보내기 위해 컨트롤러를 수정하자.
HttpSession을 선언하고 시큐리티에서 생성해준 session 정보를 꺼내서
값이 있을 경우, Model에 추가하여 화면으로 보내도록 한다.
이제 화면에 로그인 버튼을 추가하고 로그인이 성공하면 사용자 명을 노출하도록 코드를 추가하자.
이제 http://localhost:8080/에 접속해서 로그인을 시도해보자.
로그인은 성공했는데 출력되는 사용자 명이 이상하다.
컨트롤러에서 log를 찍어보았다.
분명히 내 이름이 정확히 가져와졌다.
그럼에도 왜 이런 문제가 생기는건지 알아보기 위해 또 구글선생님에게 찾아갔다.
그리고 이동욱님 깃허브에 질문글들 중에서 원인을 발견한 분의 리포트를 발견했다.
https://github.com/jojoldu/freelec-springboot2-webservice/issues/293
내용은 어플리케이션 안에서 선언한 userName과 윈도우의 환경변수 %USERNAME%이 혼용되어 발생한 문제로 보인다고 한다.
내 윈도우10 사용자명이 kdh인것을 보니 동일한 증상으로 보인다.
급하게 Model과 화면에서 선언한 변수명을 loginUserName으로 변경해보았다.
정상적으로 로그인이 되었고 이름이 출력되고 있는 것을 확인했다.
이제 로그인이 성공했으니 글쓰기를 시도해보자.
현재 내가 로그인한 계정은 롤값이 GUEST이다.
일전에 SecurityConfig에서 /api/v1/이하의 모든 url에 대해서는 USER여야만 사용이 가능하도록 설정했었다.
그러므로 작성을 시도한다면 에러가 발생해야 한다.
정상적으로 권한문제로 인헤 status:403 으로 에러가 반환되었다.
테스트를 진행하기 위해 DB에서 ROLE 컬럼값을 'USER'로 수정해주자.
로그아웃을 해준 뒤 다시 로그인을 한다음 글을 작성해보자.
글작성에 성공했다.
스프링시큐리티를 적극적으로 써보는건 오랫만이였다.
기존 SI를 할때는 주로 이미 제공되는 프레임워크안에 로그인 모듈을 재활용해야 하는 경우가 많아서 이정도로 직접적인 구현을 해보는건 오랫만이였던 것 같아서 만들면서 흥미 진진했다.
구조는 비교적 단순한 편이였지만 머스태치의 윈도우 환경변수 참조 버그때문에
엉뚱한 부분에서 시간을 많이 투자했던 것 같다.
다음 시간은 현재의 초기 코드를 개선하는 리팩토링 과정을 다뤄볼 예정이다.