React.js 和 Spring Data REST(三)

2022-12-29 14:28:57 來源:51CTO博客

第 4 部分 - 事件

在上一節(jié),您引入了條件更新以避免在編輯相同數(shù)據(jù)時與其他用戶發(fā)生沖突。您還學習了如何使用樂觀鎖定對后端的數(shù)據(jù)進行版本控制。如果有人編輯了同一記錄,您會收到通知,以便您可以刷新頁面并獲取更新。


(資料圖)

很好。但是你知道什么更好嗎?讓 UI 在其他人更新資源時動態(tài)響應。

在本節(jié)中,您將學習如何使用Spring Data REST的內(nèi)置事件系統(tǒng)來檢測后端中的更改,并通過Spring的WebSocket支持向所有用戶發(fā)布更新。然后,您將能夠在數(shù)據(jù)更新時動態(tài)調(diào)整客戶端。

隨意獲取代碼從此存儲庫并繼續(xù)操作。本節(jié)基于上一節(jié)的應用程序,并添加了額外的內(nèi)容。

將 Spring WebSocket 支持添加到項目中

在開始之前,您需要將依賴項添加到項目的pom.xml文件中:

  org.springframework.boot  spring-boot-starter-websocket

這種依賴關系引入了Spring Boot的WebSocket啟動器。

使用 Spring 配置 WebSockets

Spring帶有強大的WebSocket支持.要認識到的一件事是,WebSocket是一個非常低級的協(xié)議。它只不過提供了在客戶端和服務器之間傳輸數(shù)據(jù)的方法。建議使用子協(xié)議(本節(jié)為 STOMP)對數(shù)據(jù)和路由進行實際編碼。

以下代碼在服務器端配置 WebSocket 支持:

@Component@EnableWebSocketMessageBroker (1)public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer { (2)  static final String MESSAGE_PREFIX = "/topic"; (3)  @Override  public void registerStompEndpoints(StompEndpointRegistry registry) { (4)    registry.addEndpoint("/payroll").withSockJS();  }  @Override  public void configureMessageBroker(MessageBrokerRegistry registry) { (5)    registry.enableSimpleBroker(MESSAGE_PREFIX);    registry.setApplicationDestinationPrefixes("/app");  }}

?1

??@EnableWebSocketMessageBroker??打開 WebSocket 支持。

?2

??WebSocketMessageBrokerConfigurer??提供方便的基類來配置基本功能。

?3

MESSAGE_PREFIX是您將附加到每條消息路由前面的前綴。

?4

??registerStompEndpoints()???用于在后端為客戶端和服務器配置端點以鏈接 ()。??/payroll??

?5

??configureMessageBroker()??用于配置用于在服務器和客戶端之間中繼消息的代理。

使用此配置,您現(xiàn)在可以利用 Spring Data REST 事件并通過 WebSocket 發(fā)布它們。

訂閱 Spring 數(shù)據(jù) REST 事件

Spring Data REST 生成幾個應用程序事件基于存儲庫上發(fā)生的操作。以下代碼演示如何訂閱其中一些事件:

@Component@RepositoryEventHandler(Employee.class) (1)public class EventHandler {  private final SimpMessagingTemplate websocket; (2)  private final EntityLinks entityLinks;  @Autowired  public EventHandler(SimpMessagingTemplate websocket, EntityLinks entityLinks) {    this.websocket = websocket;    this.entityLinks = entityLinks;  }  @HandleAfterCreate (3)  public void newEmployee(Employee employee) {    this.websocket.convertAndSend(        MESSAGE_PREFIX + "/newEmployee", getPath(employee));  }  @HandleAfterDelete (3)  public void deleteEmployee(Employee employee) {    this.websocket.convertAndSend(        MESSAGE_PREFIX + "/deleteEmployee", getPath(employee));  }  @HandleAfterSave (3)  public void updateEmployee(Employee employee) {    this.websocket.convertAndSend(        MESSAGE_PREFIX + "/updateEmployee", getPath(employee));  }  /**   * Take an {@link Employee} and get the URI using Spring Data REST"s {@link EntityLinks}.   *   * @param employee   */  private String getPath(Employee employee) {    return this.entityLinks.linkForItemResource(employee.getClass(),        employee.getId()).toUri().getPath();  }}

?1

??@RepositoryEventHandler(Employee.class)??標記此類以基于員工捕獲事件。

?2

??SimpMessagingTemplate???并從應用程序上下文自動連線。??EntityLinks??

?3

批注標記需要偵聽事件的方法。這些方法必須是公共的。??@HandleXYZ??

這些處理程序方法中的每一個都調(diào)用以通過 WebSocket 傳輸消息。這是一種發(fā)布-訂閱方法,以便將一條消息中繼到每個連接的使用者。??SimpMessagingTemplate.convertAndSend()??

每條消息的路由是不同的,允許將多條消息發(fā)送到客戶端上的不同接收方,同時只需要一個開放的 WebSocket — 這是一種資源節(jié)約型方法。

