
在上一節(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)容。
在開始之前,您需要將依賴項添加到項目的pom.xml文件中:
org.springframework.boot spring-boot-starter-websocket
這種依賴關系引入了Spring Boot的WebSocket啟動器。
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 | ? |
?2 | ? |
?3 | MESSAGE_PREFIX是您將附加到每條消息路由前面的前綴。 |
?4 | ? |
?5 | ? |
使用此配置,您現(xiàn)在可以利用 Spring Data REST 事件并通過 WebSocket 發(fā)布它們。
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 | ? |
?2 | ? |
?3 | 批注標記需要偵聽事件的方法。這些方法必須是公共的。? |
這些處理程序方法中的每一個都調(diào)用以通過 WebSocket 傳輸消息。這是一種發(fā)布-訂閱方法,以便將一條消息中繼到每個連接的使用者。??SimpMessagingTemplate.convertAndSend()?
?
每條消息的路由是不同的,允許將多條消息發(fā)送到客戶端上的不同接收方,同時只需要一個開放的 WebSocket — 這是一種資源節(jié)約型方法。
??getPath()?
?使用 Spring Data REST 查找給定類類型和 id 的路徑。為了滿足客戶端的需求,此對象被轉換為 Java URI,并提取其路徑。??EntityLinks?
???Link?
?
? |
實質(zhì)上,您正在偵聽創(chuàng)建、更新和刪除事件,并在它們完成后向所有客戶端發(fā)送有關它們的通知。您還可以在此類操作發(fā)生之前攔截它們,并可能記錄它們,出于某種原因阻止它們,或者用額外的信息裝飾域對象。(在下一節(jié)中,我們將看到一個方便的用法。
下一步是編寫一些客戶端代碼來使用 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 指向應用程序的終結點。? |
?4 | 遍歷提供的數(shù)組,以便每個數(shù)組都可以在消息到達時訂閱回調(diào)。? |
每個注冊條目都有一個和一個 .在下一節(jié)中,您可以了解如何注冊事件處理程序。??route?
???callback?
?
在 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ā)布-訂閱語義的常見約定。? |
在下一節(jié)中,您可以看到執(zhí)行這些操作的實際操作。
以下代碼塊包含用于在收到 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ā)送更新或刪除消息時刷新其當前頁面。他們的當前頁面可能與當前事件無關。但是,要弄清楚這一點可能很棘手。如果刪除的記錄在第二頁上,而您正在查看第三頁,該怎么辦?每個條目都會改變。但這是想要的行為嗎?或。也許不是。 |
在完成本節(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)有的導航超媒體可能就足夠了。
在上一節(jié),您使用Spring Data REST的內(nèi)置事件處理程序和Spring Framework的WebSocket支持使應用程序動態(tài)響應其他用戶的更新。但是,如果不保護整個事情,則任何應用程序都是不完整的,因此只有適當?shù)挠脩舨拍茉L問UI及其背后的資源。
隨意獲取代碼從此存儲庫并繼續(xù)操作。本部分基于上一節(jié)的應用,添加了額外的內(nèi)容。
在開始之前,您需要將幾個依賴項添加到項目的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 | ? |
?2 | ? |
?3 | 自定義方法可確保密碼永遠不會以明文形式存儲。? |
在設計安全層時,要記住一件關鍵的事情。保護正確的數(shù)據(jù)位(如密碼),不要讓它們打印到控制臺、日志中或通過 JSON 序列化導出。
??@JsonIgnore?
?應用于密碼字段可防止杰克遜序列化此字段。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)理,而一個經(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 的屬性鏈接。 不需要 ,因為您尚未定義查找該需求。? |
?2 | 實用程序構造函數(shù)調(diào)用已更新以支持初始化。 |
在定義安全策略時,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 | ? |
在 上,員工的經(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 | ? |
?2 | ? |
?3 | 它擴展了,一個方便編寫策略的基類。? |
?4 | 它通過場注入自動連接,然后通過該方法將其插入。發(fā)件人也已設置。? |
?5 | 關鍵安全策略是用純 Java 編寫的,帶有方法調(diào)用。? |
安全策略規(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 | ? |
在這種情況下,可以查找當前用戶的安全上下文以獲取用戶的名稱。然后,您可以使用關聯(lián)管理器查找該管理器并將其應用于該管理器。如果系統(tǒng)中尚不存在該人員,則有一些額外的粘附代碼可以創(chuàng)建新經(jīng)理。但是,這主要是為了支持數(shù)據(jù)庫的初始化。在實際的生產(chǎn)系統(tǒng)中,應刪除該代碼,而是依靠 DBA 或安全運營團隊來正確維護用戶數(shù)據(jù)存儲。??findByName()?
?
加載經(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()?
?
完成所有這些修改后,您可以啟動應用程序 () 并使用以下 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?
?
通過后端的所有這些修改,您現(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?
?
如果數(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)授權更新記錄:
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); } });}
這與定制錯誤消息的編碼類似。
為此版本的應用程序加冕的最后一件事是顯示誰已登錄,并通過在文件前面包含此新功能來提供注銷按鈕:? 若要在前端查看這些更改,請重新啟動應用程序并導航到??http://localhost:8080??. 您將立即被重定向到登錄表單。此表格由Spring Security提供,但您可以創(chuàng)建您自己的如果你愿意。以 / 身份登錄,如下圖所示:? 您可以看到新添加的經(jīng)理列。瀏覽幾頁,直到找到Oliver擁有的員工,如下圖所示: 單擊“?更新”,進行一些更改,然后再次單擊“更新”。它應該失敗并顯示以下彈出窗口: 如果嘗試刪除,它應該會失敗并顯示類似的消息。如果您創(chuàng)建新員工,則應將其分配給您。 在本節(jié)中,您將: 問題? 網(wǎng)頁已經(jīng)變得相當復雜。但是,管理關系和內(nèi)聯(lián)數(shù)據(jù)呢?創(chuàng)建和更新對話框并不適合此。它可能需要一些自定義的書面形式。 經(jīng)理有權訪問員工數(shù)據(jù)。員工是否應該有權訪問?如果要添加電話號碼和地址等更多詳細信息,您將如何建模?您將如何授予員工訪問系統(tǒng)的權限,以便他們可以更新這些特定字段?是否有更多可以方便地放在頁面上的超媒體控件??
?index.html?
???react?
???
將一切整合在一起
?greg?
???turnquist?
?回顧
manager
為經(jīng)理創(chuàng)建了一個存儲庫,并告訴Spring Data REST不要導出。為員工存儲庫編寫了一組訪問規(guī)則,并編寫了安全策略。編寫了另一個 Spring Data REST 事件處理程序,以便在創(chuàng)建事件發(fā)生之前捕獲它們,以便可以將當前用戶指定為員工的經(jīng)理。更新了 UI 以顯示員工的經(jīng)理,并在執(zhí)行未經(jīng)授權的操作時顯示錯誤彈出窗口。