Hiển thị các bài đăng có nhãn Hibernate. Hiển thị tất cả bài đăng
Hiển thị các bài đăng có nhãn Hibernate. Hiển thị tất cả bài đăng

26 tháng 3, 2018

Xử lý tập dữ liệu lớn với Hibernate

Nếu bạn cần phải xử lý tập kết quả cơ sở dữ liệu lớn bằng Java, thì bạn có thể lựa chọn JDBC cho phép bạn khả năng kiểm soát ứng dụng ở tầng thấp. Mặt khác, nếu bạn đã lựa chọn sử dụng ORM trong ứng dụng của mình, nhưng sau đó thay đổi để sử dụng JDBC thì điều này lại khiến bạn mất khá nhiều tính năng thú vị như optimistic locking, caching,... Rất may là hầu hết các ORM framework, ví dụ như Hibernate, có vài lựa chọn có thể giúp bạn có thể xử lý tập kết quả cơ sở dữ liệu lớn.

Một ví dụ đơn giản như sau, giả định chúng ta có một bảng (được ánh xạ tới class DemoEntity) với một 100,000 bản ghi. Mỗi bản ghi bao gồm một cột đơn (được ánh xạ tới thuộc tính property trong lớp DemoEntity) giữ một số kí tự ngẫu nhiên với dung lượng ~ 2KB. JVM được chạy với -Xmx250m - giả sử chúng ta chỉ có tối đa 250MB cho bộ nhớ JVM trên hệ thống. Công việc của bạn là đọc toàn bộ bản ghi có trong bảng, thực hiện một vài xử lý và sau đó lưu lại kết quả. Ta giả định rằng kết quả từ việc xử lý tập dữ liệu lớn không bị thay đổi. Để bắt đầu ta sẽ thử xử lý đơn giản trước, thực hiện truy vấn lấy toàn bộ dữ liệu:

new TransactionTemplate(txManager).execute(new TransactionCallback<Void>() {
    @Override
    public Void doInTransaction(TransactionStatus status) {
        Session session = sessionFactory.getCurrentSession();
        List<DemoEntity> demoEntitities = (List<DemoEntity>) session.createQuery("from DemoEntity").list();
        for(DemoEntity demoEntity : demoEntitities) {
           // Process and write result
        }
        return null;
    }
});

Sau vài giây, ta nhận được message sau:

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded

Hình 1:

Hình 1

Rõ ràng điều này không giải quyết được vấn đề của chúng ta. Để khắc phục vấn đề này, chúng ta sẽ chuyển qua sử dụng scrollable trong Hibernate để thực thi câu truy vấn trên, ánh xạ toàn bộ kết quả vào các đối tượng entityreturn chúng. Khi sử dụng scrollable result set, các bản ghi được biến đổi sang các entity trong cùng một lần:

new TransactionTemplate(txManager).execute(new TransactionCallback<Void>() {

    @Override
    public Void doInTransaction(TransactionStatus status) {
        Session session = sessionFactory.getCurrentSession();
        ScrollableResults scrollableResults = session.createQuery("from DemoEntity").scroll(ScrollMode.FORWARD_ONLY);

        int count = 0;
        while (scrollableResults.next()) {
            if (++count > 0 && count % 100 == 0) {
                System.out.println("Fetched " + count + " entities");
            }
            DemoEntity demoEntity = (DemoEntity) scrollableResults.get()[0];
            // Process and write result
        }
        return null;
    }
});

Sau khi chạy đoạn code trên:

...
Fetched 49800 entities
Fetched 49900 entities
Fetched 50000 entities
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded

Hình 2:

Hình 2

Mặc dù ta đang sử dụng một scrollable result set, nhưng mỗi đối tượng trả về là một attached object và trở thành một phần của persistence context (aka session). Kết quả này thực sự rất giống với ví dụ đầu tiên khi mà chúng ta sử dụng session.createQuery("from DemoEntity").list(). Tuy nhiên, với phương cách đầu tiên ta không phải điều khiển bằng tay, chúng ta sẽ có được kết quả khi Hibernate hoàn thành công việc của nó. Mặt khác, bằng cách sử dụng một scrollable result set sẽ cho phép chúng ta khả năng kiểm soát quá trình xử lý và giải phóng bộ nhớ khi cần. Như ta có thể thấy thì nó sẽ không tự động giải phóng bộ nhớ, bạn phải cho Hibernate biết khi nào sẽ thự hiện việc này. Bạn có thể tham khảo vài cách thực hiện như sau:

  • Thu hồi vùng nhớ của đối tượng trong persistent context sau khi xử lý xong
  • Xóa toàn bộ session