??getPath()??使用 Spring Data REST 查找給定類類型和 id 的路徑。為了滿足客戶端的需求,此對象被轉換為 Java URI,并提取其路徑。??EntityLinks????Link??

??EntityLinks??附帶了幾種實用工具方法,以編程方式查找各種資源的路徑,無論是單個資源還是集合路徑。

實質(zhì)上,您正在偵聽創(chuàng)建、更新和刪除事件,并在它們完成后向所有客戶端發(fā)送有關它們的通知。您還可以在此類操作發(fā)生之前攔截它們,并可能記錄它們,出于某種原因阻止它們,或者用額外的信息裝飾域對象。(在下一節(jié)中,我們將看到一個方便的用法。

配置 JavaScript WebSocket

下一步是編寫一些客戶端代碼來使用 WebSocket 事件。主應用程序中的以下塊拉入一個模塊:

var stompClient = require("./websocket-listener")

該模塊如下所示:

"use strict";const SockJS = require("sockjs-client"); (1)require("stompjs"); (2)function register(registrations) {  const socket = SockJS("/payroll"); (3)  const stompClient = Stomp.over(socket);  stompClient.connect({}, function(frame) {    registrations.forEach(function (registration) { (4)      stompClient.subscribe(registration.route, registration.callback);    });  });}module.exports.register = register;

?1

拉入 SockJS JavaScript 庫,用于通過 WebSockets 進行對話。

?2

拉入 stomp-websocket JavaScript 庫以使用 STOMP 子協(xié)議。

?3

將 WebSocket 指向應用程序的終結點。??/payroll??

?4

遍歷提供的數(shù)組,以便每個數(shù)組都可以在消息到達時訂閱回調(diào)。??registrations??

每個注冊條目都有一個和一個 .在下一節(jié)中,您可以了解如何注冊事件處理程序。??route????callback??

注冊 WebSocket 事件

在 React 中,組件的函數(shù)在 DOM 中渲染后被調(diào)用。這也是注冊 WebSocket 事件的合適時機,因為該組件現(xiàn)已聯(lián)機并準備好開展業(yè)務。以下代碼執(zhí)行此操作:??componentDidMount()??

componentDidMount() {  this.loadFromServer(this.state.pageSize);  stompClient.register([    {route: "/topic/newEmployee", callback: this.refreshAndGoToLastPage},    {route: "/topic/updateEmployee", callback: this.refreshCurrentPage},    {route: "/topic/deleteEmployee", callback: this.refreshCurrentPage}  ]);}

第一行與之前相同,其中所有員工都是使用頁面大小從服務器獲取的。第二行顯示為 WebSocket 事件注冊的 JavaScript 對象數(shù)組,每個對象都帶有 a 和 .??route????callback??

創(chuàng)建新員工時,行為是刷新數(shù)據(jù)集,然后使用分頁鏈接導航到最后一頁。為什么要在導航到末尾之前刷新數(shù)據(jù)?添加新記錄可能會導致創(chuàng)建新頁面。雖然可以計算這是否會發(fā)生,但它顛覆了超媒體的觀點。與其將自定義的頁數(shù)拼湊在一起,不如使用現(xiàn)有鏈接,并且只有在有性能驅動的原因時才走這條路。

更新或刪除員工時,行為是刷新當前頁面。更新記錄時,它會影響您正在查看的頁面。當您刪除當前頁面上的記錄時,下一頁中的記錄將被拉入當前頁面 - 因此還需要刷新當前頁面。

這些 WebSocket 消息不需要以 開頭。這是指示發(fā)布-訂閱語義的常見約定。??/topic??

在下一節(jié)中,您可以看到執(zhí)行這些操作的實際操作。

對 WebSocket 事件做出反應并更新 UI 狀態(tài)

以下代碼塊包含用于在收到 WebSocket 事件時更新 UI 狀態(tài)的兩個回調(diào):

refreshAndGoToLastPage(message) {  follow(client, root, [{    rel: "employees",    params: {size: this.state.pageSize}  }]).done(response => {    if (response.entity._links.last !== undefined) {      this.onNavigate(response.entity._links.last.href);    } else {      this.onNavigate(response.entity._links.self.href);    }  })}refreshCurrentPage(message) {  follow(client, root, [{    rel: "employees",    params: {      size: this.state.pageSize,      page: this.state.page.number    }  }]).then(employeeCollection => {    this.links = employeeCollection.entity._links;    this.page = employeeCollection.entity.page;    return employeeCollection.entity._embedded.employees.map(employee => {      return client({        method: "GET",        path: employee._links.self.href      })    });  }).then(employeePromises => {    return when.all(employeePromises);  }).then(employees => {    this.setState({      page: this.page,      employees: employees,      attributes: Object.keys(this.schema.properties),      pageSize: this.state.pageSize,      links: this.links    });  });}

??refreshAndGoToLastPage()??使用熟悉的函數(shù)導航到應用了參數(shù)的鏈接,插入 .收到響應后,然后調(diào)用最后一部分中的相同函數(shù),并跳轉到最后一頁,即將找到新記錄的頁面。??follow()????employees????size????this.state.pageSize????onNavigate()??

??refreshCurrentPage()??也使用該函數(shù),但適用于 和 。這將獲取您當前正在查看的同一頁面并相應地更新狀態(tài)。??follow()????this.state.pageSize????size????this.state.page.number????page??

此行為告知每個客戶端在發(fā)送更新或刪除消息時刷新其當前頁面。他們的當前頁面可能與當前事件無關。但是,要弄清楚這一點可能很棘手。如果刪除的記錄在第二頁上,而您正在查看第三頁,該怎么辦?每個條目都會改變。但這是想要的行為嗎?或。也許不是。

將狀態(tài)管理移出本地更新

在完成本節(jié)之前,有一些東西需要識別。您剛剛為 UI 中的狀態(tài)添加了更新的新方法:當 WebSocket 消息到達時。但是更新狀態(tài)的舊方法仍然存在。

若要簡化代碼的狀態(tài)管理,請刪除舊方法。換句話說,提交您的 、 和調(diào)用,但不要使用其結果來更新 UI 的狀態(tài)。相反,請等待 WebSocket 事件回旋,然后執(zhí)行更新。??POST????PUT????DELETE??

以下代碼塊顯示了與上一節(jié)相同的函數(shù),只是進行了簡化:??onCreate()??

onCreate(newEmployee) {  follow(client, root, ["employees"]).done(response => {    client({      method: "POST",      path: response.entity._links.self.href,      entity: newEmployee,      headers: {"Content-Type": "application/json"}    })  })}

在這里,函數(shù)用于獲取鏈接,然后應用操作。注意怎么有 沒有 或 ,像以前一樣?用于偵聽更新的事件處理程序現(xiàn)在位于 中,您剛剛查看了該事件處理程序。??follow()????employees????POST????client({method: "GET" …})????then()????done()????refreshAndGoToLastPage()??

將一切整合在一起

完成所有這些修改后,啟動應用程序 () 并使用它。打開兩個瀏覽器選項卡并調(diào)整大小,以便您可以同時看到它們。開始在一個選項卡中進行更新,看看它們?nèi)绾瘟⒓锤铝硪粋€選項卡。打開手機并訪問同一頁面。找一個朋友,讓那個人做同樣的事情。您可能會發(fā)現(xiàn)這種類型的動態(tài)更新更敏銳。??./mvnw spring-boot:run??

