Notice
Recent Posts
Recent Comments
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- LastModified
- Spring
- Kotlin
- 이렇게살아야되나자괴감이
- 지뢰찾기
- 클래스레벨밸리데이션
- HTTP
- 코드스피츠
- jsr380
- i18n
- 지수반등
- 브로틀리
- etag
- brotli
- 스프링
- kotliln
- 개미수열
- 알고리즘
- cache-control
- jsr303
- 리얼월드HTTP
- cross parameter
- 워드프레스
- 알게뭐냐
- 랜선아미안해
Archives
- Today
- Total
취미개발 블로그와 마음수양
스프링 회원가입 페이스북 로그인 (2) - 로그인 본문
스프링 social -2
본래 글 링크 :
첫번째 파트에서 우리는 어떻게 설정하는지 알아보았다. 하지만 두 가지 중요한 궁금증이 아직 남아있다.
- 유저 계정을 어떻게 만들 것인가.
- 어떻게 로그인 할 것인가?
우리의 예제프로그램의 요구사항은 다음과 같다.
"전통적으로" 사용자 계정을 만드는 것이 가능해야 하며, 사용자는 사용자이름과 패스워드로 인증되어야 한다.
페이스북이나 트위터같은 SaaS API provider 를 이용해서 사용자 생성이 가능해야 한다. 이러한 경우 사용자는 SaaS API Provider 를 통해서 인증된다.
사용자이름과 비밀번호를 통해서 그리고 SaaS API Provider를 통해서 로그인되야 한다.
다음과 같은 요구사항을 이행해보자. 첫번째로 해야할것은 로그인 페이지를 만드는 것이다.
로그인 페이지 만들기
(여기서부터는 번역을 조금 대충하겠다. 좀 쉽고도 일상적인 부분이기 때문)
1. @controller 어노테이션이 붙은 LoginController 를 만든다.
2. showLoginPage() 메소드를 만들고, 렌더된 뷰페이지를 리턴한다.
3. showLoginPage() 메소드는 다음을 구현해야 한다. @ReqeustMapping('/login') 과 리턴으로 'user/login'을 리턴해야 한다.
로그인컨트롤러 소스코드는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 | import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller public class LoginController { @RequestMapping (value = "/login" , method = RequestMethod.GET) public String showLoginPage() { return "user/login" ; } } |
다음은 JSP 페이지만들기과정이다.
JSP페이지 만들기
1. 로그인 폼과 소셜 로그인은 익명 유저에게만 보여져야 한다. 우리는 다음과정을 통해 이를 처리한다.
2. 로그인이 실패하였을 때 에러메시지를 내보내야 한다. 만약 에러의 리퀘스트 파라미터가 bad_credentials 일 때 스프링태그 라이브러리를 이용하여서 지역화된 에러를 보여줘야 한다.
3. 로그인폼의 구현은 다음 과정을 거친다.
- 로그인폼이 서브밋되면 POST 리퀘스트가 /login/authenticate 로 보내진다.
- 유저네임 필드가 로그인폼에 추가된다.
- 패스워드필드가 로그인 폼에 추가된다.
- 서브밋 버튼이 로그인 폼에 추가된다.
4. 사용자계정생성 링크가 로그인폼에 생성된다. 이 링크는 GET 요청을 /user/register 에 보낸다.
5. 소셜 등록 버튼이 로그인 페이지에 다음과 같은 과정을 거쳐서 추가된다.
- 페이스북 로그인 버튼이 추가되면서 /auth/facebook 에 대한 GET요청을 만든다.
- 트위터 버튼이 추가되면서 /auth/twitter 에 대한 Get 요청이 추가된다.
6. 만약 인증된 사용자가 로그인페이지에 접근하면 도움 메시지가 보여지게 되는데, 이같은 과정을 거친다.
login.jsp 의 소스코드는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | <! DOCTYPE html> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> < html > < head > < title ></ title > < link rel = "stylesheet" type = "text/css" href = "${pageContext.request.contextPath}/static/css/social-buttons-3.css" /> </ head > < body > < div class = "page-header" > < h1 >< spring:message code = "label.user.login.page.title" /></ h1 > </ div > <!-- If the user is anonymous (not logged in), show the login form and social sign in buttons. --> < sec:authorize access = "isAnonymous()" > <!-- Login form --> < div class = "panel panel-default" > < div class = "panel-body" > < h2 >< spring:message code = "label.login.form.title" /></ h2 > <!-- Error message is shown if login fails. --> < c:if test = "${param.error eq 'bad_credentials'}" > < div class = "alert alert-danger alert-dismissable" > < button type = "button" class = "close" data-dismiss = "alert" aria-hidden = "true" >×</ button > < spring:message code = "text.login.page.login.failed.error" /> </ div > </ c:if > <!-- Specifies action and HTTP method --> < form action = "${pageContext.request.contextPath}/login/authenticate" method = "POST" role = "form" > <!-- Add CSRF token --> < input type = "hidden" name = "${_csrf.parameterName}" value = "${_csrf.token}" /> < div class = "row" > < div id = "form-group-email" class = "form-group col-lg-4" > < label class = "control-label" for = "user-email" >< spring:message code = "label.user.email" />:</ label > <!-- Add username field to the login form --> < input id = "user-email" name = "username" type = "text" class = "form-control" /> </ div > </ div > < div class = "row" > < div id = "form-group-password" class = "form-group col-lg-4" > < label class = "control-label" for = "user-password" >< spring:message code = "label.user.password" />:</ label > <!-- Add password field to the login form --> < input id = "user-password" name = "password" type = "password" class = "form-control" /> </ div > </ div > < div class = "row" > < div class = "form-group col-lg-4" > <!-- Add submit button --> < button type = "submit" class = "btn btn-default" >< spring:message code = "label.user.login.submit.button" /></ button > </ div > </ div > </ form > < div class = "row" > < div class = "form-group col-lg-4" > <!-- Add create user account link --> < a href = "${pageContext.request.contextPath}/user/register" >< spring:message code = "label.navigation.registration.link" /></ a > </ div > </ div > </ div > </ div > <!-- Social Sign In Buttons --> < div class = "panel panel-default" > < div class = "panel-body" > < h2 >< spring:message code = "label.social.sign.in.title" /></ h2 > < div class = "row social-button-row" > < div class = "col-lg-4" > <!-- Add Facebook sign in button --> < a href = "${pageContext.request.contextPath}/auth/facebook" >< button class = "btn btn-facebook" >< i class = "icon-facebook" ></ i > | < spring:message code = "label.facebook.sign.in.button" /></ button ></ a > </ div > </ div > < div class = "row social-button-row" > < div class = "col-lg-4" > <!-- Add Twitter sign in Button --> < a href = "${pageContext.request.contextPath}/auth/twitter" >< button class = "btn btn-twitter" >< i class = "icon-twitter" ></ i > | < spring:message code = "label.twitter.sign.in.button" /></ button ></ a > </ div > </ div > </ div > </ div > </ sec:authorize > <!-- If the user is already authenticated, show a help message instead of the login form and social sign in buttons. --> < sec:authorize access = "isAuthenticated()" > < p >< spring:message code = "text.login.page.authenticated.user.help" /></ p > </ sec:authorize > </ body > </ html > |
우리의 다음 단계는 회원등록 기능을 구현하는 것이다.시작해보자!
등록 기능 구현
등록기능은 두가지의 요구사항을 가진다.
1. 보통의 유저 계정을 생성할 수 있어야 하며
2. 소셜 등록을 할 수 있어야 한다.
또한 어플리케이션 콘텍스트 설정은 한가지 등록기능의 요구사항을 가지는데,
등록페이지의 url 은 '/signup' 이어야 한다. 이것은 회원등록 페이지의 기본 값이며 자바설정을 통해서 어플리케이션콘텍스트를 설정함으로써 오버라이딩하는 것이 가능하다. '/signup' 은 조금 주소가 좋지않으므로, 이것을 /user/register 로 바꿔보도록 하겠다.
Note: XML 로도 설정을 바꾸는 것이 가능하며 property 는 signUpUrl 로 불린다.
예제프로그램의 사용자는 다음의 메소드중의 하나를 사용함으로써 등록 페이지에 갈 수 있다.
1. CreateUserAccountLink 를 누름으로써. 이 링크는 보통의 등록 절차를
2. 소셜등록흐름을 시작하는 소셜버튼을 클릭함으로써
이런 간단한 설명으로 일반적으로 이해를 하기가 힘드므로, 사용자가 우리의 예제프로그램에서 결국 회원등록 페이지에 도달하는 단계를 설명하는 다이어그램을 만들어보았다. 다이어그램은 두가지 규칙을 가진다.
1. 회색은 우리 예제프로그램의 액션을 나타내고
2. 파랑색은 SaaS API Provider 의 행동을 나타낸다.
등록폼을 위한 폼 오브젝트를 만들어보자.
폼오브젝트 생성
폼 오브젝트는 데이터 전송 오브젝트(DTO) 이며, 등록폼에 들어갈 정보들과 폼인증 제약사항을 담고 있다.
폼 오브젝트를 구현하기 전에 우리의 폼오브젝트에 쓰일 폼 인증 제약을 한번 살펴보자. 제약은 다음에 설명되어있다. (간단히 생략)
폼 오브젝트를 생성해보자. 우리는 다음 과정을 거쳐서 할 수 있다.
1. RegistrationForm 을 생성한다.
2. 이메일 필드를 클래스에 추가하고, 이메일은 이메일 형식에 맞아야하며, 비어있거나, 널값이 아니며, 최대 100글자여야 한다.
3. firstName 필드를 클래스에 추가하고 널값아니고, 100글자 아래로 만든다.
4. lastName 필드추가, Not null, 100글자 이하
5. password 필드 추가.
6. passwordVerification 필드 추가
7. signInProvider 필드를 클래스에 추가하고, 타입값은 SocialMediaService 타입값이다.
8. isNormalRegistration() 메소드를 추가하고, 이 메소드는 signInProvider 필드값이 널값일때, true 를 리턴한다. 널이 아닐때는 false 리턴.
9. isSocialSignIn() 메소드를 추가하고, 이 메소드는 signInProvider 필드값이 널값이 아닐 때, true 를 리턴한다. 널일 때는 false 리턴.
RegistrationForm 클래스의 소스코드는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.NotEmpty; import javax.validation.constraints.Size; @PasswordsNotEmpty ( triggerFieldName = "signInProvider" , passwordFieldName = "password" , passwordVerificationFieldName = "passwordVerification" ) @PasswordsNotEqual ( passwordFieldName = "password" , passwordVerificationFieldName = "passwordVerification" ) public class RegistrationForm { @Email @NotEmpty @Size (max = 100 ) private String email; @NotEmpty @Size (max = 100 ) private String firstName; @NotEmpty @Size (max = 100 ) private String lastName; private String password; private String passwordVerification; private SocialMediaService signInProvider; //Constructor is omitted for the of clarity. public boolean isNormalRegistration() { return signInProvider == null ; } public boolean isSocialSignIn() { return signInProvider != null ; } //other methods are omitted for the sake of clarity. } |
SocialMediaService 는 SaaS API Provider를 명시하는 이늄이며, 사용자를 인증하는데 사용된다. 코드는 다음과 같다.
1 2 3 4 | public enum SocialMediaService { FACEBOOK, TWITTER } |
그런데, 뭔가 잊은 것같은데?!
이상한 어노테이션이 있는데 @PasswordsNotEqual 와 @PasswordsNotEmpty 이다.
음. 우리는 커스텀 빈 인증 제약을 만들 것이다. 어떻게 이러한 제약들을 만들어내는지 알아보자.
커스텀 인증 제약 만들기
두 개의 커스텀 인증 제약을 만들어 볼것이다. 만약 유저가 평범한 유저 계정을 만들면 우리는
1. password 와passwordVerification 필드가 널값이 아니다.
2. 저 두 필드가 같아야 한다는 것을 보장해야 한다.
커스텀 인증 제약은 다음단계로 만들어 진다.
제약 어노테이션을 만들고
제약이 깨지지 않는 것을 보장하는 커스텀 인증 클래스를 만든다.
=========
다음 링크에서 커스텀 빈 제약 에 대한 정보를 볼 수 있다 한다.
The reference manual of Hibernate validator 4.2 has more information about creating custom validation constraints.
=========
그럼~제약 어노테이션을 만들어보자.
제약 어노테이션 만들기
다음 단계를 거친다.
======
이 섹션은 어떻게 필요한 제약어노테이션을 만드는지 설명하지만 굳이 여기의 CustomConstraint 어노테이션을 만들 필요는 없다.여기선 단지 어떻게 커스텀 빈제약 어노테이션을 만드는 지 설명하기 위해서 예제로 쓸 뿐이고 실제 예제 프로그램에서는 사용되지 않는다.
======
1. 어노테이션 타입을 만드는데, 어노테이션의 이름은 CommonConstraint이다.
2. @Target annotation 을 붙이고, 값을 {ElementType.TYPE, ElementType.ANNOTATION_TYPE} (the Javadoc of the ElementTypeenum)로 붙인다. 이것은 클래스와 어노테이션 둘다 @CommonConstraint 로 어노테이션 될 수 있다는 것을 의미한다.
3. @Retention 어노테이션을 붙이고 값을 RetentionPolicy.RUNTIME로 한다. 이것은 리플렉션을 이용해런타임환경에서 사용되고 읽혀질 수 있다는 것을 의미한다.
6. message 속성. 타입을 String 으로 넣어주고 기본 값은 "CommonConstraint" 로 해준다.
8. payload 속성을 어노테이션 타입에 더해준다. 이 속성의 타입은 Class<? extends Payload> 의 배열이 될 것이다. 기본 값은 빈 배열이다. 이 속성은 빈인증API 에 사용되지는 않을 것이지만, 클라이언트API는 이 제약에 커스텀 PayLoad 객체를 할당할 것이다.
@CommonConstraint소스코드는 다음과 같다. ( 예제파일에 들어가지 않는다)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; @Target ( { TYPE, ANNOTATION_TYPE }) @Retention (RUNTIME) @Constraint (validatedBy = CommonConstraintValidator. class ) @Documented public @interface CommonConstraint { String message() default "CommonConstraint" ; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } |
이제 어떻게 @PasswordsNotEmpty와 @PasswordNotEqual를 만들이 알아보자.
먼저 @PasswordsNotEmpty을만들어보자. 우리는 다음단계로 진행한다.
1.먼저 앞서 설명했듯이 다음단계를 거친다. PasswordsNotEmpty 어노테이션을 만들고 @Constraint어노테이션을 붙인 뒤에 validatedBy 속성에 PasswordNotEmptyValidator.class 을 적는다.
2. triggerFieldName 속성을 String 타입으로 만들고 기본값은 빈 문자열이다. 이 속성은 값이 널값일때 커스텀제약이 적용될 필드 네임을 명시한다.
3. String 타입으로 passwordFieldName 속성을 만들고, 빈문자열을 만든다. 이 속성은 사용자의 비밀번호를 가질 필드 이름을 명시한다.
4. String 타입으로 passwordVerificationFieldName 을 만들고, 비밀번호 확인필드 이름을 명시한다.
@PasswordsNotEmpty소스는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; @Target ( { TYPE, ANNOTATION_TYPE }) @Retention (RUNTIME) @Constraint (validatedBy = PasswordsNotEmptyValidator. class ) @Documented public @interface PasswordsNotEmpty { String message() default "PasswordsNotEmpty" ; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; String triggerFieldName() default "" ; String passwordFieldName() default "" ; String passwordVerificationFieldName() default "" ; } |
다음으로 우리는 @PasswordsNotEqual 어노테이션을 만들 것이다. 다음 단계로 만든다.
1. 앞서 설명과 비슷하게 PasswordsNotEqual.로 만들고 @Constraint 의 validatedBy 속성의 값을 PasswordsNotEqualsValidaotr.class로 만들어준다.
2. passwordFieldName 과 passwordVerificationFieldName을 앞서와 비슷하게만들어 준다.
(역자. 자의적으로 생략함. 소스 코드 참조)
@PasswordNotEqual 소스코드는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target ( { TYPE, ANNOTATION_TYPE }) @Retention (RUNTIME) @Constraint (validatedBy = PasswordsNotEqualValidator. class ) @Documented public @interface PasswordsNotEqual { String message() default "PasswordsNotEqual" ; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; String passwordFieldName() default "" ; String passwordVerificationFieldName() default "" ; } |
우리는 이제 제약 어노테이션을 만들었으니 인증클래스들을 구현할 유틸리티 클래스들을 한번 살펴보자.
인증 유틸리티 클래스 생성
인증 유틸리티 클래스는 두가지 정적 메소드를 가지며 다음과 같다.
첫번째 메소드는 인증되는 객체의 필드에 인증 에러를 넣어주기 위해 사용되고,
두번째 메소드는 요청필드의 값을 리턴하기 위해 사용된다.
다음 단계로 만들수 있다.
1. ValidatorUtil 클래스를 만든다.
2. addValidationError() 메소드를 클래스에 넣고서, 이 메소드는 두개의 파라미터를 받는다. 첫번째는 필드 네임이고, 두번째는 ConstraintValidatorContext 객체이다.
3. 다음 단계에 따라서 addValidationError() 을구현한다. 첫번째로, 새로운 제약사항을 만들고 제약 위반 메시지가 빌드될때 제약 어노테이션에 의해 명시된 메시지가 prefix로 사용되게 한다.
두번째로, 제약 인증 에러에 필드를 넣는다. 세번째로 인증 제약 에러를 만든다.
4. ValidatorUtil 클래스에 getFieldValue()를넣는다. 이 메소드는 구체화된 필드값을 반환하며, 다음단계에 설명된 두개의 파라미터를 받는다.
첫번째는 요청된 객체를 가지고 있는 객체이며, 두번째 파라미터는 요청된 필드의 이름이다.
5. getFieldValue() 메소드를 다음 단계로 구현한다.
첫번째로 요청필드를 리플렉트하는 필드 객체를 참조한다.
두번째로 필드가 private 이더라도 필드의 값에 접근에할 수 있는 것을 보장한다.
세번째로 필드값을 반환한다.
ValidatorUtil 클래스의 값은 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import javax.validation.ConstraintValidatorContext; import java.lang.reflect.Field; public class ValidatorUtil { public static void addValidationError(String field, ConstraintValidatorContext context) { context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()) .addNode(field) .addConstraintViolation(); } public static Object getFieldValue(Object object, String fieldName) throws NoSuchFieldException, IllegalAccessException { Field f = object.getClass().getDeclaredField(fieldName); f.setAccessible( true ); return f.get(object); } } |
우리는 이제 우리의 validator 클래스를 구현하러 가보겠다.
validator 클래스 만들기
먼저 validator 클래스를 만든다. 이것은 @PasswordNotEmpy 어노테이션이 붙은 클래스들을 인증할 것이다. 다음단계로 진행한다.
1. PasswordsNotEmptyValidator 클래스를 만들고, ConstraintValidator 인터페이스를 구현한다. ConstraintValidator 인터페이스는 두가지의 파라미터를 정의한다. 첫번째는 어노테이션 타입인데 값은 PasswordNotEmpty 로 하고 두번째 파라미터는 validator 에 검사될 요소의 타입이다. 이 파라미터의 타입은 Object 타입이다.
2. private String validationTriggerFieldName 추가
3. private String passwordFieldName 필드 추가
4. private String passwordVerificationFieldName 필드 추가
5. ConstraintValidator인터페이스 의 initialize(PasswordsNotEmpty constraintAnnotation) 메소드 를 validator 클래스에 추가하고 다음단계로 구현한다.
첫번째로 validationTriggerFieldName 의 값을 정하고, passwordFieldName 의 값을 정하고, passwordVerificationFieldName의 값을 정한다.
첫번째로 validationTriggerFieldName 의 값을 정하고, passwordFieldName 의 값을 정하고, passwordVerificationFieldName의 값을 정한다.
6. 생성한 클래스에 private isNullOrEmpty(String field) 를 추가한다. 이 메소드는 메소드 파라미터로 주어진 문자열이 널값일 때 true 를 리턴한다. 그렇지 않으면 false 리턴
7. private passwordAreValid(Object value, ConstraintValidatorContext context)메소드를 생성된 클래스에 넣어라. 이 메소드는 비밀번호필드가 유효하면 true 를 리턴하고 그렇지 않은 경우 false 를 리턴한다. 이 메소드는 두개의 메소드 파라미터를 받고, 다음과 같이 설명된다.
8. passwordsAreValid() 메소드를 다음 단계에 따라 구현한다.
첫번째로, ValidatorUtil 클래스의 getFieldValue() 메소드를 호출함으로써 password 필드의 값을 얻는다. 메소드 파라미터로서 validated객체와 패스워드 필드의 이름을 전달한다.
두번째로, 만약 패스워드 필드가 값이 널값이면, ValidatorUtil 클래스의 addValidationError() 메소드를 호출함으로써 validation 에러를 추가한다. 메소드 파라미터로서 ConstraintValidatorContextObject 와 패스워드필드의 이름을 전달한다.
세번째로 ValidatorUtil 클래스의 getFieldVlaue() 메소드를 호출함으로써 passwordVerifiaction필드의 값을 받는다. 메소드 파라미터로서 validated 객체와 password verification 필드의 이름을 전달한다.
네번째로, 만약 패스워드값 확인 필드가 널값이면 ...(역자 주.. 두번째와 비슷함)
다섯번째로 만약 검증에러가 발생하면 false 리턴하고 아니면 true 리턴한다.
9. ConstraintValidator 인터페이스의 isValid(Object value, ConstraintValidatorContext context) 메소드를 validator 클래스에 추가하고 다음단계로 구현한다.
첫번째로, ConstraintValidatorContext인터페이스의 disableDefaultConstraintViolation() 메소드를 호출함으로써 기본 에러메시지를 비활성화시킨다.
두번째로, 메소드에서의 try-catch 구조를 추가하고 모든 체크예외를 잡아라. 만약 체크예외가 발생하면 그것을 런타임 익셉션으로 포장하라. 이것은 ConstraintValidator 인터페이스의 isValid() 가 체크예외를 발생시키지 못하기 때문에 필요하다. try 블록을 다음과 같이 구현하라.
먼저 ValidatorUtil클래스의 getFieldValue() 메소드를 호출함으로써 validation trigger 필드의 값을 얻는다. 메소드 파라미터로서, validated 객체와 validation trigger 필드를 전달하라.
그리고 만약 validation Trigger 필드 값이 널값이면 passwordFieldsAreValid() 메소드를 호출하고 메소드 파라미터로서 validated 객체와 ConstraintValidatorContext객체를 전달하라. 이 메소드에 의해서 boolean 값이 반환된다.
validationTrigger 필드가 널값이 아니라면 true 가 리턴된다.
PasswordNotEmpty Validator 클래스는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class PasswordsNotEmptyValidator implements ConstraintValidator<PasswordsNotEmpty, Object> { private String validationTriggerFieldName; private String passwordFieldName; private String passwordVerificationFieldName; @Override public void initialize(PasswordsNotEmpty constraintAnnotation) { validationTriggerFieldName = constraintAnnotation.triggerFieldName(); passwordFieldName = constraintAnnotation.passwordFieldName(); passwordVerificationFieldName = constraintAnnotation.passwordVerificationFieldName(); } @Override public boolean isValid(Object value, ConstraintValidatorContext context) { context.disableDefaultConstraintViolation(); try { Object validationTrigger = ValidatorUtil.getFieldValue(value, validationTriggerFieldName); if (validationTrigger == null ) { return passwordFieldsAreValid(value, context); } } catch (Exception ex) { throw new RuntimeException( "Exception occurred during validation" , ex); } return true ; } private boolean passwordFieldsAreValid(Object value, ConstraintValidatorContext context) throws NoSuchFieldException, IllegalAccessException { boolean passwordWordFieldsAreValid = true ; String password = (String) ValidatorUtil.getFieldValue(value, passwordFieldName); if (isNullOrEmpty(password)) { ValidatorUtil.addValidationError(passwordFieldName, context); passwordWordFieldsAreValid = false ; } String passwordVerification = (String) ValidatorUtil.getFieldValue(value, passwordVerificationFieldName); if (isNullOrEmpty(passwordVerification)) { ValidatorUtil.addValidationError(passwordVerificationFieldName, context); passwordWordFieldsAreValid = false ; } return passwordWordFieldsAreValid; } private boolean isNullOrEmpty(String field) { return field == null || field.trim().isEmpty(); } } |
두번째로, 우리는 @PasswordNotEqual 어노테이션이 붙은 클래스들의 유효성을 검사하는 validator 클래스를 만들어야 한다. 다음 단계로 할 수 있다.
1. PasswordNotEqualValidator 클래스를 만들고 ConstraintValidator인터페이스를 구현한다. ConstraintValidator인터페이스는 다음의 두가지 타입의 파라미터를 정의한다. 첫번째 파라미터는 annotation 타입으로 파라미터 값은 PasswordsNotEqual 이다. 두번째 파라미터 값은 validator 에 의해 검증될 요소의 타입이다. 타입을 Object로 한다(역자:생략)
2. private String passwordFieldName 필드 추가
3. private Strign passwordVerificationFieldName 필드 추가
4. ConstraintValidator 인터페이스의 initialize(PasswordsNotEqual constraintAnnotation)메소드를 validator 클래스에 추가하고 다음 단계로 구현한다. passwordFieldName 필드의 값을 정하고, passwordVerificationFieldName 필드의 값을 정한다.
5. 생성된 클래스에 private passwordsAreNotEqual(String password, String passwordVerification) 메소드를 추가한다. 메소듸 파라미터로 주어진 패스워드확인필드와 패스워드 필드가 다르면, 이 메소드는 true 를 리턴하고 아닌 경우 false 를 리턴한다.
6. ConstraintValidator 인터페이스의 isValid(Object value, ConstraintValidatorContext context) method를 validator 클래스에 추가하고 다음 단계로 구현한다.
(바로 윗단계에서 9번에 해당하는 내용이므로 생략. 역자주)
PasswordNotEqualsValidator 클래스는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class PasswordsNotEqualValidator implements ConstraintValidator<PasswordsNotEqual, Object> { private String passwordFieldName; private String passwordVerificationFieldName; @Override public void initialize(PasswordsNotEqual constraintAnnotation) { this .passwordFieldName = constraintAnnotation.passwordFieldName(); this .passwordVerificationFieldName = constraintAnnotation.passwordVerificationFieldName(); } @Override public boolean isValid(Object value, ConstraintValidatorContext context) { context.disableDefaultConstraintViolation(); try { String password = (String) ValidatorUtil.getFieldValue(value, passwordFieldName); String passwordVerification = (String) ValidatorUtil.getFieldValue(value, passwordVerificationFieldName); if (passwordsAreNotEqual(password, passwordVerification)) { ValidatorUtil.addValidationError(passwordFieldName, context); ValidatorUtil.addValidationError(passwordVerificationFieldName, context); return false ; } } catch (Exception ex) { throw new RuntimeException( "Exception occurred during validation" , ex); } return true ; } private boolean passwordsAreNotEqual(String password, String passwordVerification) { return !(password == null ? passwordVerification == null : password.equals(passwordVerification)); } } |
우리는 이제 커스텀 인증 제약을 만들었다. 등록 페이지를 어떻게 만들 것인지 알아보자.
등록 페이지 만들기
등록페이지의 요구사항은 다음과 같다.
등록페이지 url 은 다음과 같다. = /user/register
보통의 유저를 만들 때, 우리의 프로그램은 빈 등록 폼을 만든다.
사용자가 소셜 등록을 할 때, SaaS API provider 가 제공하는 정보는 사용자 등록 폼의 필드들을 미리 만들어내는 데 사용된다.
사용자를 등록페이지로 리다이렉터링
우리가 등록페이지로의 전환시키는 컨트롤러 메소드를 만들기 전에 우리는 사용자를 정확한 url로 리다이렉트할 컨트롤러를 구현해야 한다. 요구사항은 다음고 ㅏ같다.
Get 요청은 /signup 이고 /user/register 로 리다이렉트 되야 한다.
====
XML 설정도 가능하다는 내용 나옴(역자 주 )
If you are configuring the application context of your application by using XML configuration files, you can skip this step. This step is required only if you configure the application context of your application by using Java configuration. The reason for this is that at the moment you can configure the sign up url only if you are using XML configuration (Search for a property called signUpUrl).
====
우리는 다음 단계로 구현 가능하다.
SingUpController 를 만들고 @Controller 어노테이션을 붙인다.
String 을 반환하는 redirectRequestToRegistrationPage() 메소드를 만든다.
redirectRequestToRegistrationPage() 메소드는 다음 단계로 구현한다. (역자 주 : 생략한다. 그냥 소스코드 보면 알듯;;)
SignUpController 소스 코드는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 | import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller public class SignUpController { @RequestMapping (value = "/signup" , method = RequestMethod.GET) public String redirectRequestToRegistrationPage() { return "redirect:/user/register" ; } } |
다음으로 넘어가서 어떻게 등록페이지를 만드는 컨트롤러 메소드를 구현하는지 알아보자.
컨트롤러 메소드 구현
등록 페이지를 렌더링하는 컨트로러 메소드는 한가지 중요한 책임을 갖는다..
그것은 폼 오브젝트와 미리 만들어진 필드를 만든다. 사용자가 보통 유저계정을 만들 때, 컨틀로러 메소드는 빈(empty) 객체 오브젝트를 만들며 반면에 사용자가 소셜기능으로 사용자 계정을 만들면, 이 컨트롤러는 폼오브젝트의 필드값을 SaaS API Provider 가 제공하는 정보를 사용하여 세팅한다.
우리는 다음 단계를 통해서 등록 페이지를 만드는 컨트롤러를 구현할 수 있다.
1. @Controller 어노테이션 구현
2. @SessionAttributes 어노테이션을 세팅하고 값을 user 로 세팅한다. 우리는 이 어노테이션을 user라 불리는 모델 속성이 세션에 저장되도록 보장하는데 사용한다.
3. private createRegistrationDTO() 메소드를 클래스에 추가 한다. 이 메소드는 메소드 파라미터로서 Connection 객체를 받고 RegistrationForm 객체를 반납한다. 우리는 다음 단계를 통해 이를 구현한다.
첫째로 새로운 RegistrationForm 객체를 만든다.
둘째로 만약 메소드 파라미터로써 주어진 Connection 객체가 null 값이 아니면 이 사용자는 소셜 등록을 통해 새로운 사용자계정을 만든다. 이러한 경우 우리는 fetchUserProfile() method of the Connection 클래스를 호출함으로써 UserProfile 를 얻는다. 폼오브젝트에 email, first name, last name 값을 설정하고 UserProfile 클래스의 메소드를 호출함으로써 정보를 얻을 수 있다.
Connection 클래스의 getKey()메소드를 호출함으로써, ConnectionKey 객체를 얻을 수 있다. 이 객체는 소셜등록 provider 의 아이디와 provider 의 구체적인 user id 가 들어가있다.
다음 단계를 통해 form 객체에 대한 sign in provider 를 정할 수 있다. ConnectionKey 클래스의 getProviderId() 메소드를 호출함으로써 sign in provider 를 얻고, getProviderId() 메소드에 의해 리턴된 문자열을 대문자로 변환한다. nameOf() 메소드에 의해 호출된 SocialMediaService 이늄의 값을 얻고, 메소드 파라미터로 sign in Provider 로 전달한다. 리턴된 값을 폼 오브젝트에 세팅한다..
폼 오브젝트를 리턴한다.
4. 등록페이지를 렌더링하는 컨트롤러는 showRegistrationForm() 을 호출한다. 이 메소드를 컨틀롤러에 추가하고, 다음 단계로 구현한다.
@RequestMapping 어노테이션을 추가하고, get 요청의 /user/register 를 처리할 수 있게 한다.
WebRequest 객체를 메소드 파라미터에 전달해서 요청메타데이터에 쉬운 접근을 할 수 있도록 한다.
Model 객체를 메소드 파라미터로 전달한다.
ProviderSignInUtils클래스 의 getConnection() 정적메소드를 호출해서 코넥션을 받아오고 메소드 파라미터로 WebRequest객체를 전달한다.이 메소드는 WebRequest 객체가 SaaS API provider 메타데이터를 가지고 있지 않으면(사용자가 일반 계정을 생성하면) null 값을 리턴한다. 만약 메타데이터가 발견되면, 이 메소드는 코넥션 객체를 그 정보를 이용하여 생성하고 생성된 객체를 리턴한다.
private createRegistronDTO() 메소드를 호출하여서 폼 객체를 받고 메소드 파라미터로 Connection 객체를 전달한다.
폼오브젝트의 모델을 user라 불리우는 모델속성으로 세팅한다.
/user/registrationForm 으로 등록폼뷰의 이름을 리턴한다.
RegistrationController 클래스의 소스코드는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | import org.springframework.social.connect.Connection; import org.springframework.social.connect.ConnectionKey; import org.springframework.social.connect.UserProfile; import org.springframework.social.connect.web.ProviderSignInUtils; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.context.request.WebRequest; @Controller @SessionAttributes ( "user" ) public class RegistrationController { @RequestMapping (value = "/user/register" , method = RequestMethod.GET) public String showRegistrationForm(WebRequest request, Model model) { Connection<?> connection = ProviderSignInUtils.getConnection(request); RegistrationForm registration = createRegistrationDTO(connection); model.addAttribute( "user" , registration); return "user/registrationForm" ; } private RegistrationForm createRegistrationDTO(Connection<?> connection) { RegistrationForm dto = new RegistrationForm(); if (connection != null ) { UserProfile socialMediaProfile = connection.fetchUserProfile(); dto.setEmail(socialMediaProfile.getEmail()); dto.setFirstName(socialMediaProfile.getFirstName()); dto.setLastName(socialMediaProfile.getLastName()); ConnectionKey providerKey = connection.getKey(); dto.setSignInProvider(SocialMediaService.valueOf(providerKey.getProviderId().toUpperCase())); } return dto; } } |
다음 단계는 Jsp페이지를 만들 어 볼것이다. 어떻게 만드는지 한번 보자.
JSP Page 만들기
우리는 Jsp 페이지를 다음 단계로 만들 것이다
1. 등록폼은 오직 익명 사용자들에게만 보여져야 한다. 이렇게 하기 위해서 로그인폼과 소셜 로그인 버튼을 authorize tag of the Spring Security tag library.로 감싼다. 그리고 접근권한을 isAnonymous().로 한다.
2.다음 단계로 등록 폼을 구현한다.
2-1. 폼이 서브밋 되는 주소는 /user/register 이다.
2-3. 폼 객체에 sign in provider 가 발견되면, 히든필드로서 그것을 폼에 더하라.
2-4. 폼에 firstName필드를 추가하고, firstName 에 관한 인증 에러가 보여지도록 하라.
2-5. 위에말과 동일한데 lastName 으로만 바꿔주자.
2-6. 위에말과 동일한데 email 에 적용
2-7. 유저가 보통의 사용자 계정을 생성하면 다음과 같이 한다. 폼에 password 필드와 passwordVerification 필드를 추가하고 그 위의 말들과 동일..그리고 2-8서브밋 버튼을 폼에 넣어준다.
3. 인증된 유저가 등록 페이지에 접속시, 도움말이 뜨도록 한다. 우리는 다음과 같이 할 수 있다.
=================
폼 태그에 대한 레퍼런스는 여기있다고한다.
The Spring 3.2 reference manual has more information about the form tags of the Spring JSP tag library.
=================
registrationForm.jsp 의 소스코드는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | <! DOCTYPE html> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> < html > < head > < title ></ title > < script type = "text/javascript" src = "${pageContext.request.contextPath}/static/js/app/user.form.js" ></ script > </ head > < body > < div class = "page-header" > < h1 >< spring:message code = "label.user.registration.page.title" /></ h1 > </ div > <!-- If the user is anonymous (not logged in), show the registration form. --> < sec:authorize access = "isAnonymous()" > < div class = "panel panel-default" > < div class = "panel-body" > <!-- Ensure that when the form is submitted, a POST request is send to url '/user/register'. --> < form:form action = "${pageContext.request.contextPath}/user/register" commandName = "user" method = "POST" enctype = "utf8" role = "form" > <!-- Add CSRF token to the request. --> < input type = "hidden" name = "${_csrf.parameterName}" value = "${_csrf.token}" /> <!-- If the user is using social sign in, add the signInProvider as a hidden field. --> < c:if test = "${user.signInProvider != null}" > < form:hidden path = "signInProvider" /> </ c:if > < div class = "row" > < div id = "form-group-firstName" class = "form-group col-lg-4" > < label class = "control-label" for = "user-firstName" >< spring:message code = "label.user.firstName" />:</ label > <!-- Add the firstName field to the form and ensure that validation errors are shown. --> < form:input id = "user-firstName" path = "firstName" cssClass = "form-control" /> < form:errors id = "error-firstName" path = "firstName" cssClass = "help-block" /> </ div > </ div > < div class = "row" > < div id = "form-group-lastName" class = "form-group col-lg-4" > < label class = "control-label" for = "user-lastName" >< spring:message code = "label.user.lastName" />:</ label > <!-- Add the lastName field to the form and ensure that validation errors are shown. --> < form:input id = "user-lastName" path = "lastName" cssClass = "form-control" /> < form:errors id = "error-lastName" path = "lastName" cssClass = "help-block" /> </ div > </ div > < div class = "row" > < div id = "form-group-email" class = "form-group col-lg-4" > < label class = "control-label" for = "user-email" >< spring:message code = "label.user.email" />:</ label > <!-- Add the email field to the form and ensure that validation errors are shown. --> < form:input id = "user-email" path = "email" cssClass = "form-control" /> < form:errors id = "error-email" path = "email" cssClass = "help-block" /> </ div > </ div > <!-- If the user is creating a normal user account, add password fields to the form. --> < c:if test = "${user.signInProvider == null}" > < div class = "row" > < div id = "form-group-password" class = "form-group col-lg-4" > < label class = "control-label" for = "user-password" >< spring:message code = "label.user.password" />:</ label > <!-- Add the password field to the form and ensure that validation errors are shown. --> < form:password id = "user-password" path = "password" cssClass = "form-control" /> < form:errors id = "error-password" path = "password" cssClass = "help-block" /> </ div > </ div > < div class = "row" > < div id = "form-group-passwordVerification" class = "form-group col-lg-4" > < label class = "control-label" for = "user-passwordVerification" >< spring:message code = "label.user.passwordVerification" />:</ label > <!-- Add the passwordVerification field to the form and ensure that validation errors are shown. --> < form:password id = "user-passwordVerification" path = "passwordVerification" cssClass = "form-control" /> < form:errors id = "error-passwordVerification" path = "passwordVerification" cssClass = "help-block" /> </ div > </ div > </ c:if > <!-- Add the submit button to the form. --> < button type = "submit" class = "btn btn-default" >< spring:message code = "label.user.registration.submit.button" /></ button > </ form:form > </ div > </ div > </ sec:authorize > <!-- If the user is authenticated, show a help message instead of registration form. --> < sec:authorize access = "isAuthenticated()" > < p >< spring:message code = "text.registration.page.authenticated.user.help" /></ p > </ sec:authorize > </ body > </ html > |
어떻게 등록폼 제출의 처리가 이루어지는 지 알아보자.
등록폼의 제출 처리
우리의 다음 단계는 등록폼의 제출이다. 다음 단계로 구성해보자.
1. 등록폼에 제출된 정보들의 유효성을 검증한다. 만약 유효하지 않으면 우리는 등록 폼으로 다시 돌아가서 에러메시지를 사용자에게 보여줄 것이다
2. 사용자에게 입력된 이메일 주소가 중복되지 않아야 한다. 만약 중복되면, 우리는 등록 폼으로 다시 돌아가서 에러메시지를 사용자에게 보여줄 것이다
3. 새로운 사용자를 만들고 유저로 로그인 한다.
4. 앞번째 페이지로 리다이렉트 한다.
이 과정은 다음 다이어그램에 나타나져 있다.
등록폼 제출을 처리하는 컨트롤러 메소드를 구현해 보자!
컨트롤러 메소드 구현~
등록 폼 제출을 처리하는 컨트롤러 메소드구현은 다음 책임이 있다.
- 등록폼으로 들어가는 정보가 유효해야 한다.
- 만약 이메일 주소가 중복되면 사용자에게 알려줘야 한다.
- 서비스레이어에 폼 객체를 전달해야 한다.
- 새로운 사용자가 생성 된 후에 사용자를 로그인 시킨다.
RegistrationController에 다음 변화를 줘서 이 컨트롤러 메소드를 구현해보자.
1. 컨트롤러 클래스에 private UserService 필드를 넣어준다.
2. UserService 객체를 생성자의 필드로 받는 생성자를 RegistrationController 클래스에 추가하고 다음과 같이 구현한다.
첫째로, 생성자에 @Autowired 어노테이션을 붙이고, 생성자 주입을 통해 의존성 주입이 되도록 하고 service 필드의 값을 세팅한다.
3. 컨트롤러 클래스에 private addFieldError() 메소드를 붙인다. 이 메소드는 바인딩에러를 잡아서 바인딩결과에 추가한다. 이 메소드의 파라미터들은 다음에 설명 되어있다.
3-1 objectName파라미터는 폼 오브젝트의 이름이다.
3-2 . fieldName파라미터는 유효하지않은 값을 가진 폼필드의 이름이다.
3-3. fieldValue파라미터는 폼필드의 값을 포함한다.
3-4. errorCode 파라미터는 필드 에러의 에러코드를 포함한다.
4. 다음 단계로 addFieldError() 를 구현해보자.
4-2. BindingResult클래스의 AddError() 메소드를 호출하여서 생성된 FieldError객체를 bindingResult 에 붙여준다.
5. 컨트롤러의 private createUserAccount() 메소드를 추가한다. 이 메소드는 생성된 User객체를 리턴한다. RegistrationForm와 BindingResult 객체를 메소드 파라미터로 받는다. 만약 이메일 주소가 데이터베이스에 있으면 이 메소드는 null 값을 리턴한다. 다음단계로 구현한다.
5-1. try-catch 구조를 추가하고 DuplicateEmailException객체를 catch 한다.
5-2. registerNewUserAccount() method of the UserServiceinterface메소드를 호출함으로써, try 블록을 구현한다. 메소드 파라미터로서 RegeistrationForm 객체를 전달한다. 생성된 사용자의 정보를 리턴한다.
5-3. private addFieldError() 메소드를 호출하여 catch 블록을 구현한다. 메소드파라미터로 필요한 정보를 전달한다.
이것은 사용자가 등록폼에 적힌 이메일 주소가 데이터베이스에서 발견되었다는 에러메시지를 받는다는 것을 보장한다. null을 리턴한다.
6. 컨트롤러의 클래스의 public registerUserAccount() 메소드를 추가하고, 다음 단계로 구현한다.
6-1. @RequestMapping 어노테이션을 붙이고, '/user/register' 를 POST 형식으로 처리하게 한다.
6-2. 메소드 파라미터로 RegistrationForm 객체를 추가하고, 다음 단계로 어노테이션을 붙인다.
6-2-1. @Valid 어노테이션을 메소드 파라미터로 붙인다. 이것은 컨트롤러메소드가 호출되기전에 객체의 정보를 검증한다.
6-2-2. 메소드파라미터에 @ModelAttribute 어노테이션하고 user값으로 정해준다.
6-3. 메소드파라미터로 BindingResult객체를 추가한다.
6-5. 만약 바인딩 결과에 에러가 있다면, 폼뷰의 이름을 리턴한다.
6-6. private createUserAccount() 메소드를 호출하고, 메소드파라미터로 RegistrationForm 과 BindingResult를 전달한다.
6-7. 만약 the createUserAccount() method 에 의해 리턴된 유저객체가 null값이면, 그것은 이메일주소가 데이터베이스에서 발견되었다는 것을 의미한다. 폼뷰의 이름을 리턴한다.
6-8. SecurityUtil클래스의 loginInUser() 메소드를 호출함으로써 생성된 유저로 로그인 한다. 메소드파라미터로 생성된 user 객체를 전달한다.
6-9. ProviderSignInUtils 클래스의 handlePostSignUp()메소드를 호출한다. 생성된 유저의 이메일주소와 WebRequest객체를 메소드 파라미터로 전달한다. 만약 사용자가 소셜기능을 통해서 사요자를 생성하면 이 메소드는 UserConnection 에 연결을 영속화한다. 만약 사용자가 평범한 사용자 계정을 만들면, 이 메소드는 아무것도 하지 않는다.
6-10. 사용자를 우리의 front페이지로 이동시킨다. redirect=/ 를 통해서..
RegistrationController 클래스는 다음과같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | import org.springframework.beans.factory.annotation.Autowired; import org.springframework.social.connect.web.ProviderSignInUtils; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.context.request.WebRequest; import javax.validation.Valid; @Controller @SessionAttributes ( "user" ) public class RegistrationController { private UserService service; @Autowired public RegistrationController(UserService service) { this .service = service; } @RequestMapping (value = "/user/register" , method = RequestMethod.POST) public String registerUserAccount( @Valid @ModelAttribute ( "user" ) RegistrationForm userAccountData, BindingResult result, WebRequest request) throws DuplicateEmailException { if (result.hasErrors()) { return "user/registrationForm" ; } User registered = createUserAccount(userAccountData, result); if (registered == null ) { return "user/registrationForm" ; } SecurityUtil.logInUser(registered); ProviderSignInUtils.handlePostSignUp(registered.getEmail(), request); return "redirect:/" ; } private User createUserAccount(RegistrationForm userAccountData, BindingResult result) { User registered = null ; try { registered = service.registerNewUserAccount(userAccountData); } catch (DuplicateEmailException ex) { addFieldError( "user" , "email" , userAccountData.getEmail(), "NotExist.user.email" , result); } return registered; } private void addFieldError(String objectName, String fieldName, String fieldValue, String errorCode, BindingResult result) { FieldError error = new FieldError( objectName, fieldName, fieldValue, false , new String[]{errorCode}, new Object[]{}, errorCode ); result.addError(error); } } |
SecurityUtil 클래스는 하나의 정적 메소드 loginInUser() 를 가진다. 이 메소드는 메소드 파라미터로서, 생성된 유저의 정보를 가진다. 그리고 프로그래밍적으로 사용자를 로그인 시킨다. 우리는 다음단계를 통해 구현할 수 있다.
2-1. 첫번째 아규먼트는 principal(aka logged in user) (이부분 무슨 말인지모르겠다. 역자주.) 첫번째 아규먼트로 생성된 ExampleUserDetails객체를 전달한다.
2-2. 두번째 아규먼트는 사용자의 인증(credentials)을 포함한다. 두번째 생성자 아규먼트로 null을 전달한다.
2-3. 세번째 아규먼트는 사용자의 권한들을 포함한다. 우리는 ExampleUserDetails 클래스의 getAuthorities() 를 호출함으로써 그러한 권한들을 얻을 수 있다.
3. 생성된 인증 객체를 security context에 세팅한다.
3-2. SecurityContext 클래스의 setAuthentication() 를 호출하고서, 메소드파라미터로서 생성된 UsernamePasswordAuthenticationToken 객체를 전달한다.
SecurityUtil 클래스의 소스코드는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; public class SecurityUtil { public static void logInUser(User user) { ExampleUserDetails userDetails = ExampleUserDetails.getBuilder() .firstName(user.getFirstName()) .id(user.getId()) .lastName(user.getLastName()) .password(user.getPassword()) .role(user.getRole()) .socialSignInProvider(user.getSignInProvider()) .username(user.getEmail()) .build(); Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null , userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } } |
=====경고
생성한 보통 유저로 로그인하는 것은 좋은 생각이 아니다. 일반적으로 사용자의 이메일이 맞는지 확인 이메일을 보내야 한다. 그러나 이 예제는 등록절차를 단순화하기 위해, 이런 식으로 동작하게 하였다.
=====
도메인 모델 만들기
프로그램의 도메인 모델은 두개의 클래스와 두 개의 이늄으로 구성되어 있다.
- BaseEntity 클래스는 예제 프로그램의 모든 엔티티들의 슈퍼클래스이다.
- User 클래스는 우리 프로그램의 유일한 엔티티 클래스이다. 하나의 유저정보를 포함하고 있다.
- Role 이늄은 우리 프로그램에서 유저의 권한을 나타낸다.
- SocialMediaService 이늄은 우리의 예제프로그램이 지원하는 SaasAPI Provider 를 명시한다.
================
우리의 예제 어플리케이션은 엔티티들을 위하여 분리된 base클래스를 필요로 하지 않는다. 왜냐하면 단지 하나의 엔티티이기 때문이다. 그러나 실제 프로그램에서 분리된 base 클래스를 사용하는 것은 좋은 생각이기 때문에 나는 분리된 base엔티티를 추가하기로 결정하였다.
================
어떻게 도메인 모델을 만들지 알아보자.
먼저, 우리는 모든 엔티티가 공통적으로 가질 필드를 가지고, BaseEntity 클래스를 만들어야 한다. 다음 단계로 구현할 수 있다.
1. 추상BaseEntity 클래스를 만들고 ID라 불리는 하나의 파라미터를 갖게 한다. 이 파라미터는엔티티의 private Key 가 될 것이다.
3. 클래스의 creationTime이라고 불리는 DateTime 필드를 추가하고, 다음 단계로 설정한다.
3-1. @Column 어노테이션을 붙이고, 데이터베이스의 컬럼이름으로 설정한다. nullable 속성은 false 설정한다.
3-2. @Type 어노테이션을 붙이고, type 속성을 ‘org.jadira.usertype.dateandtime.joda.PersistentDateTime’로 한다. 이것은 필드를 커스텀타입으로 처리하게 해주며, DateTime 객체를 하이버네이트에서 영속화하게 해준다.
4. 클래스에 modificationTime으로 불리는 DateTime 필드를 추가하고 다음 단계로 구현한다.
4-1. @Column 어노테이션을 붙이고, 데이터베이스 컬럼명으로 name 을 세팅한다. Nullable 안되게 한다.
4-2. @Type 어노테이션을 붙이고, type 속성을 ‘org.jadira.usertype.dateandtime.joda.PersistentDateTime’로 한다.
5. version 이라고 불리는 long 타입의 필드를 붙이고 @Version 어노테이션을 붙인다. 이것은 긍정적락킹을 가능하게 해주며 긍정적 락킹값으로 쓰이는 버젼값을 보여준다.
6. abstract getId() 라 불리는 메소드를 클래스에 추가한다. 이 메소드는 실제 엔티티의 id 값을 리턴한다.
7. public prePersist() 라는 메소드를 클래스에 추가하고 @PrePersist라는 메소드를 추가한다. 이 메소드는 엔티티매니저가 객체를 영속화하기 전에 호출되서 현재 시간을 생성시간이나 수정시간 필드에 셋팅한다.
8. pulibc preUpdate() 라는 메소드를 클래스에 추가하고~(이하는 바로 위와 비슷하다 역자주 )
BaseEntity 의 소스코드는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | import org.hibernate.annotations.Type; import org.joda.time.DateTime; import javax.persistence.*; @MappedSuperclass public abstract class BaseEntity<ID> { @Column (name = "creation_time" , nullable = false ) @Type (type= "org.jadira.usertype.dateandtime.joda.PersistentDateTime" ) private DateTime creationTime; @Column (name = "modification_time" , nullable = false ) @Type (type= "org.jadira.usertype.dateandtime.joda.PersistentDateTime" ) private DateTime modificationTime; @Version private long version; public abstract ID getId(); //Other getters are omitted for the sake of clarity. @PrePersist public void prePersist() { DateTime now = DateTime.now(); this .creationTime = now; this .modificationTime = now; } @PreUpdate public void preUpdate() { this .modificationTime = DateTime.now(); } } |
두번째로 우리는 User클래스를 만들어야 한다. 우리는 다음 단계로 구현할 수 있다.
1. BaseEntity 클래스를 확장하는 User클래스를 만들어야 한다. 그리고 타입파라미터로 Long 키를 주키로 하는 타입을 준다.
2. @Entity 어노테이션을 붙인다.
3. @Table 어노테이션을 붙이고 사용자 정보가 user_accounts 라는 테이블에 저장되게 한다.
4. id 필드를 Long 타입으로 정하고, 다음으로 설정한다
(여기서부터 소스코드까지의 User클래스는 좀 대충 설명하겠다. 흔한 내용이므로. 역자주)
4-1 @Id 어노테이션 붙인다.
4-2. @GenerationType.AUTO 설정한다.
5. String email 클래스 추가. @Column 어노테이션 추가하고 다음 설정
5-1. email 은 users테이블의 email 컬럼에 저장되야 한다.
5-2. 최대 길이는 100글자
5-3. 널값이 되면 안된다.
5-4. 값이 유니크 해야 한다.
6. String firstName값을 입력하고 @Column 어노테이션을 붙여라.
6-1 firstname 은 users 테이블의 first_name 컬럼에 저장되어야 한다.
6-2. maximum 길이는 100글자
6-3. 널값이 되면 안된다.
7. private String lastName 을 추가. @Column 어노테이션 붙이고. 윗단계와 같다.
8. private String password 필드를 붙이고 필드를 @Column 속성을 세팅하는데 users 테이블의 password 컬럼에 저장하게 하고, 255글자가 최대 글자여야 한다.
9. Role 타입의 role 필드를 붙인다. @Enumerated어노테이션을 붙이고 EnumType.String 으로 값을 세팅한다. 이것은 이늄의 이름으로 디비에 저장된다.
9-1. role속성은 user테이블의 role 컬럼으로 저장된다.
9-2. 최대 길이는 20 글자이고
9-3 널값이 될 수 없다.
10. signInProvider 필드를 클래스에 추가하고 타입을 SocialMediaService로 한다. @Enumerated 어노테이션을 붙이고 EnumType.String 으로 값을 세팅한다. 다음단계로 구현한다.
11. public static Builder 내부 클래스 생성. 다음 단계로 구현.
11-1 User필드를 클래스에 추가. 생성된 User객체를 참조한다.
11-2 생성자추가. 생성자는 새로운 user객체를 만들고 role을 Role.ROLE_USER 로 세팅한다.
11-3. 생성된 유저객체의 필드값들을 세팅하는 메소드들을 빌더 클래스에 넣어준다. 각각의 메소드들은 메소드파라미터로 전해진 값들을 세팅해주며 User.builder 객체를 참조하는 값을 리턴한다.
11-4 build() 메소드를 빌더클래스에 넣자. 이 메소드는 생성된 User객체를 리턴한다.
12. public static getBuilder() 메소드를 User클래스에 추가하자. 이 메소드는 새로운 User.Builder객체를 리턴한다.
=====빌더 패턴이 궁금하면 여기를 보란 얘기같다.
You can get more information about the builder pattern by reading a blog post called The builder pattern in practice.
=====
유저클래스는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | import javax.persistence.*; @Entity @Table (name = "user_accounts" ) public class User extends BaseEntity<Long> { @Id @GeneratedValue (strategy = GenerationType.AUTO) private Long id; @Column (name = "email" , length = 100 , nullable = false , unique = true ) private String email; @Column (name = "first_name" , length = 100 ,nullable = false ) private String firstName; @Column (name = "last_name" , length = 100 , nullable = false ) private String lastName; @Column (name = "password" , length = 255 ) private String password; @Enumerated (EnumType.STRING) @Column (name = "role" , length = 20 , nullable = false ) private Role role; @Enumerated (EnumType.STRING) @Column (name = "sign_in_provider" , length = 20 ) private SocialMediaService signInProvider; //The constructor and getters are omitted for the sake of clarity public static Builder getBuilder() { return new Builder(); } public static class Builder { private User user; public Builder() { user = new User(); user.role = Role.ROLE_USER; } public Builder email(String email) { user.email = email; return this ; } public Builder firstName(String firstName) { user.firstName = firstName; return this ; } public Builder lastName(String lastName) { user.lastName = lastName; return this ; } public Builder password(String password) { user.password = password; return this ; } public Builder signInProvider(SocialMediaService signInProvider) { user.signInProvider = signInProvider; return this ; } public User build() { return user; } } } |
Role 은 이늄인데 다음과 같다.
1 2 3 | public enum Role { ROLE_USER } |
소셜미디어서비스 이늄은 다음과 같다.
1 2 3 4 | public enum SocialMediaService { FACEBOOK, TWITTER } |
다음은 어떻게 새로운 유저계정을 만들고 영속화하는 서비스클래스를 구현할지 알아보자.
서비스 클래스 생성하기
먼저, 우리는 데이터베이스에 새로운 계정을 추가할려고 쓰이는 메소드를 선언하는 인터페이스를 만들어야 하는데, 다음과 같다.
registerNewUserAccount() 메소드는 RegistrationForm 객체를 메소드 파라미터로 받고, 유저객체를 리턴한다. 만약 RegistrationForm 에 저장된 이메일필드의 이메일주소가 데이터베이스에서 발견되면 DuplicateEmailException를 발생한다.
Userservice 인터페이스는 다음과같다.
1 2 3 4 | public interface UserService { public User registerNewUserAccount(RegistrationForm userAccountData) throws DuplicateEmailException; } |
두번째로, 우리는 UserService 인터페이스를 구현해야 한다. 다음 단계를 거친다.
1. UserService인터페이스를 구현하는 클래스를 만들고 @Service 인터페이스를 붙인다.
3. 생성된 클래스에 UserRepository 에 붙인다.
4. 생성자 아규먼트로 PasswordEncoder 와 UserRepository 객체를 받는 생성자를 만든다. 생성자는 다음같이 구현
4-1. @Autowired 붙이고
4-2 passwordEncoder와 repository 필드의 값을 붙인다.
5. private emailExist() 메소드를 추가한다. 이 메소드는 메소드 아규먼트로 이메일 주소를 받고, Boolean 형태로 리턴한다. 다음같이 구현한다.
5-1. UserRepository 인터페이스의 findByEmail() 메소드를 호출하여, 메소드파라미터로 주어진 이메일어드레스로 유저를 받는다.
5-2 유저가 발견되면 ture 리턴
5-3. 유저가 발견되지 않으면 false 리턴
6. private encodePassword()메소드를 서비스 클래스에 추가. 이 메소드는 RegistrationForm객체를 메소드 파라미터로 받아서 암호화된 패스워드를 리턴한다. 다음같이 구현한다.
6-1. 사용자가 평범한 사용자를 만드는지 알아본 뒤 우리는 RegistrationForm 클래스의 isNormalRegistration() 메소드를 호출함으로써, 정보를 얻을 수 있다. 만약 ture를 리턴하면, the encode() method of the PasswordEncoder class를 호출해서 암호화된 패스워드를 얻는다. 메소드 파라미터로 평문 비밀번호를 보낸다. 암호화된 비밀번호를 리턴한다.
6-2. 만약 사용자가 소셜로 등록된 유저면 null 값을 리턴한다.
7. registerNewUserAccount() 메소드를 서비스 클래스에 추가하고 다음과 같이 구현한다.
7-1 @Transactional 어노테이션을 붙이고 이것은 이 메소드가 읽고쓰는 트랜잭션 안에서 실행된다는 것을 의미한다.
7-2 이메일 주소가 데이터베이스에서 찾을 수 있는지 확인한다. 우리는 private emailExist() 메소드를 호출함으로써 이를 확인할 수 있다. 메소드파라미터로 RegistrationForm객체를 전달한다. 만약 메소드가 true 를 리턴하면, DuplicateEmailException를 발생시킨다.
7-3. private encodePassword() 메소드를 호출함으로써 암호화된 비밀번호를 얻는다. 메소드파라미터로 RegistratonForm 을 전달한다.
7-4. user클래스의 getBuilder() 메소드를 호출함으로써 Builder 객체를 얻고 생성된 객체의 다음값들을 세팅한다.
- Email 주소
- 첫번째 이름
- 마지막 이름
- 비밀번호
7-5 소셜로그인을 통해서 사용자가 새로운 사용자 계정을 만들었는지 알아본다. 우리는 RegistrationForm 클래스의 메소드를 호출함으로써 이것을 할 수 있다. 만약 이 메소드가 true 를 리턴하면 user클래스의 signInProvider() 를 호출하여서 소셜provider를 세팅한다. 메소드파라미터로 사용된 Sign in provider 를 전달한다.
7-6. 사용자 객체를 생성한다.
7-7. UserRepository인터페이스의 save메소드를 호출하여서 User객체를 db에 영속화한다. 메소드파라미터로 생성된 user객체를 전달한다.
7-8. 영속화된 객체를 리턴한다.
RepositoryUserService클래스는 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class RepositoryUserService implements UserService { private PasswordEncoder passwordEncoder; private UserRepository repository; @Autowired public RepositoryUserService(PasswordEncoder passwordEncoder, UserRepository repository) { this .passwordEncoder = passwordEncoder; this .repository = repository; } @Transactional @Override public User registerNewUserAccount(RegistrationForm userAccountData) throws DuplicateEmailException { if (emailExist(userAccountData.getEmail())) { throw new DuplicateEmailException( "The email address: " + userAccountData.getEmail() + " is already in use." ); } String encodedPassword = encodePassword(userAccountData); User.Builder user = User.getBuilder() .email(userAccountData.getEmail()) .firstName(userAccountData.getFirstName()) .lastName(userAccountData.getLastName()) .password(encodedPassword); if (userAccountData.isSocialSignIn()) { user.signInProvider(userAccountData.getSignInProvider()); } User registered = user.build(); return repository.save(registered); } private boolean emailExist(String email) { User user = repository.findByEmail(email); if (user != null ) { return true ; } return false ; } private String encodePassword(RegistrationForm dto) { String encodedPassword = null ; if (dto.isNormalRegistration()) { encodedPassword = passwordEncoder.encode(dto.getPassword()); } return encodedPassword; } } |
우리는 여전히 Spring Data JPA repository 를 만들어야 한다. 어떻게 만들이 한번 알아보자.
Creating Spring data JPA Repository
우리의 마지막 단계는 Spring data JPA repository 를 만드는 것이다.
- 새로운 사용자를 데이터베이스에 저장해야 한다.
- 이메일주소를 검색 크리테리아로 써서, 데이터베이스로부터 user객체를 받아야 한다.
우리는 다음 요구사항을 이행하는 JPA 리파지토리를 만들수 있다.
1. JpaRepository 인터페이스를 확장하는 리파지토리를 만든다. 엔티티 타입은 User 고 주키는 Long으로 한다. 이것은 JpaRepository 에 선언된 메소드에 접근할 수 있는 권한을 준다. 그 메소드 중의 하나는 save() 메소드인데 이것은 데이터베이스에 User객체를 저장하는데 쓰인다.
2. findByEmail()메소드를 생성된 리파지토리 인터페이스에 추가한다. 이 메소드는 email주소를 메소드파라미터로 쓰며, 메소드파라미터로 주어진 이메일 주소가 같은 유저객체를 리턴한다. 유저가 발견되지 않으면 null값을 리턴한다.
====
정보가 더 필요하면 여기를 보란 말 같다~~
If you want to get more information about Spring Data JPA, you can take a look at my Spring Data JPA tutorial.
====
UserRepository인터페이스의 소스코드는 다음과 같다.
1 2 3 4 5 6 | import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository<User, Long> { public User findByEmail(String email); } |
이것이다. 우리가 지금까지 한것을 요약해보는 시간을 가져보자.
요약
우리는 우리예제프로그램의 몇가지 요구사항을 구현하였다. 그것은 다음과 같다.
- 우리는 보통 유저계정과 소셜 을 통한 유저계정의 등록 기능을 만들었고,
- 사용자 이름과 비밀번호를 사용하여서 로그인이 되게 하였고,
- 소셜기능을 통해서 로그인이 되게 하였다.
우리의 기억을 되살려서 등록프로세스를 다시 한번 살펴보자. 이 과정은 다음의 표에 설명되어있다.
이 블로그 포스트는 우리에게 다음것들을 가르쳐주었다.
- 어떻게 흐름 속에서 소셜 로그인이 시작되는지 배웠다.
- SaaS API Provider를 통해 제공된 정보를 이용하여서 등록폼의 정보가 어떻게 미리 생성되는지 보았다.
- 등록폼에 들어가는 정보가 유효한지를 검증하는 커스텀 인증 제약을 어떻게 만들었는지 배웠다.
번역글이라서, 원저작자의 링크를 남기고자 한다.
(똥망 번역글이 되지 않았나 싶다. 그냥 일단 나만보자식의 번역글이라 보면서 바로바로 그냥 한글로 치긴했는데;;
잘 됬을지 모르겠다.;;)
About the Author
Petri Kainulainen 은 소프트웨어개발과 지속적향상에 열정과 관심을 가지고 있으며, 스프링 프레임워크의 전문가이자 Spring Data book 의 저자이기도 합니다. About Petri Kainulainen →
Connect With Him
트위터 : https://www.twitter.com/petrikainulaine
구글+: https://plus.google.com/+PetriKainulainen?rel=author
링크드인 : http://www.linkedin.com/in/petrikainulainen
유튜브 : https://www.youtube.com/user/Loketus
RSS : http://feeds.feedblitz.com/PetriKainulainen
깃헙 주소들 :
저의 깃헙: https://github.com/arahansa/LearningSpringSocial
'FrameWork_ETC > Spring' 카테고리의 다른 글
Google App engine 스프링 + JPA 연동 (앱엔진 스프링 JPA) (2) | 2014.12.15 |
---|---|
Spring의 Encryptors 문서 둘러보기. (0) | 2014.12.03 |
스프링 페이스북 회원가입 로그인 - (1) (0) | 2014.11.20 |
web.xml 스프링3입문 (0) | 2014.08.05 |
DataSource 트랜잭션 매니져 (0) | 2014.08.03 |