在上一节,您引入了条件更新以避免在编辑相同数据时与其他用户发生冲突。您还学习了如何使用乐观锁定对后端的数据进行版本控制。如果有人编辑了同一记录,您会收到通知,以便您可以刷新页面并获取更新。
(资料图)
很好。但是你知道什么更好吗?让 UI 在其他人更新资源时动态响应。
在本节中,您将学习如何使用Spring Data REST的内置事件系统来检测后端中的更改,并通过Spring的WebSocket支持向所有用户发布更新。然后,您将能够在数据更新时动态调整客户端。
随意获取代码从此存储库并继续操作。本节基于上一节的应用程序,并添加了额外的内容。
在开始之前,您需要将依赖项添加到项目的pom.xml文件中:
org.springframework.boot spring-boot-starter-websocket
这种依赖关系引入了Spring Boot的WebSocket启动器。
Spring带有强大的WebSocket支持.要认识到的一件事是,WebSocket是一个非常低级的协议。它只不过提供了在客户端和服务器之间传输数据的方法。建议使用子协议(本节为 STOMP)对数据和路由进行实际编码。
以下代码在服务器端配置 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 | |
使用此配置,您现在可以利用 Spring Data REST 事件并通过 WebSocket 发布它们。
Spring Data REST 生成几个应用程序事件基于存储库上发生的操作。以下代码演示如何订阅其中一些事件:
@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 | 批注标记需要侦听事件的方法。这些方法必须是公共的。 |
这些处理程序方法中的每一个都调用以通过 WebSocket 传输消息。这是一种发布-订阅方法,以便将一条消息中继到每个连接的使用者。SimpMessagingTemplate.convertAndSend()
每条消息的路由是不同的,允许将多条消息发送到客户端上的不同接收方,同时只需要一个开放的 WebSocket — 这是一种资源节约型方法。
getPath()
使用 Spring Data REST 查找给定类类型和 id 的路径。为了满足客户端的需求,此对象被转换为 Java URI,并提取其路径。EntityLinks
Link
|
实质上,您正在侦听创建、更新和删除事件,并在它们完成后向所有客户端发送有关它们的通知。您还可以在此类操作发生之前拦截它们,并可能记录它们,出于某种原因阻止它们,或者用额外的信息装饰域对象。(在下一节中,我们将看到一个方便的用法。
下一步是编写一些客户端代码来使用 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 子协议。 |
3 | 将 WebSocket 指向应用程序的终结点。 |
4 | 遍历提供的数组,以便每个数组都可以在消息到达时订阅回调。 |
每个注册条目都有一个和一个 .在下一节中,您可以了解如何注册事件处理程序。route
callback
在 React 中,组件的函数在 DOM 中渲染后被调用。这也是注册 WebSocket 事件的合适时机,因为该组件现已联机并准备好开展业务。以下代码执行此操作: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 对象数组,每个对象都带有 a 和 .route
callback
创建新员工时,行为是刷新数据集,然后使用分页链接导航到最后一页。为什么要在导航到末尾之前刷新数据?添加新记录可能会导致创建新页面。虽然可以计算这是否会发生,但它颠覆了超媒体的观点。与其将自定义的页数拼凑在一起,不如使用现有链接,并且只有在有性能驱动的原因时才走这条路。
更新或删除员工时,行为是刷新当前页面。更新记录时,它会影响您正在查看的页面。当您删除当前页面上的记录时,下一页中的记录将被拉入当前页面 - 因此还需要刷新当前页面。
这些 WebSocket 消息不需要以 开头。这是指示发布-订阅语义的常见约定。 |
在下一节中,您可以看到执行这些操作的实际操作。
以下代码块包含用于在收到 WebSocket 事件时更新 UI 状态的两个回调:
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()
使用熟悉的函数导航到应用了参数的链接,插入 .收到响应后,然后调用最后一部分中的相同函数,并跳转到最后一页,即将找到新记录的页面。follow()
employees
size
this.state.pageSize
onNavigate()
refreshCurrentPage()
也使用该函数,但适用于 和 。这将获取您当前正在查看的同一页面并相应地更新状态。follow()
this.state.pageSize
size
this.state.page.number
page
此行为告知每个客户端在发送更新或删除消息时刷新其当前页面。他们的当前页面可能与当前事件无关。但是,要弄清楚这一点可能很棘手。如果删除的记录在第二页上,而您正在查看第三页,该怎么办?每个条目都会改变。但这是想要的行为吗?或。也许不是。 |
在完成本节之前,有一些东西需要识别。您刚刚为 UI 中的状态添加了更新的新方法:当 WebSocket 消息到达时。但是更新状态的旧方法仍然存在。
若要简化代码的状态管理,请删除旧方法。换句话说,提交您的 、 和调用,但不要使用其结果来更新 UI 的状态。相反,请等待 WebSocket 事件回旋,然后执行更新。POST
PUT
DELETE
以下代码块显示了与上一节相同的函数,只是进行了简化: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"} }) })}
在这里,函数用于获取链接,然后应用操作。注意怎么有 没有 或 ,像以前一样?用于侦听更新的事件处理程序现在位于 中,您刚刚查看了该事件处理程序。follow()
employees
POST
client({method: "GET" …})
then()
done()
refreshAndGoToLastPage()
完成所有这些修改后,启动应用程序 () 并使用它。打开两个浏览器选项卡并调整大小,以便您可以同时看到它们。开始在一个选项卡中进行更新,看看它们如何立即更新另一个选项卡。打开手机并访问同一页面。找一个朋友,让那个人做同样的事情。您可能会发现这种类型的动态更新更敏锐。./mvnw spring-boot:run
想要挑战吗?尝试上一节中的练习,在两个不同的浏览器选项卡中打开同一记录。尝试在一个中更新它,而在另一个中看不到它更新。如果可能,条件代码仍应保护您。但要做到这一点可能会更棘手!PUT
在本节中,您将:
配置了Spring的WebSocket支持和SockJS回退。订阅了从 Spring Data REST 创建、更新和删除事件以动态更新 UI。发布了受影响的 REST 资源的 URI 以及上下文消息(“/topic/newEmployee”、“/topic/updateEmployee”等)。在 UI 中注册 WebSocket 侦听器以侦听这些事件。将侦听器连接到处理程序以更新 UI 状态。有了所有这些功能,可以轻松地并排运行两个浏览器,并查看如何将一个浏览器更新到另一个浏览器。
问题?
虽然多个显示器可以很好地更新,但需要完善精确的行为。例如,创建一个新用户将导致所有用户跳到最后。关于如何处理这个问题的任何想法?
分页很有用,但它提供了一个棘手的管理状态。此示例应用程序的成本很低,而且 React 在更新 DOM 方面非常有效,而不会在 UI 中引起大量闪烁。但是对于更复杂的应用程序,并非所有这些方法都适合。
在设计时考虑分页时,您必须确定客户端之间的预期行为是什么,以及是否需要更新。根据您的要求和系统性能,现有的导航超媒体可能就足够了。
在上一节,您使用Spring Data REST的内置事件处理程序和Spring Framework的WebSocket支持使应用程序动态响应其他用户的更新。但是,如果不保护整个事情,则任何应用程序都是不完整的,因此只有适当的用户才能访问UI及其背后的资源。
随意获取代码从此存储库并继续操作。本部分基于上一节的应用,添加了额外的内容。
在开始之前,您需要将几个依赖项添加到项目的pom.xml文件中:
org.springframework.boot spring-boot-starter-security org.thymeleaf.extras thymeleaf-extras-springsecurity5
这带来了Spring Boot的Spring Security启动器以及一些额外的Thymeleaf标签,以便在网页中进行安全查找。
在过去的部分中,您使用了一个不错的工资单系统。在后端声明内容并让Spring Data REST完成繁重的工作很方便。下一步是模拟需要建立安全控制的系统。
如果这是一个工资单系统,那么只有经理才能访问它。因此,通过对对象进行建模来开始工作: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 | 自定义方法可确保密码永远不会以明文形式存储。 |
在设计安全层时,要记住一件关键的事情。保护正确的数据位(如密码),不要让它们打印到控制台、日志中或通过 JSON 序列化导出。
@JsonIgnore
应用于密码字段可防止杰克逊序列化此字段。Spring Data非常擅长管理实体。为什么不创建一个存储库来处理这些管理器呢?以下代码执行此操作:
@RepositoryRestResource(exported = false)public interface ManagerRepository extends Repository{ Manager save(Manager manager); Manager findByName(String name);}
而不是扩展通常的,你不需要那么多的方法。相反,您需要保存数据(也用于更新),并且需要查找现有用户。因此,您可以使用Spring Data Common的最小标记接口。它没有预定义的操作。CrudRepository
Repository
默认情况下,Spring Data REST将导出它找到的任何存储库。您不希望此存储库公开用于 REST 操作!应用批注以阻止其导出。这可以防止提供存储库及其元数据。@RepositoryRestResource(exported = false)
建模安全性的最后一点是将员工与经理相关联。在此域中,一个员工可以有一个经理,而一个经理可以有多个员工。以下代码定义该关系:
@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 | 实用程序构造函数调用已更新以支持初始化。 |
在定义安全策略时,Spring 安全性支持多种选项。在本节中,您希望限制以下内容,以便只有经理可以查看员工工资单数据,并且保存、更新和删除操作仅限于员工的经理。换句话说,任何经理都可以登录并查看数据,但只有给定员工的经理才能进行任何更改。以下代码可实现这些目标:
@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 | |
在 上,员工的经理为 null(在未分配经理时首次创建新员工),或者员工的经理姓名与当前经过身份验证的用户名匹配。在这里,您正在使用save()
Spring Security 的 SpEL 表达式以定义访问权限。它带有一个方便的属性导航器来处理空检查。同样重要的是要注意使用 on 参数将 HTTP 操作与方法链接起来。?.
@Param(…)
在 上,该方法可以访问员工,或者如果它只有一个 ,则必须在应用程序上下文中找到 ,执行 ,并根据当前经过身份验证的用户检查经理。delete()
id
employeeRepository
findOne(id)
UserDetails
与安全性集成的一个常见点是定义 .这是将用户的数据存储连接到 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
实现 Spring 安全的 .该接口有一种方法:。此方法旨在返回一个对象,以便 Spring 安全性可以查询用户的信息。UserDetailsService
loadUserByUsername()
UserDetails
因为您有一个 ,所以不需要编写任何 SQL 或 JPA 表达式来获取这些所需的数据。在此类中,它通过构造函数注入自动连接。ManagerRepository
loadUserByUsername()
点击您刚才编写的自定义查找器。然后,它填充一个 Spring 安全实例,该实例实现接口。您还使用 Spring Securiy 从基于字符串的角色数组过渡到 Java 类型的 Java。findByName()
User
UserDetails
AuthorityUtils
List
GrantedAuthority
应用于存储库的表达式是访问规则。如果没有安全策略,这些规则是徒劳的:@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 | 它通过场注入自动连接,然后通过该方法将其插入。发件人也已设置。 |
5 | 关键安全策略是用纯 Java 编写的,带有方法调用。 |
安全策略规定使用前面定义的访问规则授权所有请求:
中列出的路径被授予无条件访问权限,因为没有理由阻止静态 Web 资源。antMatchers()
任何与该策略不匹配的内容都属于 ,这意味着它需要身份验证。anyRequest().authenticated()
设置这些访问规则后,Spring 安全性将被告知使用基于表单的身份验证(默认为成功时)并授予对登录页面的访问权限。/
基本登录也配置为禁用 CSRF。这主要用于演示,不建议用于未经仔细分析的生产系统。注销配置为将用户带到 。/
当您尝试使用 curl 时,BASIC 身份验证非常方便。使用 curl 访问基于表单的系统是令人生畏的。重要的是要认识到,通过 HTTP(而不是 HTTPS)使用任何机制进行身份验证会使您面临通过网络嗅探凭据的风险。CSRF 是一个很好的协议,可以保持完整。禁用它以使与 BASIC 和 curl 的交互更容易。在生产中,最好保持打开状态。 |
良好用户体验的一部分是应用程序可以自动应用上下文。在此示例中,如果登录的经理创建新的员工记录,则该经理拥有该记录是有意义的。使用Spring Data REST的事件处理程序,用户不需要显式链接它。它还可确保用户不会意外地将记录分配给错误的经理。为我们处理: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 | |
在这种情况下,可以查找当前用户的安全上下文以获取用户的名称。然后,您可以使用关联管理器查找该管理器并将其应用于该管理器。如果系统中尚不存在该人员,则有一些额外的粘附代码可以创建新经理。但是,这主要是为了支持数据库的初始化。在实际的生产系统中,应删除该代码,而是依靠 DBA 或安全运营团队来正确维护用户数据存储。findByName()
加载经理并将员工链接到这些经理非常简单:
@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 处于活动状态,访问规则完全有效。因此,要保存员工数据,您必须使用 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" } }}
这显示了比您在第一部分中看到的更多细节。首先,Spring Security 会打开多个 HTTP 协议来抵御各种媒介(Pragma、Expires、X-Frame-Options 等)。您还将颁发用于呈现授权标头的 BASIC 凭据。-u greg:turnquist
在所有标头中,可以看到受版本控制的资源中的标头。ETag
最后,在数据本身内部,您可以看到一个新属性:。您可以看到它包括名称和角色,但不包括密码。这是由于在该字段上使用。因为 Spring Data REST 没有导出该存储库,所以它的值内联在此资源中。在下一节中更新 UI 时,您将充分利用这一点。manager
@JsonIgnore
通过后端的所有这些修改,您现在可以转向更新前端的内容。首先,您可以在 React 组件中显示员工的经理:
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 架构元数据中具有条目。您可以在以下摘录中看到它:
{ ... "manager" : { "readOnly" : false, "$ref" : "#/descriptors/manager" }, ... }, ... "$schema" : "https://json-schema.org/draft-04/schema#"}
该字段不是您希望人们直接编辑的内容。由于它是内联的,因此应将其视为只读属性。要从 和 中筛选出内联条目,您可以在获取 中的 JSON 架构元数据后删除此类条目: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条目。
通过在后端配置安全检查,您可以添加处理程序,以防有人尝试未经授权更新记录:
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 状态代码并提供合适的警报。
您可以对删除操作执行相同的操作:
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提供,但您可以创建您自己的如果你愿意。以 / 身份登录,如下图所示: 您可以看到新添加的经理列。浏览几页,直到找到Oliver拥有的员工,如下图所示: 单击“更新”,进行一些更改,然后再次单击“更新”。它应该失败并显示以下弹出窗口: 如果尝试删除,它应该会失败并显示类似的消息。如果您创建新员工,则应将其分配给您。 在本节中,您将: 问题? 网页已经变得相当复杂。但是,管理关系和内联数据呢?创建和更新对话框并不适合此。它可能需要一些自定义的书面形式。 经理有权访问员工数据。员工是否应该有权访问?如果要添加电话号码和地址等更多详细信息,您将如何建模?您将如何授予员工访问系统的权限,以便他们可以更新这些特定字段?是否有更多可以方便地放在页面上的超媒体控件?
index.html
react
将一切整合在一起
greg
turnquist
回顾
manager
为经理创建了一个存储库,并告诉Spring Data REST不要导出。为员工存储库编写了一组访问规则,并编写了安全策略。编写了另一个 Spring Data REST 事件处理程序,以便在创建事件发生之前捕获它们,以便可以将当前用户指定为员工的经理。更新了 UI 以显示员工的经理,并在执行未经授权的操作时显示错误弹出窗口。