想要挑戰(zhàn)嗎?嘗試上一節(jié)中的練習,在兩個不同的瀏覽器選項卡中打開同一記錄。嘗試在一個中更新它,而在另一個中看不到它更新。如果可能,條件代碼仍應保護您。但要做到這一點可能會更棘手!??PUT??

回顧

在本節(jié)中,您將:

配置了Spring的WebSocket支持和SockJS回退。訂閱了從 Spring Data REST 創(chuàng)建、更新和刪除事件以動態(tài)更新 UI。發(fā)布了受影響的 REST 資源的 URI 以及上下文消息(“/topic/newEmployee”、“/topic/updateEmployee”等)。在 UI 中注冊 WebSocket 偵聽器以偵聽這些事件。將偵聽器連接到處理程序以更新 UI 狀態(tài)。

有了所有這些功能,可以輕松地并排運行兩個瀏覽器,并查看如何將一個瀏覽器更新到另一個瀏覽器。

問題?

雖然多個顯示器可以很好地更新,但需要完善精確的行為。例如,創(chuàng)建一個新用戶將導致所有用戶跳到最后。關于如何處理這個問題的任何想法?

分頁很有用,但它提供了一個棘手的管理狀態(tài)。此示例應用程序的成本很低,而且 React 在更新 DOM 方面非常有效,而不會在 UI 中引起大量閃爍。但是對于更復雜的應用程序,并非所有這些方法都適合。

在設計時考慮分頁時,您必須確定客戶端之間的預期行為是什么,以及是否需要更新。根據(jù)您的要求和系統(tǒng)性能,現(xiàn)有的導航超媒體可能就足夠了。

第 5 部分 - 保護 UI 和 API

在上一節(jié),您使用Spring Data REST的內(nèi)置事件處理程序和Spring Framework的WebSocket支持使應用程序動態(tài)響應其他用戶的更新。但是,如果不保護整個事情,則任何應用程序都是不完整的,因此只有適當?shù)挠脩舨拍茉L問UI及其背后的資源。

隨意獲取代碼從此存儲庫并繼續(xù)操作。本部分基于上一節(jié)的應用,添加了額外的內(nèi)容。

將 Spring 安全性添加到項目中

在開始之前,您需要將幾個依賴項添加到項目的pom.xml文件中:

  org.springframework.boot  spring-boot-starter-security  org.thymeleaf.extras  thymeleaf-extras-springsecurity5