Chúng ta sẽ lựa chọn cách đầu tiên, ở ví dụ trên tại dòng số 13 (// Process and write result) ta sẽ thêm đoạn code sau:

session.evict(demoEntity);

Lưu ý:

  • Nếu bạn đã thực hiện bất kì thay đổi nào trên một hoặc nhiều đối tượng entity hãy chắc chắn rằng bạn đã thực hiện lệnh flush session trước khi thực hiện evict hoặc clear, nếu không thì câu truy vấn sẽ bị giữ lại vì nếu thực hiện sau thì Hibernate sẽ không gửi các thay đổi này đến cơ sở dữ liệu
  • Evict hoặc clear sẽ không xóa các đối tượng entity khỏi second level cache. Nếu bạn cho phép và đang sử dụng second level cache, bạn có thể phương thức sessionFactory.getCache().evictXxx() nếu muốn loại bỏ đối tượng khỏi second level cache
  • Sau thời điểm bạn sử dụng evict trên một entity, thì nó sẽ không liên kết với session nữa. Bất kì thay đổi nào được thực hiện trên đối tượng entity sẽ không được tự động phản ánh vào trong database. Nếu bạn đang sử dụng lazy loading, khi truy cập vào bất kì thuộc tính nào mà không được load trước khi thực hiện evict thì một ngoại lệ org.hibernate.LazyInitializationException sẽ được ném ra. Vì vậy, về cơ bản hãy đảm bảo xử lý xong các đối tượng entity (hoặc ít nhất khởi tạo những thứ cần thiết) trước khi bạn sử dụng evict hoặc clear

Sau khi chạy lại chương trình, ta sẽ thấy nó được thực thi thành công:

...
Fetched 99800 entities
Fetched 99900 entities
Fetched 100000 entities

Hình 3:

Hình 3

Bạn cũng có thể thiết lập câu truy vấn là read-only, điều này cho phép Hibernate thực hiện thêm một vài tối ưu.

ScrollableResults scrollableResults = session.createQuery("from DemoEntity").setReadOnly(true).scroll(ScrollMode.FORWARD_ONLY);

Thực hiện điều này chỉ mang lại sự khác biệt rất nhỏ trong việc sử dụng bộ nhớ.

Chúng ta đã có thể xử lý được 100,000 bản ghi, nhưng Hibernate có một tùy chọn khác để xử lý số lượng lớn: Stateless session. Bạn có thể lấy được scrollable result set từ một stateless session tương tự với cách làm với một session thông thường. Một stateless session nằm ngay bên trên của JDBC. Gần như, Hibernate sẽ chạy ở chế độ tắt toàn bộ các tính năng, nghĩa là sẽ không có persistent context, không có 2nd level caching, không có dirty detection, không có lazy loading, về cơ bản là sẽ không có gì. Javadoc có mô tả như sau:

/**
 * A command-oriented API for performing bulk operations against a database.
 * A stateless session does not implement a first-level cache nor interact with any 
 * second-level cache, nor does it implement transactional write-behind or automatic 
 * dirty checking, nor do operations cascade to associated instances. Collections are 
 * ignored by a stateless session. Operations performed via a stateless session bypass 
 * Hibernate's event model and interceptors.  Stateless sessions are vulnerable to data 
 * aliasing effects, due to the lack of a first-level cache. For certain kinds of 
 * transactions, a stateless session may perform slightly faster than a stateful session.
 *
 * @author Gavin King
 */

Điều duy nhất sẽ được thực hiện đó là chuyển đổi các bản ghi thành các đối tượng java. Đây có thể là một lựa chọn hấp dẫn bởi vì nó giúp bạn thoát khỏi ý nghĩ là khi nào sẽ cần phải evict/flush.

new TransactionTemplate(txManager).execute(new TransactionCallback<Void>() {

    @Override
    public Void doInTransaction(TransactionStatus status) {
        sessionFactory.getCurrentSession().doWork(new Work() {
            @Override
            public void execute(Connection connection) throws SQLException {
                StatelessSession statelessSession = sessionFactory.openStatelessSession(connection);
                try {
                    ScrollableResults scrollableResults = statelessSession.createQuery("from DemoEntity").scroll(ScrollMode.FORWARD_ONLY);

                    int count = 0;
                    while (scrollableResults.next()) {
                        if (++count > 0 && count % 100 == 0) {
                            System.out.println("Fetched " + count + " entities");
                        }
                        DemoEntity demoEntity = (DemoEntity) scrollableResults.get()[0];
                        // Process and write result 
                    }
                } finally {
                    statelessSession.close();
                }
            }
        });
        return null;
    }
});

Hình 4:

Hình 4

Bên cạnh thực tế là sử dụng stateless session sẽ rất tối ưu bộ nhớ, thì khi sử nó cũng có một vài hạn chế, bạn có thể nhận thấy là ta phải thực hiện mở và đóng stateless session bằng tay một cách tường minh: sẽ không có bất kì phương thức nào giống như sessionFactory.getCurrentStatelessSession() hoặc bất kì cách thức tích hợp với Spring để quản lý stateless session (vào thời điểm hiện tại). Mặc định khi mở một stateless session sẽ cấp phát mới một đối tượng java.sql.Connection (nếu bạn sử dụng phương thức openStatelessSession()), do đó nó sẽ gián tiếp tạo ra một transaction thứ hai. Bạn có thể tránh được việc này bằng cách sử dụng API của Hibernate như trong ví dụ trên, ta sẽ lấy được current connection và truyền đối số qua phương thức openStatelessSession(Connection connection). Ta thực hiện đóng session trong khối finally sẽ không có tác động đến kết nối vật lý vì nó được quản lý bởi Spring: chỉ có connection logic bị đóng và một connection logic mới được tạo ra khi mở một stateless session.

Như đã nói ở trước đó, Hibernate sẽ chạy trong chế độ tất cả tính năng bị vô hiệu và các đối tượng entity được trả về ở trạng thái tách rời. Nghĩa là, mỗi entity mà bạn chỉnh sửa thì bạn sẽ phải gọi phương thức statelessSession.update(entity) trực tiếp. Đầu tiên, ta sẽ thử chỉnh sửa một đối tượng entity:

new TransactionTemplate(txManager).execute(new TransactionCallback<Void>() {
    @Override
    public Void doInTransaction(TransactionStatus status) {
        sessionFactory.getCurrentSession().doWork(new Work() {
            @Override
            public void execute(Connection connection) throws SQLException {
                StatelessSession statelessSession = sessionFactory.openStatelessSession(connection);
                try {
                    DemoEntity demoEntity = (DemoEntity) statelessSession.createQuery("from DemoEntity where id = 1").uniqueResult();
                    demoEntity.setProperty("test");
                    statelessSession.update(demoEntity);
                } finally {
                    statelessSession.close();
                }
            }
        });
        return null;
    }
});

Ý tưởng là chúng ta sẽ thực hiện mở một stateless session với connection hiện tại, và như javadoc StatelessSession chỉ ra rằng sẽ không thực hiện ghi dữ liệu sau khi chỉnh sửa đổi dữ liệu, mỗi câu lệnh được thực hiện bởi stateless session sẽ được gửi trực tiếp đến database. Cuối cùng, khi transaction (được bắt đầu bởi TransactionTemplate) được commit thì kết quả sau khi được chỉnh sửa sẽ hiển thị trong database.

28 tháng 12, 2017

SQL Injection là gì? Ngăn chặn SQL Injection như thế nào?

1. SQL Injection là gì?

SQL Injection xảy ra khi kẻ tấn công có thể giả mạo các thao tác truy vấn để nhằm mục đích thực hiện một câu lệnh truy vấn SQL khác so với những gì mà nhà phát triển ứng dụng đã dự định ban đầu.

Khi thực thi câu lệnh truy vấn, chúng ta có hai tùy chọn cơ bản là:

  • Sử dụng statement (ví dụ, java.sql.Statement)
  • Hoặc sử dụng prepared statement (ví dụ, java.sql.PreparedStatement)

Khi xây dựng các câu truy vấn căn bản, nếu chúng ta thực hiện việc nối chuỗi thì cả hai loại java.sql.Statementjava.sql.PreparedStatement đều rất dễ bị tấn công SQL Injection.

Để có thể thực hiện một câu lệnh truy vấn thì trong hai lớp java.sql.Statementjava.sql.PreparedStatement có định nghĩa ra hai phương thức là:

  • executeQuery(String sql) để thực thi câu lệnh SQL SELECT
  • executeUpdate(String sql) để thực thi các câu lệnh SQL INSERT, UPDATE, DELETE

Tùy thuộc vào sự kết hợp của Statement/PreparedStatementexecuteQuery/executeUpdate, mà mục tiêu tấn công SQL Injection sẽ thay đổi, được thể hiện bằng các kịch bản dưới đây.

2. Statement và executeUpdate

Đây là sự kết hợp dễ bị tấn công nhất. Giả định, chúng ta có một phương thức dùng để cập nhật giá trị column review của một bản ghi thuộc bảng post_comment:

public void updatePostCommentReviewUsingStatement(Long id, String review) {
    doInJPA(entityManager -> {
        Session session = entityManager.unwrap(Session.class);
        session.doWork(connection -> {
            try(Statement statement = connection.createStatement()) {
                statement.executeUpdate(
                    "UPDATE post_comment " +
                    "SET review = '" + review + "' " +
                    "WHERE id = " + id);
            }
        });
    });
}

Và chúng ta sẽ thường gọi phương thức trên như sau:

updatePostCommentReviewUsingStatement(1L, "Awesome");

Một kẻ tấn công giả mạo chỉ đơn giả thực hiện cuộc tấn công SQL Injection như sau:

updatePostCommentReviewUsingStatement(1L, "'; DROP TABLE post_comment; -- '");

Và đay là những gì mà database sẽ thực thi:

Query:["UPDATE post_comment SET review = ''; DROP TABLE post_comment; -- '' WHERE id = 1"], Params:[]
  • Câu lệnh UPDATE sẽ được thực thi trước tiên
  • Sau đó sẽ là câu lệnh DROP
  • Cú pháp comment -- sẽ đảm bảo bỏ qua điều kiện trong mệnh đề WHERE trong phần còn lại của câu truy vấn.

Sau khi thực hiện tấn công SQL Injection trên, hãy thử truy vấn lại dữ liệu từ bảng post_comment để xem câu lệnh tấn công SQL Injection của chúng ta có thành công hay không.

ORACLE

Trên Oracle 11g, câu lệnh SQL Injection phía trên sẽ bị lỗi, vì JDBC driver không nhận ra dấu ;

Query:["UPDATE post_comment SET review = ''; DROP TABLE post_comment; -- '' WHERE id = 1"], Params:[]
WARN  [Alice]: o.h.e.j.s.SqlExceptionHelper - SQL Error: 911, SQLState: 22019
ERROR [Alice]: o.h.e.j.s.SqlExceptionHelper - ORA-00911: invalid character
 
Query:["select p.id as id1_1_0_, p.post_id as post_id3_1_0_, p.review as review2_1_0_ from post_comment p where p.id=?"], Params:[(1)]

SQL Server

Trên SQL Server 2014, câu lệnh SQL Injection sẽ được thực thi thành công và bảng post_comment đã bị xóa.

Query:["UPDATE post_comment SET review = ''; DROP TABLE post_comment; -- '' WHERE id = 1"], Params:[]
 
Query:["select p.id as id1_1_0_, p.post_id as post_id3_1_0_, p.review as review2_1_0_ from post_comment p where p.id=?"], Params:[(1)]
 
WARN  [Alice]: o.h.e.j.s.SqlExceptionHelper - SQL Error: 208, SQLState: S0002
ERROR [Alice]: o.h.e.j.s.SqlExceptionHelper - Invalid object name 'post_comment'.
INFO  [Alice]: o.h.e.i.DefaultLoadEventListener - HHH000327: Error performing load command : org.hibernate.exception.SQLGrammarException: could not extract ResultSet

PostgreSQL

Trên PostgreSQL 9.5, câu lệnh SQL Injection sẽ được thực thi thành công và bảng post_comment đã bị xóa.

Query:["UPDATE post_comment SET review = ''; DROP TABLE post_comment; -- '' WHERE id = 1"], Params:[]
 
Query:["select p.id as id1_1_0_, p.post_id as post_id3_1_0_, p.review as review2_1_0_ from post_comment p where p.id=?"], Params:[(1)]
WARN  [Alice]: o.h.e.j.s.SqlExceptionHelper - SQL Error: 0, SQLState: 42P01
ERROR [Alice]: o.h.e.j.s.SqlExceptionHelper - ERROR: relation "post_comment" does not exist

MySQL

Trên MySQL 5.7, câu lệnh SQL Injection sẽ bị lỗi vì JDBC driver không biên dịch đúng nhiều câu lệnh DML

Query:["UPDATE post_comment SET review = ''; DROP TABLE post_comment; -- '' WHERE id = 1"], Params:[]
WARN  [Alice]: o.h.e.j.s.SqlExceptionHelper - SQL Error: 1064, SQLState: 42000
ERROR [Alice]: o.h.e.j.s.SqlExceptionHelper - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'DROP TABLE post_comment; -- '' WHERE id = 1' at line 1
 
Query:["select p.id as id1_1_0_, p.post_id as post_id3_1_0_, p.review as review2_1_0_ from post_comment p where p.id=?"], Params:[(1)]

Mặc dù, lần tấn công SQL Injection đầu tiên không thành công trên tất cả các database, nhưng bạn sẽ nhận thấy rằng mọi cơ sở dữ liệu đều có ít nhất một biến thể của SQL Injection.

3. PreparedStatement và executeUpdate

Chúng ta sẽ thay đổi ví dụ trước để sử dụng PreparedStatement nhưng sẽ tránh sử dụng đến tham số ràng buộc

public void updatePostCommentReviewUsingPreparedStatement(Long id, String review) {
    doInJPA(entityManager -> {
        Session session = entityManager.unwrap(Session.class);
        session.doWork(connection -> {
            String sql = 
                "UPDATE post_comment " +
                "SET review = '" + review + "' " +
                "WHERE id = " + id;
            try(PreparedStatement statement = connection.prepareStatement(sql)) {
                statement.executeUpdate();
            }
        });
    });
}

Và thự hiện lại test case trước:

updatePostCommentReviewUsingPreparedStatement(
    1L, "'; DROP TABLE post_comment; -- '");
 
doInJPA(entityManager -> {
    PostComment comment = entityManager.find(
        PostComment.class, 1L);
    assertNotNull(comment);
});

Chúng ta cùng xem kết quả sau khi thực hiện tấn công SQL Injection

ORACLE

Trên Oracle 11g, câu lệnh SQL Injection phía trên sẽ bị lỗi, vì JDBC driver không nhận ra dấu ;

Query:["UPDATE post_comment SET review = ''; DROP TABLE post_comment; -- '' WHERE id = 1"], Params:[]
WARN  [Alice]: o.h.e.j.s.SqlExceptionHelper - SQL Error: 911, SQLState: 22019
ERROR [Alice]: o.h.e.j.s.SqlExceptionHelper - ORA-00911: invalid character
 
Query:["select p.id as id1_1_0_, p.post_id as post_id3_1_0_, p.review as review2_1_0_ from post_comment p where p.id=?"], Params:[(1)]

SQL Server

Trên SQL Server 2014, câu lệnh SQL Injection sẽ được thực thi thành công và bảng post_comment đã bị xóa.

Query:["UPDATE post_comment SET review = ''; DROP TABLE post_comment; -- '' WHERE id = 1"], Params:[]
 
Query:["select p.id as id1_1_0_, p.post_id as post_id3_1_0_, p.review as review2_1_0_ from post_comment p where p.id=?"], Params:[(1)]
 
WARN  [Alice]: o.h.e.j.s.SqlExceptionHelper - SQL Error: 208, SQLState: S0002
ERROR [Alice]: o.h.e.j.s.SqlExceptionHelper - Invalid object name 'post_comment'.
INFO  [Alice]: o.h.e.i.DefaultLoadEventListener - HHH000327: Error performing load command : org.hibernate.exception.SQLGrammarException: could not extract ResultSet

PostgreSQL

Trên PostgreSQL 9.5, câu lệnh SQL Injection sẽ được thực thi thành công, vì mặc định PreparedStatement chỉ mô phỏng câu lệnh trong giai đoạn chuẩn bị để chỉ phải thực thi câu truy vấn một lần.

Query:["UPDATE post_comment SET review = ''; DROP TABLE post_comment; -- '' WHERE id = 1"], Params:[]
 
Query:["select p.id as id1_1_0_, p.post_id as post_id3_1_0_, p.review as review2_1_0_ from post_comment p where p.id=?"], Params:[(1)]
WARN  [Alice]: o.h.e.j.s.SqlExceptionHelper - SQL Error: 0, SQLState: 42P01
ERROR [Alice]: o.h.e.j.s.SqlExceptionHelper - ERROR: relation "post_comment" does not exist

MySQL

Trên MySQL 5.7, câu lệnh SQL Injection sẽ bị lỗi vì JDBC driver không biên dịch đúng nhiều câu lệnh DML

Query:["UPDATE post_comment SET review = ''; DROP TABLE post_comment; -- '' WHERE id = 1"], Params:[]
WARN  [Alice]: o.h.e.j.s.SqlExceptionHelper - SQL Error: 1064, SQLState: 42000
ERROR [Alice]: o.h.e.j.s.SqlExceptionHelper - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'DROP TABLE post_comment; -- '' WHERE id = 1' at line 1
 
Query:["select p.id as id1_1_0_, p.post_id as post_id3_1_0_, p.review as review2_1_0_ from post_comment p where p.id=?"], Params:[(1)]

PreparedStatement không bảo vệ bạn khỏi một cuộc tấn công SQL Injection nếu bạn không sử dụng các tham số ràng buộc.

4. Ngăn chặn tấn công SQL Injection như thế nào?

Giải pháp rất đơn giản, bạn chỉ cần luôn chắc chắn sử dụng các tham số ràng buộc:

public PostComment getPostCommentByReview(String review) {
    return doInJPA(entityManager -> {
        return entityManager.createQuery(
            "select p " +
            "from PostComment p " +
            "where p.review = :review", PostComment.class)
        .setParameter("review", review)
        .getSingleResult();
    });
}

Bây giờ, khi cố thực hiện hack vào câu truy vấn trên:

getPostCommentByReview("1 AND 1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(10) )");

tấn công SQL Injection sẽ bị ngăn chặn:

Time:1, Query:["select postcommen0_.id as id1_1_, postcommen0_.post_id as post_id3_1_, postcommen0_.review as review2_1_ from post_comment postcommen0_ where postcommen0_.review=?"], Params:[(1 AND 1 >= ALL ( SELECT 1 FROM pg_locks, pg_sleep(10) ))]
 
javax.persistence.NoResultException: No entity found for query

2 tháng 4, 2016

Password confirm validator sử dụng Hibernate Validator

Cách tạo và kiểm tra tính hợp lệ của mật khẩu xác nhận khi đăng kí tài khoản sử dụng hibernate-validator

...
@Transient
private boolean passwordConfirmValid;
public void setPasswordConfirmValid(boolean passwordConfirmValid) {
this.passwordConfirmValid = passwordConfirmValid;
}
@AssertTrue(message = "Password do not match.")
public boolean isPasswordConfirmValid() {
if (this.password == null) {
passwordConfirmValid = this.password_again == null;
} else {
passwordConfirmValid = this.password_again.equals(this.password);
}
return passwordConfirmValid;
}
...
view raw Account.java hosted with ❤ by GitHub
...
<div class="form-group">
<sForm:label path="account.password" title="Password" cssClass="control-label col-sm-2">Password:</sForm:label>
<div class="col-sm-6">
<sForm:password path="account.password" title="Password" placeholder="Password" cssClass="form-control" />
<sForm:errors path="account.password" cssClass="error" />
</div>
</div>
<div class="form-group">
<sForm:label path="account.password_again" title="Password Again" class="control-label col-sm-2">
Password Again:</sForm:label>
<div class="col-sm-6">
<sForm:password path="account.password_again" title="Password Again" placeholder="Password Again"
class="form-control" />
<sForm:errors path="account.password_again" cssClass="error" />
<sForm:errors path="account.passwordConfirmValid" cssClass="error" />
</div>
</div>
...