在 Spring MVC 裡面做 redirect 再常見不過,但怎麼安全地傳遞參數,就有點講究了。之前有個專案因為 redirect 時沒處理好敏感資訊,差點出事。今天就完整介紹幾種傳遞參數的方式。
問題背景
假設用戶提交了一個表單,你的 controller 處理完後想要 redirect 到另一個頁面。但有時候你需要傳一些資料給下一個頁面,比如成功/失敗的訊息,或者使用者 ID。
最簡單的方式就是加到 URL query string:
1
| redirect:/order/confirm?orderId=12345&status=success
|
但這樣有問題:
- 敏感資訊會暴露在 URL 上
- 用戶可以直接改 URL query string 作弊
- 如果參數很多,URL 會很長很醜
更好的方式是用 RedirectAttributes。
RedirectAttributes 的兩種方式
方式一:addAttribute(加到 URL query string)
1 2 3 4 5 6 7
| @PostMapping("/order/create") public String createOrder(Order order, RedirectAttributes redirectAttributes) { int orderId = orderService.save(order); redirectAttributes.addAttribute("orderId", orderId); redirectAttributes.addAttribute("status", "success"); return "redirect:/order/confirm"; }
|
這樣會 redirect 到 /order/confirm?orderId=12345&status=success。
用途:傳遞不敏感的、可以被使用者看到的資訊。
方式二:addFlashAttribute(存在 session,一次性)
1 2 3 4 5 6 7 8 9 10
| @PostMapping("/order/create") public String createOrder(Order order, RedirectAttributes redirectAttributes) { int orderId = orderService.save(order); Order createdOrder = orderService.getOrderById(orderId); redirectAttributes.addFlashAttribute("message", "訂單建立成功!"); redirectAttributes.addFlashAttribute("order", createdOrder); return "redirect:/order/confirm"; }
|
redirect 之後,下一個 request 可以直接在 Model 裡拿到:
1 2 3 4 5 6 7 8 9 10
| @GetMapping("/order/confirm") public String confirm(Model model) { String message = (String) model.asMap().get("message"); Order order = (Order) model.asMap().get("order"); System.out.println(message); System.out.println(order); return "order/confirm"; }
|
或者在 Thymeleaf template 裡面直接用:
1 2 3
| <div th:if="${message}" class="alert alert-success"> <span th:text="${message}"></span> </div>
|
addAttribute vs addFlashAttribute
| 特性 |
addAttribute |
addFlashAttribute |
| 存放位置 |
URL query string |
HTTP session |
| 可見性 |
在 URL 上可見 |
不在 URL 上 |
| 生命週期 |
永久(直到手動改) |
一次性(redirect 後就消失) |
| 使用場景 |
篩選、分頁等參數 |
訊息、通知提示 |
| 敏感資訊 |
不適合 |
適合 |
實務例子
假設你有個刪除訂單的功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @PostMapping("/order/{id}/delete") public String deleteOrder(@PathVariable int id, RedirectAttributes redirectAttributes) { try { orderService.delete(id); redirectAttributes.addFlashAttribute("message", "訂單已刪除"); redirectAttributes.addFlashAttribute("type", "success"); } catch (OrderNotFoundException e) { redirectAttributes.addFlashAttribute("message", "訂單不存在"); redirectAttributes.addFlashAttribute("type", "error"); } return "redirect:/orders"; }
@GetMapping("/orders") public String listOrders(Model model) { model.addAttribute("orders", orderService.findAll()); return "orders/list"; }
|
Template 裡面:
1 2 3 4 5 6 7 8 9 10
| <div th:if="${message}" th:class="'alert alert-' + ${type}"> <span th:text="${message}"></span> </div>
<table> <tr th:each="order : ${orders}"> <td th:text="${order.id}"></td> <td th:text="${order.amount}"></td> </tr> </table>
|
四種 Redirect 的方式
除了 addAttribute 和 addFlashAttribute,Spring MVC 還提供了其他 redirect 的方式。
方式一:String 方式(最常用)
1 2 3 4 5
| @PostMapping("/save") public String save(User user) { userService.save(user); return "redirect:/users"; }
|
方式二:RedirectView
1 2 3 4 5 6 7
| @PostMapping("/save") public RedirectView save(User user) { userService.save(user); RedirectView redirectView = new RedirectView("/users"); redirectView.setContextRelative(true); return redirectView; }
|
setContextRelative 的作用是把 redirect 的路徑當成相對於應用的 context path。如果你的應用的 context path 是 /myapp,contextRelative(true) 就會 redirect 到 /myapp/users;contextRelative(false)(預設)則會 redirect 到 /users。
方式三:ModelAndView
1 2 3 4 5 6 7 8 9
| @PostMapping("/save") public ModelAndView save(User user) { userService.save(user); ModelAndView modelAndView = new ModelAndView("redirect:/users"); modelAndView.addObject("message", "使用者已儲存"); return modelAndView; }
|
方式四:HttpServletResponse
有時候你想要更細粒度的控制,可以直接用 HttpServletResponse:
1 2 3 4 5
| @PostMapping("/save") public void save(User user, HttpServletResponse response) throws IOException { userService.save(user); response.sendRedirect("/users"); }
|
用這種方式的時候,Spring 不會再處理 view,你要自己負責整個 response。
常見的坑
1. Flash attribute 被 redirect 吞掉
Flash attribute 會自動在 redirect 後消失。如果你不小心做了多次 redirect,會丟掉資料:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @PostMapping("/step1") public String step1(RedirectAttributes redirectAttributes) { redirectAttributes.addFlashAttribute("message", "Step 1 done"); return "redirect:/step2"; }
@GetMapping("/step2") public String step2(RedirectAttributes redirectAttributes) { redirectAttributes.addFlashAttribute("message", "Step 2 done"); return "redirect:/result"; }
|
解法就是只在最後一個 step 前加 flash attribute。
2. addAttribute 的參數沒有被正確編碼
如果參數含有特殊字元,可能會被破壞:
1 2 3
| redirectAttributes.addAttribute("name", "小王&小李");
|
Spring 會自動幫你編碼,所以通常不是問題。但如果你用 String.format 手動組 URL,要小心:
1 2 3 4 5 6
| return String.format("redirect:/users?name=%s", name);
redirectAttributes.addAttribute("name", name); return "redirect:/users";
|
3. POST-Redirect-GET 模式
如果表單提交之後直接 forward(不 redirect),使用者按 F5 重新整理會重複提交表單。正確的做法是 POST 之後 redirect,然後 GET 新頁面:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @PostMapping("/order/create") public String createOrder(Order order, RedirectAttributes redirectAttributes) { int orderId = orderService.save(order); return "redirect:/order/" + orderId; }
@GetMapping("/order/{id}") public String getOrder(@PathVariable int id, Model model) { Order order = orderService.getOrderById(id); model.addAttribute("order", order); return "order/detail"; }
|
URI Template 變數
有時候你想要 redirect 到有 path variable 的 URL,可以這樣做:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @PostMapping("/order") public String createOrder(Order order, RedirectAttributes redirectAttributes) { int orderId = orderService.save(order); return String.format("redirect:/order/%d", orderId); }
@PostMapping("/order") public String createOrder(Order order, RedirectAttributes redirectAttributes) { int orderId = orderService.save(order); redirectAttributes.addAttribute("id", orderId); return "redirect:/order/{id}"; }
|
重點整理
- addAttribute - 加到 URL query string,適合不敏感的參數
- addFlashAttribute - 存在 session,一次性,適合訊息提示
- Flash attribute 只活一次 redirect,刷新頁面就消失了
- 四種 redirect 方式:String、RedirectView、ModelAndView、HttpServletResponse
- POST 之後一定要 redirect,不要 forward,防止表單重複提交
- 參數編碼交給 Spring 處理,不要手動組 URL
用 RedirectAttributes 搞定 redirect 的參數傳遞,代碼會更乾淨更安全。
你的鼓勵將被轉換為我明天繼續加班的動力(真的)。 ❤️