這帶來了Spring Boot的Spring Security啟動器以及一些額外的Thymeleaf標簽,以便在網(wǎng)頁中進行安全查找。

定義安全模型

在過去的部分中,您使用了一個不錯的工資單系統(tǒng)。在后端聲明內(nèi)容并讓Spring Data REST完成繁重的工作很方便。下一步是模擬需要建立安全控制的系統(tǒng)。

如果這是一個工資單系統(tǒng),那么只有經(jīng)理才能訪問它。因此,通過對對象進行建模來開始工作:??Manager??

@Entitypublic class Manager {  public static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); (1)  private @Id @GeneratedValue Long id; (2)  private String name; (2)  private @JsonIgnore String password; (2)  private String[] roles; (2)  public void setPassword(String password) { (3)    this.password = PASSWORD_ENCODER.encode(password);  }  protected Manager() {}  public Manager(String name, String password, String... roles) {    this.name = name;    this.setPassword(password);    this.roles = roles;  }  @Override  public boolean equals(Object o) {    if (this == o) return true;    if (o == null || getClass() != o.getClass()) return false;    Manager manager = (Manager) o;    return Objects.equals(id, manager.id) &&      Objects.equals(name, manager.name) &&      Objects.equals(password, manager.password) &&      Arrays.equals(roles, manager.roles);  }  @Override  public int hashCode() {    int result = Objects.hash(id, name, password);    result = 31 * result + Arrays.hashCode(roles);    return result;  }  public Long getId() {    return id;  }  public void setId(Long id) {    this.id = id;  }  public String getName() {    return name;  }  public void setName(String name) {    this.name = name;  }  public String getPassword() {    return password;  }  public String[] getRoles() {    return roles;  }  public void setRoles(String[] roles) {    this.roles = roles;  }  @Override  public String toString() {    return "Manager{" +      "id=" + id +      ", name="" + name + "\"" +      ", roles=" + Arrays.toString(roles) +      "}";  }}

?1

??PASSWORD_ENCODER??是加密新密碼或獲取密碼輸入并在比較之前對其進行加密的方法。

?2

??id???、、、 和定義限制訪問所需的參數(shù)。??name????password????roles??

?3

自定義方法可確保密碼永遠不會以明文形式存儲。??setPassword()??

在設計安全層時,要記住一件關鍵的事情。保護正確的數(shù)據(jù)位(如密碼),不要讓它們打印到控制臺、日志中或通過 JSON 序列化導出。

??@JsonIgnore??應用于密碼字段可防止杰克遜序列化此字段。

創(chuàng)建經(jīng)理的存儲庫

Spring Data非常擅長管理實體。為什么不創(chuàng)建一個存儲庫來處理這些管理器呢?以下代碼執(zhí)行此操作:

@RepositoryRestResource(exported = false)public interface ManagerRepository extends Repository {  Manager save(Manager manager);  Manager findByName(String name);}

而不是擴展通常的,你不需要那么多的方法。相反,您需要保存數(shù)據(jù)(也用于更新),并且需要查找現(xiàn)有用戶。因此,您可以使用Spring Data Common的最小標記接口。它沒有預定義的操作。??CrudRepository????Repository??

默認情況下,Spring Data REST將導出它找到的任何存儲庫。您不希望此存儲庫公開用于 REST 操作!應用批注以阻止其導出。這可以防止提供存儲庫及其元數(shù)據(jù)。??@RepositoryRestResource(exported = false)??

將員工與經(jīng)理聯(lián)系起來

建模安全性的最后一點是將員工與經(jīng)理相關聯(lián)。在此域中,一個員工可以有一個經(jīng)理,而一個經(jīng)理可以有多個員工。以下代碼定義該關系:

@Entitypublic class Employee {  private @Id @GeneratedValue Long id;  private String firstName;  private String lastName;  private String description;  private @Version @JsonIgnore Long version;  private @ManyToOne Manager manager; (1)  private Employee() {}  public Employee(String firstName, String lastName, String description, Manager manager) { (2)    this.firstName = firstName;    this.lastName = lastName;    this.description = description;    this.manager = manager;  }  @Override  public boolean equals(Object o) {    if (this == o) return true;    if (o == null || getClass() != o.getClass()) return false;    Employee employee = (Employee) o;    return Objects.equals(id, employee.id) &&      Objects.equals(firstName, employee.firstName) &&      Objects.equals(lastName, employee.lastName) &&      Objects.equals(description, employee.description) &&      Objects.equals(version, employee.version) &&      Objects.equals(manager, employee.manager);  }  @Override  public int hashCode() {    return Objects.hash(id, firstName, lastName, description, version, manager);  }  public Long getId() {    return id;  }  public void setId(Long id) {    this.id = id;  }  public String getFirstName() {    return firstName;  }  public void setFirstName(String firstName) {    this.firstName = firstName;  }  public String getLastName() {    return lastName;  }  public void setLastName(String lastName) {    this.lastName = lastName;  }  public String getDescription() {    return description;  }  public void setDescription(String description) {    this.description = description;  }  public Long getVersion() {    return version;  }  public void setVersion(Long version) {    this.version = version;  }  public Manager getManager() {    return manager;  }  public void setManager(Manager manager) {    this.manager = manager;  }  @Override  public String toString() {    return "Employee{" +      "id=" + id +      ", firstName="" + firstName + "\"" +      ", lastName="" + lastName + "\"" +      ", description="" + description + "\"" +      ", version=" + version +      ", manager=" + manager +      "}";  }}

