http://rifers.org 에서 발췌한 내용을 번역 하였습니다.
번역: 이원찬(wonchan.lee@gmail.com)
-------------------------------------------------------
Using a database for user data
앞장에서 우리는 인증시에 XML에 저장된 사용자 목록을 사용하였다. 더 많은 사용자 목록을 필요로 한다면 RIFE의 데이터베이스 사용자 지원을 사용할 수 있다. 이것이 메모리 사용자와 어떻게 다른지 간략히 다루겠다.
먼저, 인증요소가 memory.xml 대신 rife/autheticated/database.xml 을 확장하도록 해야한다. 그리고, 어떤 데이터소스를 사용할지 설정파일을 기술하여야 한다. 예제를 살펴보자.
------------------------------------------------------------------
데이터베이스 사용자를 위한 인증요소: elements/auth.xml
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE element SYSTEM "/dtd/element.dtd">
<element extends="rife/authenticated/database.xml">
<property name="template_name">auth</property>
<property name="role">admin</property>
<property name="datasource"><datasource>postgresql</datasource></property>
<submission name="credentials">
<param name="login"/>
<param name="password"/>
</submission>
<childtrigger name="authid"/>
</element>
------------------------------------------------------------------
------------------------------------------------------------------
사이트 파일에 authid 와 elements/auth.xml 추가
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE site SYSTEM "/dtd/site.dtd">
<site>
<!-- Add authid variable -->
<globalvar name="authid"/>
<!-- Refer to the AUTH element definition -->
<element id="AUTH" file="elements/auth.xml" />
<!-- Indicating that a subsite requires authentication -->
<subsite id="ADMIN" file="admin.xml" urlprefix="/admin" inherits="AUTH"/>
...
</site>
------------------------------------------------------------------
이제 DatabaseUserFactory와 DatabaseSessionsFactory로 DatabaseUsers와 DatabaseSessions 객체를 생성해야 한다. RIFE는 많은 데이터베이스들을 지원한다. (Derby, Firebird, H2, HSQLDB, MMcKoiSQL, MySQL, One$DB/DaffodilDB, Oracle, PostgreSQL등) 이것들을 사용하기 위해 어떠한 코딩작업도 필요없다. 사용자와 세션객체의 install메소드만 호출하면 된다. 그후로 인증은 메모리사용자와 동일하게 작동한다.
------------------------------------------------------------------
데이터베이스 사용자를 위한 테이블설치
------------------------------------------------------------------
Datasource source = Datasources.getRepInstance().
getDatasource(Config.getRepInstance().getString("DATASOURCE"));
DatabaseUsersFactory.getInstance(source).install();
DatabaseSessionsFactory.getInstance(source).install();
------------------------------------------------------------------
아래 예제 참여자는 초기에 테이블을 추가하고 기본 사용자인 admin 을 생성한다.
------------------------------------------------------------------
package my.participants;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.uwyn.rife.authentication.credentials.RoleUser;
import com.uwyn.rife.authentication.credentialsmanagers.DatabaseUsers;
import com.uwyn.rife.authentication.credentialsmanagers.DatabaseUsersFactory;
import com.uwyn.rife.authentication.credentialsmanagers.RoleUserAttributes;
import com.uwyn.rife.authentication.sessionmanagers.DatabaseSessionsFactory;
import com.uwyn.rife.database.Datasource;
import com.uwyn.rife.rep.BlockingParticipant;
public class ParticipantDatabaseAuthentication extends
BlockingParticipant {
protected void initialize() {
String dsName = getParameter();
if( null == dsName || 0 == dsName.length() ) {
dsName="datasource";
}
Datasource mDatasource = (Datasource)getRepository().getProperties().getValue(dsName);
boolean hasUsers = true;
DatabaseUsers users = null;
try {
users = DatabaseUsersFactory.getInstance(mDatasource);
users.install();
hasUsers = false;
} catch (Exception e) {
Logger.getLogger(this.getClass().getName()).log(Level.INFO,
"The Database Authentication tables could not be installed -- they probably already exist.", e);
}
// If we were able to create a users table, then
// there aren't going to be any users in it.
if( !hasUsers ) {
try {
// Create an 'admin' role
String defaultRoleName = "admin";
users.addRole(defaultRoleName);
// Create a user named 'admin', with password 'password', and role 'admin'
RoleUser user = new RoleUser();
user.setLogin("admin");
user.setPassword("password");
RoleUserAttributes userAttrs = new RoleUserAttributes();
userAttrs.setPassword(user.getPassword());
userAttrs.setRoles(new String[] {defaultRoleName} );
users.addUser(user.getLogin(), userAttrs);
} catch (Exception e) {
Logger.getLogger(this.getClass().getName()).log(Level.WARNING,
"Unable to create a default user. This will need to be done manually.", e);
}
}
try {
DatabaseSessionsFactory.getInstance(mDatasource).install();
} catch (Exception e) {
Logger.getLogger(this.getClass().getName()).log(Level.INFO,
"The Database Sessions tables could not be installed -- they probably already exist.", e);
}
}
}
------------------------------------------------------------------
------------------------------------------------------------------
참여자 정의 : rep/participants.xml
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE rep SYSTEM "/dtd/rep.dtd">
<rep>
...
<participant param="rep/datasources.xml">ParticipantDatasources</participant>
<property name="datasource"><datasource>mysql</datasource></property>
<participant blocking="true" param="datasource">my.participants.ParticipantDatabaseAuthentication</participant>
...
</rep>
------------------------------------------------------------------
메모리와 데이터베이스 백엔드 복합
어떤경우, 사용자는 데이터베이스에 저장하더로도 세션정보는 저장하지 않길 원할 수도 있다, 이를위해 혼합(mixed) 인증요소가 있다: rife/autheticated/mixed.xml. 이것은 세션데이터가 메모리에 저장되므로 데이터베이스 세션이 필요하지 않다는것만 제외하면 데이터베이스 인증요소와 같은 방법으로 이용한다.
자동 세션 폐기
기본적으로 memory, database, mixed 인증은 이것이 만료(outdated)되었을때 자동으로 세션정보를 폐기한다.
------------------------------------------------------------------
<element extends="rife/authenticated/database.xml">
------------------------------------------------------------------
사용자가 최대 대기가능한 세션타임은 SESSION_DURATION 옵셕으로 설정된다. 기본값은 20분이다. 이값은 milliseconds 단위로 적용된다.
그외, 폐기주기도 SESSION_PURGE_FREQUENCEY와 SESSION_PURGE_SCALE에 값을 기술해 수정할 수 있다. 기본적으로 각 20과 1000을 권장한다. 이것은 폐기가 인승세션에 50번 접근될 때마다 폐기를 한다는걸 의미한다.
사용자 생성
데이터베이스에 사용자를 추가하려면 DatabaseUsers클래스를 사용해야 한다. 사용자는 로그인명과, 비밀번호 그리고, 최소한 하나이상의 역할(role)을 필요로 한다. RoleUser와 RoleUserAttribute 클래스가 이값들을 유지한다. 만약, 가입폼이 있고 사용자의 로그인명과 비밀번호를 입력받는다면, submission 처리자(handler)는 아래와 같을 것이다.
------------------------------------------------------------------
/* Fetch the login and password from the form. */
RoleUser user = getSubmissionBean(RoleUser.class);
/* If the login and password are okay, create the user. */
if (user.validate()) {
/* Fetch the data source for your database in whatever way you like. */
Datasource ds = (Datasource) getProperty("datasource");
/* And use it to create a user-management helper object. */
DatabaseUsers users = DatabaseUsersFactory.getInstance(ds);
try {
/* The user should have a password and a single role of "user".
We could also use the RoleUserAttributes() constructor to avoid
these "set" calls. */
RoleUserAttributes attrs = new RoleUserAttributes();
attrs.setPassword(user.getPassword());
attrs.setRoles(new String[] { "user" });
/* Add the user to the database. */
users.addUser(user.getLogin(), attrs);
/* Send the user to a "thanks for signing up" page (this assumes
an exit called "success" is defined for the registration page.) */
exit("success");
} catch (CredentialsManagerException e) {
/* Handle the error; in this example we just log the exception
and fall through to redisplaying the form. */
log.severe(ExceptionUtils.getExceptionStackTrace(e));
}
}
/* Redisplay the signup form with errors. */
generateForm(template, user);
print(template);
------------------------------------------------------------------
그밖의 사용자정보 관리
물론, 대부분의 사이트는 로그인시 사용자의 로그인명과 비밀번호만 필요로 한다. RIFE의 인증관리는 사용자 테이블의 내용을 관리하며 부가적인 사용자 정보는 분리되어 관리된다. (사용자 모델을 변경하여 그밖의 정보도 포함하려면 RIFE의 인증관리자 클래스를 대체할 클래스를 생성해서 사용 하여도 된다.)
아래 예제에서는 "Generic query manager" 를 사용해 사용자 데이터에 접근할것이다.
Account 라는 클래스에 부가정보를 넣기로 했다. (하지만, 이것은 정해진것이 아니므로 어떤 이름을 사용해도 관계 없다.) 예를들어, 각 사용자를 위해 e-mail주소를 추가할 것이다.
------------------------------------------------------------------
Account.java
------------------------------------------------------------------
public class Account {
/* Account ID. */
private int id;
/* RIFE-managed user ID. */
private long rifeUserId;
/* The user's E-mail address. */
private String email;
... getters and setters for the above ...
}
------------------------------------------------------------------
RIFE에서 제공된 사용자의 ID외에 별개의 유일키(ID)가 저장되는 부분을 주목하자. 기본적으로 이속성은 one-to-one매핑이다. 하지만, RIFE의 퍼시스턴스 레이어는 현재 long타입에 대해 identifier를 지원하지 않는다.
이 클래스는 메타데이터를 갖는다. 메터데이터 결합 메커니즘은 Account클래스를 깨끗하게 유지할 수 있게 한다.
------------------------------------------------------------------
AccountMetaData.java
------------------------------------------------------------------
public class AccountMetaData extends MetaData {
public void activateMetaData() {
String userTable = RifeConfig.Authentication.getTableUser();
addConstraint(new ConstrainedProperty("id")
.identifier(true)
.editable(false));
addConstraint(new ConstrainedProperty("rifeUserId")
.notNull(true)
.manyToOne(userTable, "userid"));
addConstraint(new ConstrainedProperty("email")
.notNull(true)
.email(true)
.maxLength(100));
}
}
------------------------------------------------------------------
사용자 생성
가입폼에 email 필드를 추가하자. RIFE는 폼의 필드에 기반해 Account와 RoleUser객체를 인스턴스화 한다. 가입폼의 요소(elemet)정의는 아래와 같다.
------------------------------------------------------------------
가입폼 요소 정의
------------------------------------------------------------------
<element id="Register" implementation="com.foobar.elements.Register" url="register">
<submission name="register">
<bean classname="com.foobar.Account" />
<bean classname="com.uwyn.rife.authentication.credentials.RoleUser" />
</submission>
</element>
------------------------------------------------------------------
그리고, 가입을 위한 submission 핸들러는 다음과 같다.
------------------------------------------------------------------
사용자 계정 생성
------------------------------------------------------------------
public void doRegister() {
RoleUser user = getSubmissionBean(RoleUser.class);
/* Fetch the email address from the form. */
Account account = getSubmissionBean(Account.class);
/* If the login, password, and email are okay, create the user. */
if (user.validate() && ((Validated)account).validate()) {
Datasource ds = (Datasource) getProperty("datasource");
DatabaseUsers users = DatabaseUsersFactory.getInstance(ds);
/* We will also need to store the account, so get a query manager for it. */
ContentQueryManager<Account> accountManager =
new ContentQueryManager<Account>(ds, Account.class);
try {
/* Create the user as in the previous example (but more tersely). */
users.addUser(user.getLogin(), new RoleUserAttributes(user.getPassword(),
new String[] { "user" }));
/* The user is created. Now we can fetch its ID and remember it in the
Account object. */
account.setRifeUserId(users.getUserId(user.getLogin()));
/* Store the new account in the database. */
accountManager.save(account);
exit("success");
} catch (CredentialsManagerException e) {
/* Handle the error; in this example we just log the exception
and fall through to redisplaying the form. */
log.severe(ExceptionUtils.getExceptionStackTrace(e));
}
}
/* Redisplay the signup form with errors. */
generateForm(template, user);
generateForm(template, account);
print(template);
}
------------------------------------------------------------------
대부분의 경우 RIFE 사용자와 Account객체를 한 트랜잭션으로 처리하고 싶을 것이다. 그렇지 않으면 사용자 생성에 실패할 경우 Account가 생성되지 않을 수도 있다. 자세한 정보는 cookbook의 Chainable transactions 장을 참고하라.
부가정보에 접근하기
이제 사용자를 위해 Account를 추가했다. 이제 요소에서 이것을 읽을 수 있어야 한다. 요소가 인증이나 identified.xml을 확장했다고 가정할 경우 Generic query manager를 사용해 사용자의 Account객체를 가져올 수 있다.
------------------------------------------------------------------
Account 객체 가져오기
------------------------------------------------------------------
RoleUserIdentity identity;
Account account = null;
/* Since this is an identified or authenticated element, there is a
RoleUserIdentity assigned to the request if the user is logged in. */
identity = (RoleUserIdentity)getRequestAttribute(Identified.IDENTITY_ATTRIBUTE_NAME);
/* Anonymous users can hit identified (but not authentication-required) pages.
Don't try to fetch their account data. */
if (identity != null)
{
GenericQueryManager<Account> manager;
/* Fetch the data source using whatever technique you like. */
Datasource ds = (Datasource) getProperty("datasource");
manager = GenericQueryManagerFactory.getInstance(ds, Account.class);
/* Now we fetch the account using a simple query manager call.
The RoleUserIdentity has a child object which contains the user ID. */
account = mgr.restoreFirst(mgr.getRestoreQuery()
.where("rifeUserId", "=",
identity.getAttributes().getUserId()));
}
------------------------------------------------------------------
Database schema
만약 수동으로 인증시스템 데이터베이스 테이블을 생성한다면 아래의 정의를 참고하라. 정확한 데이터 타입은 데이터베이스 마다 조금씩 차이가 있다.
authentication
이 테이블은 현재 모든 활성화된 세션상태를 가지고 있다. 이것은 "혼합(mixed)" 백엔드를 사용할 경우 사용되지 않는다. 사용자가 로그인 또는 로그아웃시에 삽입또는 삭제된다. 모든 컬럼은 NOT NULL이며 테이블명은 TABLE_AUTHENTICATION 설정 파라미터를 오버라이드하여 변경될 수 있다.
authid varchar(32) Opaque session identifier, as stored in the authid cookie
or request parameter. Primary key.
userid integer (64-bit) User ID.
hostip varchar(40) Remote IP address of the session.
sessstart integer (64-bit) Session start time, in milliseconds since the
epoch (System.currentTimeMillis() format). Indexed
(non-unique) to allow fast deletion of expired sessions.
remembered boolean True if the user should be remembered after the
session expires.
authrole
이 테이블은 인증 역할(role)들을 가진다. 모든컬럼은 NOT NULL이며 TABLE_ROLE 설정 파라미터로 오버라이드 될수 있다.
roleid integer (32-bit) Unique numeric identifier of the role. Primary key.
name varchar(20) Name of the role. Has a unique constraint.
authrolelink
authrole 과 authuser 테이블 간의 링크 테이블; 어떤 사용자가 어떤 role을 가지는지 정의한다. 모든 컬럼은 NOT NULL이며 TABLE_ROLELINK 설정 파라미터로 오버라이드 할 수 있다.
userid integer (64-bit) User ID, foreign key to authuser.userid. First
part of composite primary key.
roleid integer (32-bit) Which role the user has, foreign key to
authrole.roleid with cascading deletes enabled.
Second part of composite primary key.
authuser
사용자ID와 비밀번호를 가지고 있다. 모두 NOT NULL이며 테이블명은 TABLE_USER설정 파라미터를 오버라이드할 수 있다.
userid integer (64-bit) User ID. Primary key.
login varchar(20) User's login name. Has a unique constraint.
passwd varchar(100) User's password, possibly encrypted
(see Password encryption).
댓글 없음:
댓글 쓰기