?1

管理器屬性由 JPA 的屬性鏈接。 不需要 ,因為您尚未定義查找該需求。??@ManyToOne????Manager????@OneToMany??

?2

實用程序構造函數(shù)調(diào)用已更新以支持初始化。

確保員工與經(jīng)理的關系

在定義安全策略時,Spring 安全性支持多種選項。在本節(jié)中,您希望限制以下內(nèi)容,以便只有經(jīng)理可以查看員工工資單數(shù)據(jù),并且保存、更新和刪除操作僅限于員工的經(jīng)理。換句話說,任何經(jīng)理都可以登錄并查看數(shù)據(jù),但只有給定員工的經(jīng)理才能進行任何更改。以下代碼可實現(xiàn)這些目標:

@PreAuthorize("hasRole("ROLE_MANAGER")") (1)public interface EmployeeRepository extends PagingAndSortingRepository {  @Override  @PreAuthorize("#employee?.manager == null or #employee?.manager?.name == authentication?.name")  Employee save(@Param("employee") Employee employee);  @Override  @PreAuthorize("@employeeRepository.findById(#id)?.manager?.name == authentication?.name")  void deleteById(@Param("id") Long id);  @Override  @PreAuthorize("#employee?.manager?.name == authentication?.name")  void delete(@Param("employee") Employee employee);}

?1

??@PreAuthorize???在界面頂部限制對具有 .??ROLE_MANAGER??

在 上,員工的經(jīng)理為 null(在未分配經(jīng)理時首次創(chuàng)建新員工),或者員工的經(jīng)理姓名與當前經(jīng)過身份驗證的用戶名匹配。在這里,您正在使用??save()??Spring Security 的 SpEL 表達式以定義訪問權限。它帶有一個方便的屬性導航器來處理空檢查。同樣重要的是要注意使用 on 參數(shù)將 HTTP 操作與方法鏈接起來。???.????@Param(…)??

在 上,該方法可以訪問員工,或者如果它只有一個 ,則必須在應用程序上下文中找到 ,執(zhí)行 ,并根據(jù)當前經(jīng)過身份驗證的用戶檢查經(jīng)理。??delete()????id????employeeRepository????findOne(id)??

編寫服務??UserDetails??

與安全性集成的一個常見點是定義 .這是將用戶的數(shù)據(jù)存儲連接到 Spring 安全性界面的方法。Spring 安全性需要一種方法來查找用戶以進行安全檢查,這就是橋梁。值得慶幸的是,使用Spring Data,工作量非常小:??UserDetailsService??

@Componentpublic class SpringDataJpaUserDetailsService implements UserDetailsService {  private final ManagerRepository repository;  @Autowired  public SpringDataJpaUserDetailsService(ManagerRepository repository) {    this.repository = repository;  }  @Override  public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {    Manager manager = this.repository.findByName(name);    return new User(manager.getName(), manager.getPassword(),        AuthorityUtils.createAuthorityList(manager.getRoles()));  }}

??SpringDataJpaUserDetailsService??實現(xiàn) Spring 安全的 .該接口有一種方法:。此方法旨在返回一個對象,以便 Spring 安全性可以查詢用戶的信息。??UserDetailsService????loadUserByUsername()????UserDetails??

因為您有一個 ,所以不需要編寫任何 SQL 或 JPA 表達式來獲取這些所需的數(shù)據(jù)。在此類中,它通過構造函數(shù)注入自動連接。??ManagerRepository??

??loadUserByUsername()??點擊您剛才編寫的自定義查找器。然后,它填充一個 Spring 安全實例,該實例實現(xiàn)接口。您還使用 Spring Securiy 從基于字符串的角色數(shù)組過渡到 Java 類型的 Java。??findByName()????User????UserDetails????AuthorityUtils????List????GrantedAuthority??

連接您的安全策略

應用于存儲庫的表達式是訪問規(guī)則。如果沒有安全策略,這些規(guī)則是徒勞的:??@PreAuthorize??

@Configuration@EnableWebSecurity (1)@EnableGlobalMethodSecurity(prePostEnabled = true) (2)public class SecurityConfiguration extends WebSecurityConfigurerAdapter { (3)  @Autowired  private SpringDataJpaUserDetailsService userDetailsService; (4)  @Override  protected void configure(AuthenticationManagerBuilder auth) throws Exception {    auth      .userDetailsService(this.userDetailsService)        .passwordEncoder(Manager.PASSWORD_ENCODER);  }  @Override  protected void configure(HttpSecurity http) throws Exception { (5)    http      .authorizeRequests()        .antMatchers("/built/**", "/main.css").permitAll()        .anyRequest().authenticated()        .and()      .formLogin()        .defaultSuccessUrl("/", true)        .permitAll()        .and()      .httpBasic()        .and()      .csrf().disable()      .logout()        .logoutSuccessUrl("/");  }}

這段代碼非常復雜,因此我們將逐步介紹它,首先討論注釋和 API。然后我們將討論它定義的安全策略。

?1

??@EnableWebSecurity??告訴 Spring Boot 放棄其自動配置的安全策略并改用此策略。對于快速演示,自動配置的安全性是可以的。但對于任何真實的東西,你應該自己寫政策。

?2

??@EnableGlobalMethodSecurity???使用 Spring 安全性的復雜功能打開方法級安全性@Pre和注釋@Post.

?3

它擴展了,一個方便編寫策略的基類。??WebSecurityConfigurerAdapter??

?4

它通過場注入自動連接,然后通過該方法將其插入。發(fā)件人也已設置。??SpringDataJpaUserDetailsService????configure(AuthenticationManagerBuilder)????PASSWORD_ENCODER????Manager??

?5

關鍵安全策略是用純 Java 編寫的,帶有方法調(diào)用。??configure(HttpSecurity)??

安全策略規(guī)定使用前面定義的訪問規(guī)則授權所有請求:

中列出的路徑被授予無條件訪問權限,因為沒有理由阻止靜態(tài) Web 資源。antMatchers()任何與該策略不匹配的內(nèi)容都屬于 ,這意味著它需要身份驗證。anyRequest().authenticated()設置這些訪問規(guī)則后,Spring 安全性將被告知使用基于表單的身份驗證(默認為成功時)并授予對登錄頁面的訪問權限。/基本登錄也配置為禁用 CSRF。這主要用于演示,不建議用于未經(jīng)仔細分析的生產(chǎn)系統(tǒng)。注銷配置為將用戶帶到 。/

當您嘗試使用 curl 時,BASIC 身份驗證非常方便。使用 curl 訪問基于表單的系統(tǒng)是令人生畏的。重要的是要認識到,通過 HTTP(而不是 HTTPS)使用任何機制進行身份驗證會使您面臨通過網(wǎng)絡嗅探憑據(jù)的風險。CSRF 是一個很好的協(xié)議,可以保持完整。禁用它以使與 BASIC 和 curl 的交互更容易。在生產(chǎn)中,最好保持打開狀態(tài)。

自動添加安全詳細信息

良好用戶體驗的一部分是應用程序可以自動應用上下文。在此示例中,如果登錄的經(jīng)理創(chuàng)建新的員工記錄,則該經(jīng)理擁有該記錄是有意義的。使用Spring Data REST的事件處理程序,用戶不需要顯式鏈接它。它還可確保用戶不會意外地將記錄分配給錯誤的經(jīng)理。為我們處理:??SpringDataRestEventHandler??

@Component@RepositoryEventHandler(Employee.class) (1)public class SpringDataRestEventHandler {  private final ManagerRepository managerRepository;  @Autowired  public SpringDataRestEventHandler(ManagerRepository managerRepository) {    this.managerRepository = managerRepository;  }  @HandleBeforeCreate  @HandleBeforeSave  public void applyUserInformationUsingSecurityContext(Employee employee) {    String name = SecurityContextHolder.getContext().getAuthentication().getName();    Manager manager = this.managerRepository.findByName(name);    if (manager == null) {      Manager newManager = new Manager();      newManager.setName(name);      newManager.setRoles(new String[]{"ROLE_MANAGER"});      manager = this.managerRepository.save(newManager);    }    employee.setManager(manager);  }}

?1

??@RepositoryEventHandler(Employee.class)???將此事件處理程序標記為僅適用于對象。批注使您有機會在傳入記錄寫入數(shù)據(jù)庫之前對其進行更改。??Employee????@HandleBeforeCreate????Employee??

在這種情況下,可以查找當前用戶的安全上下文以獲取用戶的名稱。然后,您可以使用關聯(lián)管理器查找該管理器并將其應用于該管理器。如果系統(tǒng)中尚不存在該人員,則有一些額外的粘附代碼可以創(chuàng)建新經(jīng)理。但是,這主要是為了支持數(shù)據(jù)庫的初始化。在實際的生產(chǎn)系統(tǒng)中,應刪除該代碼,而是依靠 DBA 或安全運營團隊來正確維護用戶數(shù)據(jù)存儲。??findByName()??

預加載管理器數(shù)據(jù)

加載經(jīng)理并將員工鏈接到這些經(jīng)理非常簡單:

@Componentpublic class DatabaseLoader implements CommandLineRunner {  private final EmployeeRepository employees;  private final ManagerRepository managers;  @Autowired  public DatabaseLoader(EmployeeRepository employeeRepository,              ManagerRepository managerRepository) {    this.employees = employeeRepository;    this.managers = managerRepository;  }  @Override  public void run(String... strings) throws Exception {    Manager greg = this.managers.save(new Manager("greg", "turnquist",              "ROLE_MANAGER"));    Manager oliver = this.managers.save(new Manager("oliver", "gierke",              "ROLE_MANAGER"));    SecurityContextHolder.getContext().setAuthentication(      new UsernamePasswordAuthenticationToken("greg", "doesn"t matter",        AuthorityUtils.createAuthorityList("ROLE_MANAGER")));    this.employees.save(new Employee("Frodo", "Baggins", "ring bearer", greg));    this.employees.save(new Employee("Bilbo", "Baggins", "burglar", greg));    this.employees.save(new Employee("Gandalf", "the Grey", "wizard", greg));    SecurityContextHolder.getContext().setAuthentication(      new UsernamePasswordAuthenticationToken("oliver", "doesn"t matter",        AuthorityUtils.createAuthorityList("ROLE_MANAGER")));    this.employees.save(new Employee("Samwise", "Gamgee", "gardener", oliver));    this.employees.save(new Employee("Merry", "Brandybuck", "pony rider", oliver));    this.employees.save(new Employee("Peregrin", "Took", "pipe smoker", oliver));    SecurityContextHolder.clearContext();  }}

一個問題是,當這個加載器運行時,Spring Security 處于活動狀態(tài),訪問規(guī)則完全有效。因此,要保存員工數(shù)據(jù),您必須使用 Spring 安全性的 API 使用正確的名稱和角色對此加載器進行身份驗證。最后,將清除安全上下文。??setAuthentication()??

瀏覽您的安全 REST 服務

完成所有這些修改后,您可以啟動應用程序 () 并使用以下 curl(與其輸出一起顯示)檢查修改:??./mvnw spring-boot:run??

$ curl -v -u greg:turnquist localhost:8080/api/employees/1*   Trying ::1...* Connected to localhost (::1) port 8080 (#0)* Server auth using Basic with user "greg"> GET /api/employees/1 HTTP/1.1> Host: localhost:8080> Authorization: Basic Z3JlZzp0dXJucXVpc3Q=> User-Agent: curl/7.43.0> Accept: */*>< HTTP/1.1 200 OK< Server: Apache-Coyote/1.1< X-Content-Type-Options: nosniff< X-XSS-Protection: 1; mode=block< Cache-Control: no-cache, no-store, max-age=0, must-revalidate< Pragma: no-cache< Expires: 0< X-Frame-Options: DENY< Set-Cookie: JSESSIONID=E27F929C1836CC5BABBEAB78A548DF8C; Path=/; HttpOnly< ETag: "0"< Content-Type: application/hal+json;charset=UTF-8< Transfer-Encoding: chunked< Date: Tue, 25 Aug 2015 15:57:34 GMT<{  "firstName" : "Frodo",  "lastName" : "Baggins",  "description" : "ring bearer",  "manager" : {    "name" : "greg",    "roles" : [ "ROLE_MANAGER" ]  },  "_links" : {    "self" : {      "href" : "http://localhost:8080/api/employees/1"    }  }}

這顯示了比您在第一部分中看到的更多細節(jié)。首先,Spring Security 會打開多個 HTTP 協(xié)議來抵御各種媒介(Pragma、Expires、X-Frame-Options 等)。您還將頒發(fā)用于呈現(xiàn)授權標頭的 BASIC 憑據(jù)。??-u greg:turnquist??

在所有標頭中,可以看到受版本控制的資源中的標頭。??ETag??

最后,在數(shù)據(jù)本身內(nèi)部,您可以看到一個新屬性:。您可以看到它包括名稱和角色,但不包括密碼。這是由于在該字段上使用。因為 Spring Data REST 沒有導出該存儲庫,所以它的值內(nèi)聯(lián)在此資源中。在下一節(jié)中更新 UI 時,您將充分利用這一點。??manager????@JsonIgnore??

在 UI 中顯示經(jīng)理信息

通過后端的所有這些修改,您現(xiàn)在可以轉向更新前端的內(nèi)容。首先,您可以在 React 組件中顯示員工的經(jīng)理:????

class Employee extends React.Component {  constructor(props) {    super(props);    this.handleDelete = this.handleDelete.bind(this);  }  handleDelete() {    this.props.onDelete(this.props.employee);  }  render() {    return (              {this.props.employee.entity.firstName}        {this.props.employee.entity.lastName}        {this.props.employee.entity.description}        {this.props.employee.entity.manager.name}                                                              )  }}

這僅為 添加一列。??this.props.employee.entity.manager.name??

篩選出 JSON 架構元數(shù)據(jù)

如果數(shù)據(jù)輸出中顯示某個字段,則可以安全地假設該字段在 JSON 架構元數(shù)據(jù)中具有條目。您可以在以下摘錄中看到它:

{  ...    "manager" : {      "readOnly" : false,      "$ref" : "#/descriptors/manager"    },    ...  },  ...  "$schema" : "https://json-schema.org/draft-04/schema#"}

該字段不是您希望人們直接編輯的內(nèi)容。由于它是內(nèi)聯(lián)的,因此應將其視為只讀屬性。要從 和 中篩選出內(nèi)聯(lián)條目,您可以在獲取 中的 JSON 架構元數(shù)據(jù)后刪除此類條目:??manager????CreateDialog????UpdateDialog????loadFromServer()??

/** * Filter unneeded JSON Schema properties, like uri references and * subtypes ($ref). */Object.keys(schema.entity.properties).forEach(function (property) {  if (schema.entity.properties[property].hasOwnProperty("format") &&    schema.entity.properties[property].format === "uri") {    delete schema.entity.properties[property];  }  else if (schema.entity.properties[property].hasOwnProperty("$ref")) {    delete schema.entity.properties[property];  }});this.schema = schema.entity;this.links = employeeCollection.entity._links;return employeeCollection;

此代碼修剪了 URI 關系以及$ref條目。

陷阱用于未經(jīng)授權的訪問

通過在后端配置安全檢查,您可以添加處理程序,以防有人嘗試未經(jīng)授權更新記錄:

onUpdate(employee, updatedEmployee) {  if(employee.entity.manager.name === this.state.loggedInManager) {    updatedEmployee["manager"] = employee.entity.manager;    client({      method: "PUT",      path: employee.entity._links.self.href,      entity: updatedEmployee,      headers: {        "Content-Type": "application/json",        "If-Match": employee.headers.Etag      }    }).done(response => {      /* Let the websocket handler update the state */    }, response => {      if (response.status.code === 403) {        alert("ACCESS DENIED: You are not authorized to update " +          employee.entity._links.self.href);      }      if (response.status.code === 412) {        alert("DENIED: Unable to update " + employee.entity._links.self.href +          ". Your copy is stale.");      }    });  } else {    alert("You are not authorized to update");  }}

您有代碼可以捕獲 HTTP 412 錯誤。這會捕獲 HTTP 403 狀態(tài)代碼并提供合適的警報。

您可以對刪除操作執(zhí)行相同的操作:

onDelete(employee) {  client({method: "DELETE", path: employee.entity._links.self.href}  ).done(response => {/* let the websocket handle updating the UI */},  response => {    if (response.status.code === 403) {      alert("ACCESS DENIED: You are not authorized to delete " +        employee.entity._links.self.href);    }  });}

這與定制錯誤消息的編碼類似。

向 UI 添加一些安全詳細信息

為此版本的應用程序加冕的最后一件事是顯示誰已登錄,并通過在文件前面包含此新功能來提供注銷按鈕:??

????index.html????react????
??

Hello, user.

將一切整合在一起

若要在前端查看這些更改,請重新啟動應用程序并導航到??http://localhost:8080??.

您將立即被重定向到登錄表單。此表格由Spring Security提供,但您可以創(chuàng)建您自己的如果你愿意。以 / 身份登錄,如下圖所示:??greg????turnquist??

您可以看到新添加的經(jīng)理列。瀏覽幾頁,直到找到Oliver擁有的員工,如下圖所示:

單擊“?更新”,進行一些更改,然后再次單擊“更新”。它應該失敗并顯示以下彈出窗口:

如果嘗試刪除,它應該會失敗并顯示類似的消息。如果您創(chuàng)建新員工,則應將其分配給您。

回顧

在本節(jié)中,您將:

定義模型并通過一對多關系將其鏈接到員工。manager為經(jīng)理創(chuàng)建了一個存儲庫,并告訴Spring Data REST不要導出。為員工存儲庫編寫了一組訪問規(guī)則,并編寫了安全策略。編寫了另一個 Spring Data REST 事件處理程序,以便在創(chuàng)建事件發(fā)生之前捕獲它們,以便可以將當前用戶指定為員工的經(jīng)理。更新了 UI 以顯示員工的經(jīng)理,并在執(zhí)行未經(jīng)授權的操作時顯示錯誤彈出窗口。

問題?

網(wǎng)頁已經(jīng)變得相當復雜。但是,管理關系和內(nèi)聯(lián)數(shù)據(jù)呢?創(chuàng)建和更新對話框并不適合此。它可能需要一些自定義的書面形式。

經(jīng)理有權訪問員工數(shù)據(jù)。員工是否應該有權訪問?如果要添加電話號碼和地址等更多詳細信息,您將如何建模?您將如何授予員工訪問系統(tǒng)的權限,以便他們可以更新這些特定字段?是否有更多可以方便地放在頁面上的超媒體控件?

標簽: 應用程序 安全策略 身份驗證

上一篇:天天最資訊丨Spring Boot 啟動和 OAuth2
下一篇:世界看點:React.js 和 Spring Data REST(二)