“我喜欢编写身份验证和授权代码。” 〜从来没有Java开发人员。 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证。

React的设计使创建交互式UI变得轻松自如。 它的状态管理非常有效,并且仅在数据更改时才更新组件。 组件逻辑是用JavaScript编写的,这意味着您可以将状态保持在DOM之外,并创建封装的组件。

开发人员喜欢CRUD(创建,读取,更新和删除)应用程序,因为它们显示了创建应用程序时需要的许多基本功能。 一旦在应用程序中完成了CRUD的基础知识,大多数客户端-服务器管道就完成了,您可以继续实施必要的业务逻辑。

今天,我将向您展示如何在React中使用Spring Boot创建一个基本的CRUD应用。 您可能还记得我去年为Angular撰写的一篇类似文章: 使用Angular 5.0和Spring Boot 2.0构建Ba​​sic CRUD应用程序 。 该教程使用OAuth 2.0的隐式流程和我们的Okta Angular SDK 。 在本教程中,我将使用OAuth 2.0授权代码流,并将React应用打包在Spring Boot应用中进行生产。 同时,我将向您展示如何保持React高效的工作流以进行本地开发。

您将需要安装Java 8 , Node.js 8和Yarn才能完成本教程。 您可以使用npm代替Yarn,但是您需要将Yarn语法转换为npm。

使用Spring Boot 2.0创建API应用

我经常在世界各地的会议和用户组中演讲。 我最喜欢发言的用户组是Java用户组(JUG)。 我从事Java开发人员已有近20年的时间,而且我喜欢Java社区。 我的一个好朋友詹姆斯·沃德(James Ward)表示,进行水罐巡游是他当时最喜欢的开发商倡导者活动之一。 我最近接受了他的建议,并在海外会议上进行了JUG聚会在美国的聚会。

我为什么要告诉你呢? 因为我认为今天创建一个“ JUG Tours”应用很有趣,它允许您创建/编辑/删除JUG,以及查看即将发生的事件。

首先,导航至start.spring.io并进行以下选择:

  • 组: com.okta.developer
  • 神器: jugtours
  • 依赖项JPAH2WebLombok

单击生成项目 ,下载后展开jugtours.zip ,然后在您喜欢的IDE中打开该项目。

提示:如果您使用的是IntelliJ IDEA或Spring Tool Suite,则在创建新项目时也可以使用Spring Initializr。

添加一个JPA域模型

您需要做的第一件事是创建一个保存数据的域模型。 在高层次上,有一个Group表示酒壶,一个Event有一个多到一的关系Group ,以及User具有与一个一对多的关系Group

创建一个src/main/java/com/okta/developer/jugtours/model目录和其中的Group.java类。

package com.okta.developer.jugtours.model;import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;import javax.persistence.*;
import java.util.Set;@Data
@NoArgsConstructor
@RequiredArgsConstructor
@Entity
@Table(name = "user_group")
public class Group {@Id@GeneratedValueprivate Long id;@NonNullprivate String name;private String address;private String city;private String stateOrProvince;private String country;private String postalCode;@ManyToOne(cascade=CascadeType.PERSIST)private User user;@OneToMany(fetch = FetchType.EAGER, cascade=CascadeType.ALL)private Set<Event> events;
}

在同一包中创建一个Event.java类。

package com.okta.developer.jugtours.model;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import java.time.Instant;
import java.util.Set;@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class Event {@Id@GeneratedValueprivate Long id;private Instant date;private String title;private String description;@ManyToManyprivate Set<User> attendees;
}

还有一个User.java类。

package com.okta.developer.jugtours.model;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import javax.persistence.Entity;
import javax.persistence.Id;@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class User {@Idprivate String id;private String name;private String email;
}

创建一个GroupRepository.java来管理组实体。

package com.okta.developer.jugtours.model;import org.springframework.data.jpa.repository.JpaRepository;import java.util.List;public interface GroupRepository extends JpaRepository<Group, Long> {Group findByName(String name);
}

要加载一些默认数据,请在com.okta.developer.jugtours包中创建一个Initializer.java类。

package com.okta.developer.jugtours;import com.okta.developer.jugtours.model.Event;
import com.okta.developer.jugtours.model.Group;
import com.okta.developer.jugtours.model.GroupRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;import java.time.Instant;
import java.util.Collections;
import java.util.stream.Stream;@Component
class Initializer implements CommandLineRunner {private final GroupRepository repository;public Initializer(GroupRepository repository) {this.repository = repository;}@Overridepublic void run(String... strings) {Stream.of("Denver JUG", "Utah JUG", "Seattle JUG","Richmond JUG").forEach(name ->repository.save(new Group(name)));Group djug = repository.findByName("Denver JUG");Event e = Event.builder().title("Full Stack Reactive").description("Reactive with Spring Boot + React").date(Instant.parse("2018-12-12T18:00:00.000Z")).build();djug.setEvents(Collections.singleton(e));repository.save(djug);repository.findAll().forEach(System.out::println);}
}

提示:如果您的IDE Event.builder()问题,则意味着您需要打开注释处理和/或安装Lombok插件。 我必须在IntelliJ IDEA中卸载/重新安装Lombok插件才能正常工作。

如果在添加此代码后启动应用程序(使用./mvnw spring-boot:run ),您将看到控制台中显示的组和事件列表。

Group(id=1, name=Denver JUG, address=null, city=null, stateOrProvince=null, country=null, postalCode=null, user=null, events=[Event(id=5, date=2018-12-12T18:00:00Z, title=Full Stack Reactive, description=Reactive with Spring Boot + React, attendees=[])])
Group(id=2, name=Utah JUG, address=null, city=null, stateOrProvince=null, country=null, postalCode=null, user=null, events=[])
Group(id=3, name=Seattle JUG, address=null, city=null, stateOrProvince=null, country=null, postalCode=null, user=null, events=[])
Group(id=4, name=Richmond JUG, address=null, city=null, stateOrProvince=null, country=null, postalCode=null, user=null, events=[])

添加一个GroupController.java类(在src/main/java/.../jugtours/web/GroupController.java ), src/main/java/.../jugtours/web/GroupController.java可用于CRUD组。

package com.okta.developer.jugtours.web;import com.okta.developer.jugtours.model.Group;
import com.okta.developer.jugtours.model.GroupRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;import javax.validation.Valid;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Optional;@RestController
@RequestMapping("/api")
class GroupController {private final Logger log = LoggerFactory.getLogger(GroupController.class);private GroupRepository groupRepository;public GroupController(GroupRepository groupRepository) {this.groupRepository = groupRepository;}@GetMapping("/groups")Collection<Group> groups() {return groupRepository.findAll();}@GetMapping("/group/{id}")ResponseEntity<?> getGroup(@PathVariable Long id) {Optional<Group> group = groupRepository.findById(id);return group.map(response -> ResponseEntity.ok().body(response)).orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));}@PostMapping("/group")ResponseEntity<Group> createGroup(@Valid @RequestBody Group group) throws URISyntaxException {log.info("Request to create group: {}", group);Group result = groupRepository.save(group);return ResponseEntity.created(new URI("/api/group/" + result.getId())).body(result);}@PutMapping("/group/{id}")ResponseEntity<Group> updateGroup(@PathVariable Long id, @Valid @RequestBody Group group) {group.setId(id);log.info("Request to update group: {}", group);Group result = groupRepository.save(group);return ResponseEntity.ok().body(result);}@DeleteMapping("/group/{id}")public ResponseEntity<?> deleteGroup(@PathVariable Long id) {log.info("Request to delete group: {}", id);groupRepository.deleteById(id);return ResponseEntity.ok().build();}
}

如果重新启动服务器应用程序,并使用浏览器或命令行客户端访问http://localhost:8080/api/groups ,则应看到组列表。

您可以使用以下HTTPie命令创建,读取,更新和删除组。

http POST :8080/api/group name='Dublin JUG' city=Dublin country=Ireland
http :8080/api/group/6
http PUT :8080/api/group/6 name='Dublin JUG' city=Dublin country=Ireland address=Downtown
http DELETE :8080/api/group/6

使用Create React App创建一个React UI

Create React App是一个命令行实用程序,可为您生成React项目。 这是一个方便的工具,因为它还提供了一些命令,这些命令将生成和优化您的项目以进行生产。 它使用webpack在后台进行构建。 如果您想了解更多关于webpack的信息,我建议使用webpack.academy 。

使用Yarn在jugtours目录中创建一个新项目。

yarn create react-app app

应用程序创建过程完成后,导航至app目录并安装Bootstrap ,对React的cookie支持,React Router和Reactstrap 。

cd app
yarn add bootstrap@4.1.2 react-cookie@2.2.0 react-router-dom@4.3.1 reactstrap@6.3.0

您将使用BootstrapCSS和Reactstrap的组件来使UI看起来更好,尤其是在手机上。 如果您想了解有关Reactstrap的更多信息,请参见https://reactstrap.github.io 。 它具有有关其各种组件以及如何使用它们的大量文档。

将BootstrapCSS文件添加为app/src/index.js的导入文件。

import 'bootstrap/dist/css/bootstrap.min.css';

调用您的Spring Boot API并显示结果

修改app/src/App.js以使用以下代码调用/api/groups并在UI中显示列表。

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';class App extends Component {state = {isLoading: true,groups: []};async componentDidMount() {const response = await fetch('/api/groups');const body = await response.json();this.setState({ groups: body, isLoading: false });}render() {const {groups, isLoading} = this.state;if (isLoading) {return <p>Loading...</p>;}return (<div className="App"><header className="App-header"><img src={logo} className="App-logo" alt="logo" /><h1 className="App-title">Welcome to React</h1></header><div className="App-intro"><h2>JUG List</h2>{groups.map(group =><div key={group.id}>{group.name}</div>)}</div></div>);}
}export default App;

要将代理从/api代理到http://localhost:8080/api ,请将代理设置添加到app/package.json

"scripts": {...},
"proxy": "http://localhost:8080"

要了解有关此功能的更多信息,请在app/README.md搜索“ proxy”。 Create React App随该文件附带了各种文档,这有多酷?

确保Spring Boot正在运行,然后在您的app目录中运行yarn start 。 您应该看到默认组的列表。

构建一个React GroupList组件

React完全是关于组件的,您不想在主App呈现所有内容,因此请创建app/src/GroupList.js并使用以下JavaScript进行填充。

import React, { Component } from 'react';
import { Button, ButtonGroup, Container, Table } from 'reactstrap';
import AppNavbar from './AppNavbar';
import { Link } from 'react-router-dom';class GroupList extends Component {constructor(props) {super(props);this.state = {groups: [], isLoading: true};this.remove = this.remove.bind(this);}componentDidMount() {this.setState({isLoading: true});fetch('api/groups').then(response => response.json()).then(data => this.setState({groups: data, isLoading: false}));}async remove(id) {await fetch(`/api/group/${id}`, {method: 'DELETE',headers: {'Accept': 'application/json','Content-Type': 'application/json'}}).then(() => {let updatedGroups = [...this.state.groups].filter(i => i.id !== id);this.setState({groups: updatedGroups});});}render() {const {groups, isLoading} = this.state;if (isLoading) {return <p>Loading...</p>;}const groupList = groups.map(group => {const address = `${group.address || ''} ${group.city || ''} ${group.stateOrProvince || ''}`;return <tr key={group.id}><td style={{whiteSpace: 'nowrap'}}>{group.name}</td><td>{address}</td><td>{group.events.map(event => {return <div key={event.id}>{new Intl.DateTimeFormat('en-US', {year: 'numeric',month: 'long',day: '2-digit'}).format(new Date(event.date))}: {event.title}</div>})}</td><td><ButtonGroup><Button size="sm" color="primary" tag={Link} to={"/groups/" + group.id}>Edit</Button><Button size="sm" color="danger" onClick={() => this.remove(group.id)}>Delete</Button></ButtonGroup></td></tr>});return (<div><AppNavbar/><Container fluid><div className="float-right"><Button color="success" tag={Link} to="/groups/new">Add Group</Button></div><h3>My JUG Tour</h3><Table className="mt-4"><thead><tr><th width="20%">Name</th><th width="20%">Location</th><th>Events</th><th width="10%">Actions</th></tr></thead><tbody>{groupList}</tbody></Table></Container></div>);}
}export default GroupList;

在同一目录中创建AppNavbar.js ,以在组件之间建立通用的UI功能。

import React, { Component } from 'react';
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
import { Link } from 'react-router-dom';export default class AppNavbar extends Component {constructor(props) {super(props);this.state = {isOpen: false};this.toggle = this.toggle.bind(this);}toggle() {this.setState({isOpen: !this.state.isOpen});}render() {return <Navbar color="dark" dark expand="md"><NavbarBrand tag={Link} to="/">Home</NavbarBrand><NavbarToggler onClick={this.toggle}/><Collapse isOpen={this.state.isOpen} navbar><Nav className="ml-auto" navbar><NavItem><NavLinkhref="https://twitter.com/oktadev">@oktadev</NavLink></NavItem><NavItem><NavLink href="https://github.com/oktadeveloper/okta-spring-boot-react-crud-example">GitHub</NavLink></NavItem></Nav></Collapse></Navbar>;}
}

创建app/src/Home.js作为应用程序的登录页面。

import React, { Component } from 'react';
import './App.css';
import AppNavbar from './AppNavbar';
import { Link } from 'react-router-dom';
import { Button, Container } from 'reactstrap';class Home extends Component {render() {return (<div><AppNavbar/><Container fluid><Button color="link"><Link to="/groups">Manage JUG Tour</Link></Button></Container></div>);}
}export default Home;

另外,更改app/src/App.js以使用React Router在组件之间导航。

import React, { Component } from 'react';
import './App.css';
import Home from './Home';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import GroupList from './GroupList';class App extends Component {render() {return (<Router><Switch><Route path='/' exact={true} component={Home}/><Route path='/groups' exact={true} component={GroupList}/></Switch></Router>)}
}export default App;

为了使您的UI更加宽敞,请在app/src/App.css容器类中添加一个上边距。

.container, .container-fluid {margin-top: 20px
}

当您进行更改时,您的React应用程序应该会自我更新,并且您应该在http://localhost:3000看到如下屏幕。 点击Manage JUG Tour ,您将看到默认组的列表。 可以在React应用程序中查看Spring Boot API的数据真是太好了,但是如果您不能编辑它就不好玩了!

添加一个React GroupEdit组件

创建app/src/GroupEdit.js并使用其componentDidMount()从URL中获取具有ID的组资源。

import React, { Component } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { Button, Container, Form, FormGroup, Input, Label } from 'reactstrap';
import AppNavbar from './AppNavbar';class GroupEdit extends Component {emptyItem = {name: '',address: '',city: '',stateOrProvince: '',country: '',postalCode: ''};constructor(props) {super(props);this.state = {item: this.emptyItem};this.handleChange = this.handleChange.bind(this);this.handleSubmit = this.handleSubmit.bind(this);}async componentDidMount() {if (this.props.match.params.id !== 'new') {const group = await (await fetch(`/api/group/${this.props.match.params.id}`)).json();this.setState({item: group});}}handleChange(event) {const target = event.target;const value = target.value;const name = target.name;let item = {...this.state.item};item[name] = value;this.setState({item});}async handleSubmit(event) {event.preventDefault();const {item} = this.state;await fetch('/api/group', {method: (item.id) ? 'PUT' : 'POST',headers: {'Accept': 'application/json','Content-Type': 'application/json'},body: JSON.stringify(item),});this.props.history.push('/groups');}render() {const {item} = this.state;const title = <h2>{item.id ? 'Edit Group' : 'Add Group'}</h2>;return <div><AppNavbar/><Container>{title}<Form onSubmit={this.handleSubmit}><FormGroup><Label for="name">Name</Label><Input type="text" name="name" id="name" value={item.name || ''}onChange={this.handleChange} autoComplete="name"/></FormGroup><FormGroup><Label for="address">Address</Label><Input type="text" name="address" id="address" value={item.address || ''}onChange={this.handleChange} autoComplete="address-level1"/></FormGroup><FormGroup><Label for="city">City</Label><Input type="text" name="city" id="city" value={item.city || ''}onChange={this.handleChange} autoComplete="address-level1"/></FormGroup><div className="row"><FormGroup className="col-md-4 mb-3"><Label for="stateOrProvince">State/Province</Label><Input type="text" name="stateOrProvince" id="stateOrProvince" value={item.stateOrProvince || ''}onChange={this.handleChange} autoComplete="address-level1"/></FormGroup><FormGroup className="col-md-5 mb-3"><Label for="country">Country</Label><Input type="text" name="country" id="country" value={item.country || ''}onChange={this.handleChange} autoComplete="address-level1"/></FormGroup><FormGroup className="col-md-3 mb-3"><Label for="country">Postal Code</Label><Input type="text" name="postalCode" id="postalCode" value={item.postalCode || ''}onChange={this.handleChange} autoComplete="address-level1"/></FormGroup></div><FormGroup><Button color="primary" type="submit">Save</Button>{' '}<Button color="secondary" tag={Link} to="/groups">Cancel</Button></FormGroup></Form></Container></div>}
}export default withRouter(GroupEdit);

底部需要使用withRouter()高阶组件来显示this.props.history因此您可以在添加或保存GroupList后导航回this.props.history

修改app/src/App.js以导入GroupEdit并指定其路径。

import GroupEdit from './GroupEdit';class App extends Component {render() {return (<Router><Switch>...<Route path='/groups/:id' component={GroupEdit}/></Switch></Router>)}
}

现在,您应该可以添加和编辑组了!

使用Okta添加身份验证

构建CRUD应用程序非常酷,但是构建安全的应用程序甚至更酷。 为此,您需要添加身份验证,以便用户必须先登录才能查看/修改组。 为简化起见,您可以使用Okta的OIDC API。 在Okta,我们的目标是使身份管理比您以往更加轻松,安全和可扩展。 Okta是一项云服务,允许开发人员创建,编辑和安全地存储用户帐户和用户帐户数据,并将它们与一个或多个应用程序连接。 我们的API使您能够:

  • 验证和授权用户
  • 存储有关您的用户的数据
  • 执行基于密码的社交登录
  • 通过多因素身份验证保护您的应用程序
  • 以及更多! 查看我们的产品文档

你卖了吗 注册一个永久免费的开发者帐户 ,完成后再回来,这样您就可以了解有关使用Spring Boot构建安全应用程序的更多信息!

Spring Security + OIDC

Spring Security在其5.0版本中增加了OIDC支持 。 从那时起,他们进行了许多改进并简化了所需的配置。 我认为探索最新和最有趣的东西很有趣,所以我首先使用Spring的快照存储库更新pom.xml ,将Spring Boot和Spring Security升级到夜间构建,并添加必要的Spring Security依赖项来进行OIDC身份验证。

<?xml version="1.0" encoding="UTF-8"?>
<project>...<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.0.BUILD-SNAPSHOT</version><relativePath/> <!-- lookup parent from repository --></parent><properties>...<spring-security.version>5.1.0.BUILD-SNAPSHOT</spring-security.version></properties><dependencies>...<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-config</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-client</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-jose</artifactId></dependency></dependencies><build...><pluginRepositories><pluginRepository><id>spring-snapshots</id><name>Spring Snapshots</name><url>https://repo.spring.io/snapshot</url><snapshots><enabled>true</enabled></snapshots></pluginRepository></pluginRepositories><repositories><repository><id>spring-snapshots</id><name>Spring Snapshot</name><url>http://repo.spring.io/snapshot</url></repository></repositories>
</project>

在Okta中创建OIDC应用

登录到您的1563开发者帐户(或者注册 ,如果你没有一个帐户)并导航到应用程序 > 添加应用程序 。 单击“ Web” ,然后单击“ 下一步” 。 给应用程序起一个您会记住的名称,并指定http://localhost:8080/login/oauth2/code/okta作为登录重定向URI。 点击完成 ,然后点击编辑以编辑常规设置。 添加http://localhost:3000http://localhost:8080作为注销重定向URI,然后点击保存

将默认授权服务器的URI,客户端ID和客户端密钥复制并粘贴到src/main/resources/application.yml 。 创建此文件,然后可以删除同一目录中的application.properties文件。

spring:security:oauth2:client:registration:okta:client-id: {clientId}client-secret: {clientSecret}scope: openid email profileprovider:okta:issuer-uri: https://{yourOktaDomain}/oauth2/default

为React和用户身份配置Spring Security

为了使Spring Security React友好,请在src/main/java/.../jugtours/config创建一个SecurityConfiguration.java文件。 创建config目录并将该类放入其中。

package com.okta.developer.jugtours.config;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {private final Logger log = LoggerFactory.getLogger(SecurityConfiguration.class);@Overrideprotected void configure(HttpSecurity http) throws Exception {RequestCache requestCache = refererRequestCache();SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler();handler.setRequestCache(requestCache);http.exceptionHandling().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/oauth2/authorization/okta")).and().oauth2Login().successHandler(handler).and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and().requestCache().requestCache(requestCache).and().authorizeRequests().antMatchers("/**/*.{js,html,css}").permitAll().antMatchers("/", "/api/user").permitAll().anyRequest().authenticated();}@Beanpublic RequestCache refererRequestCache() {return new RequestCache() {private String savedAttrName = getClass().getName().concat(".SAVED");@Overridepublic void saveRequest(HttpServletRequest request, HttpServletResponse response) {String referrer = request.getHeader("referer");if (referrer != null) {request.getSession().setAttribute(this.savedAttrName, referrerRequest(referrer));}}@Overridepublic SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response) {HttpSession session = request.getSession(false);if (session != null) {return (SavedRequest) session.getAttribute(this.savedAttrName);}return null;}@Overridepublic HttpServletRequest getMatchingRequest(HttpServletRequest request, HttpServletResponse response) {return request;}@Overridepublic void removeRequest(HttpServletRequest request, HttpServletResponse response) {HttpSession session = request.getSession(false);if (session != null) {log.debug("Removing SavedRequest from session if present");session.removeAttribute(this.savedAttrName);}}};}private SavedRequest referrerRequest(final String referrer) {return new SavedRequest() {@Overridepublic String getRedirectUrl() {return referrer;}@Overridepublic List<Cookie> getCookies() {return null;}@Overridepublic String getMethod() {return null;}@Overridepublic List<String> getHeaderValues(String name) {return null;}@Overridepublic Collection<String> getHeaderNames() {return null;}@Overridepublic List<Locale> getLocales() {return null;}@Overridepublic String[] getParameterValues(String name) {return new String[0];}@Overridepublic Map<String, String[]> getParameterMap() {return null;}};}
}

这堂课正在进行很多,所以让我解释一些事情。 在年初configure()方法,你建立一个新类型,缓存网址标头(拼错请求缓存的referer在现实生活中),所以Spring Security可以验证后回重定向到它。 当您在http://localhost:3000上开发React并希望在登录后重定向到那里时,基于引用者的请求缓存会派上用场。

@Override
protected void configure(HttpSecurity http) throws Exception {RequestCache requestCache = refererRequestCache();SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler();handler.setRequestCache(requestCache);http.exceptionHandling().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/oauth2/authorization/okta")).and().oauth2Login().successHandler(handler).and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and().requestCache().requestCache(requestCache).and().authorizeRequests().antMatchers("/**/*.{js,html,css}").permitAll().antMatchers("/", "/api/user").permitAll().anyRequest().authenticated();
}

authenticationEntryPoint()行使Spring Security自动重定向到Okta。 在Spring Security 5.1.0.RELEASE中,当您仅配置一个OIDC提供程序时,将不需要此行。 它会自动重定向。

使用CookieCsrfTokenRepository.withHttpOnlyFalse()配置CSRF(跨站点请求伪造)保护意味着XSRF-TOKEN cookie将不会被标记为仅HTTP,因此React可以读取它并在尝试操作数据时将其发送回去。

antMatchers行定义了匿名用户可以使用哪些URL。 您将很快进行配置,以便由Spring Boot应用程序服务您的React应用程序,因此允许使用Web文件和“ /”的原因。 您可能会注意到也有一个公开的/api/user路径。 创建src/main/java/.../jugtours/web/UserController.java并使用以下代码填充它。 React将使用此API来1)找出用户是否已通过身份验证,以及2)执行全局注销。

package com.okta.developer.jugtours.web;import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;@RestController
public class UserController {@Value("${spring.security.oauth2.client.provider.okta.issuer-uri}")String issuerUri;@GetMapping("/api/user")public ResponseEntity<?> getUser(@AuthenticationPrincipal OAuth2User user) {if (user == null) {return new ResponseEntity<>("", HttpStatus.OK);} else {return ResponseEntity.ok().body(user.getAttributes());}}@PostMapping("/api/logout")public ResponseEntity<?> logout(HttpServletRequest request,@AuthenticationPrincipal(expression = "idToken") OidcIdToken idToken) {// send logout URL to client so they can initiate logout - doesn't work from the server side// Make it easier: https://github.com/spring-projects/spring-security/issues/5540String logoutUrl = issuerUri + "/v1/logout";Map<String, String> logoutDetails = new HashMap<>();logoutDetails.put("logoutUrl", logoutUrl);logoutDetails.put("idToken", idToken.getTokenValue());request.getSession(false).invalidate();return ResponseEntity.ok().body(logoutDetails);}
}

您还需要创建组时,这样就可以通过你的壶之旅筛选添加用户信息。 在与GroupRepository.java相同的目录中添加UserRepository.java

package com.okta.developer.jugtours.model;import org.springframework.data.jpa.repository.JpaRepository;public interface UserRepository extends JpaRepository<User, String> {
}

将新的findAllByUserId(String id)方法添加到GroupRepository.java

List<Group> findAllByUserId(String id);

然后将UserRepository注入GroupController.java并在添加新组时使用它来创建(或获取现有用户)。 在那里,请修改groups()方法以按用户过滤。

package com.okta.developer.jugtours.web;import com.okta.developer.jugtours.model.Group;
import com.okta.developer.jugtours.model.GroupRepository;
import com.okta.developer.jugtours.model.User;
import com.okta.developer.jugtours.model.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.*;import javax.validation.Valid;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.Principal;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;@RestController
@RequestMapping("/api")
class GroupController {private final Logger log = LoggerFactory.getLogger(GroupController.class);private GroupRepository groupRepository;private UserRepository userRepository;public GroupController(GroupRepository groupRepository, UserRepository userRepository) {this.groupRepository = groupRepository;this.userRepository = userRepository;}@GetMapping("/groups")Collection<Group> groups(Principal principal) {return groupRepository.findAllByUserId(principal.getName());}@GetMapping("/group/{id}")ResponseEntity<?> getGroup(@PathVariable Long id) {Optional<Group> group = groupRepository.findById(id);return group.map(response -> ResponseEntity.ok().body(response)).orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));}@PostMapping("/group")ResponseEntity<Group> createGroup(@Valid @RequestBody Group group,@AuthenticationPrincipal OAuth2User principal) throws URISyntaxException {log.info("Request to create group: {}", group);Map<String, Object> details = principal.getAttributes();String userId = details.get("sub").toString();// check to see if user already existsOptional<User> user = userRepository.findById(userId);group.setUser(user.orElse(new User(userId,details.get("name").toString(), details.get("email").toString())));Group result = groupRepository.save(group);return ResponseEntity.created(new URI("/api/group/" + result.getId())).body(result);}@PutMapping("/group")ResponseEntity<Group> updateGroup(@Valid @RequestBody Group group) {log.info("Request to update group: {}", group);Group result = groupRepository.save(group);return ResponseEntity.ok().body(result);}@DeleteMapping("/group/{id}")public ResponseEntity<?> deleteGroup(@PathVariable Long id) {log.info("Request to delete group: {}", id);groupRepository.deleteById(id);return ResponseEntity.ok().build();}
}

为了放大更改,它们在groups()createGroup()方法中。 Spring JPA会为您创建findAllByUserId()方法/查询,并且userRepository.findById()使用Java 8的Optional ,这是一个很好的选择 。

@GetMapping("/groups")
Collection<Group> groups(Principal principal) {return groupRepository.findAllByUserId(principal.getName());
}@PostMapping("/group")
ResponseEntity<Group> createGroup(@Valid @RequestBody Group group,@AuthenticationPrincipal OAuth2User principal) throws URISyntaxException {log.info("Request to create group: {}", group);Map<String, Object> details = principal.getAttributes();String userId = details.get("sub").toString();// check to see if user already existsOptional<User> user = userRepository.findById(userId);group.setUser(user.orElse(new User(userId,details.get("name").toString(), details.get("email").toString())));Group result = groupRepository.save(group);return ResponseEntity.created(new URI("/api/group/" + result.getId())).body(result);
}

修改React Handle CSRF并识别身份

您需要对React组件进行一些更改,以使它们能够识别身份。 您要做的第一件事是修改App.js以将所有内容包装在CookieProvider 。 该组件允许您读取CSRF cookie并将其作为标题发送回。

import { CookiesProvider } from 'react-cookie';class App extends Component {render() {return (<CookiesProvider><Router...></CookiesProvider>)}
}

修改app/src/Home.js以调用/api/user来查看用户是否已登录。如果没有Login ,请显示“ Login按钮。

import React, { Component } from 'react';
import './App.css';
import AppNavbar from './AppNavbar';
import { Link } from 'react-router-dom';
import { Button, Container } from 'reactstrap';
import { withCookies } from 'react-cookie';class Home extends Component {state = {isLoading: true,isAuthenticated: false,user: undefined};constructor(props) {super(props);const {cookies} = props;this.state.csrfToken = cookies.get('XSRF-TOKEN');this.login = this.login.bind(this);this.logout = this.logout.bind(this);}async componentDidMount() {const response = await fetch('/api/user', {credentials: 'include'});const body = await response.text();if (body === '') {this.setState(({isAuthenticated: false}))} else {this.setState({isAuthenticated: true, user: JSON.parse(body)})}}login() {let port = (window.location.port ? ':' + window.location.port : '');if (port === ':3000') {port = ':8080';}window.location.href = '//' + window.location.hostname + port + '/private';}logout() {console.log('logging out...');fetch('/api/logout', {method: 'POST', credentials: 'include',headers: {'X-XSRF-TOKEN': this.state.csrfToken}}).then(res => res.json()).then(response => {window.location.href = response.logoutUrl + "?id_token_hint=" +response.idToken + "&post_logout_redirect_uri=" + window.location.origin;});}render() {const message = this.state.user ?<h2>Welcome, {this.state.user.name}!</h2> :<p>Please log in to manage your JUG Tour.</p>;const button = this.state.isAuthenticated ?<div><Button color="link"><Link to="/groups">Manage JUG Tour</Link></Button><br/><Button color="link" onClick={this.logout}>Logout</Button></div> :<Button color="primary" onClick={this.login}>Login</Button>;return (<div><AppNavbar/><Container fluid>{message}{button}</Container></div>);}
}export default withCookies(Home);

您应该在此组件中注意一些事项:

  1. withCookies()Home组件包装在底部,以使其可以访问cookie。 然后,您可以在构造const {cookies} = props中使用const {cookies} = props ,并使用cookies.get('XSRF-TOKEN')获取cookie。
  2. 使用fetch() ,需要包括{credentials: 'include'}来传输cookie。 如果不包含此选项,则将获得“ 403禁止访问”。
  3. Spring Security的CSRF cookie的名称与您需要发回的标头的名称不同。 cookie名称是XSRF-TOKEN ,而标题名称是X-XSRF-TOKEN

更新app/src/GroupList.js以进行类似更改。 好消息是您不需要对render()方法进行任何更改。

import { Link, withRouter } from 'react-router-dom';
import { instanceOf } from 'prop-types';
import { withCookies, Cookies } from 'react-cookie';class GroupList extends Component {static propTypes = {cookies: instanceOf(Cookies).isRequired};constructor(props) {super(props);const {cookies} = props;this.state = {groups: [], csrfToken: cookies.get('XSRF-TOKEN'), isLoading: true};this.remove = this.remove.bind(this);}componentDidMount() {this.setState({isLoading: true});fetch('api/groups', {credentials: 'include'}).then(response => response.json()).then(data => this.setState({groups: data, isLoading: false})).catch(() => this.props.history.push('/'))}async remove(id) {await fetch(`/api/group/${id}`, {method: 'DELETE',headers: {'X-XSRF-TOKEN': this.state.csrfToken,'Accept': 'application/json','Content-Type': 'application/json'},credentials: 'include'}).then(() => {let updatedGroups = [...this.state.groups].filter(i => i.id !== id);this.setState({groups: updatedGroups});});}render() {...}
}export default withCookies(withRouter(GroupList));

也更新GroupEdit.js

import { instanceOf } from 'prop-types';
import { Cookies, withCookies } from 'react-cookie';class GroupEdit extends Component {static propTypes = {cookies: instanceOf(Cookies).isRequired};emptyItem = {name: '',address: '',city: '',stateOrProvince: '',country: '',postalCode: ''};constructor(props) {super(props);const {cookies} = props;this.state = {item: this.emptyItem,csrfToken: cookies.get('XSRF-TOKEN')};this.handleChange = this.handleChange.bind(this);this.handleSubmit = this.handleSubmit.bind(this);}async componentDidMount() {if (this.props.match.params.id !== 'new') {try {const group = await (await fetch(`/api/group/${this.props.match.params.id}`, {credentials: 'include'})).json();this.setState({item: group});} catch (error) {this.props.history.push('/');}}}handleChange(event) {const target = event.target;const value = target.value;const name = target.name;let item = {...this.state.item};item[name] = value;this.setState({item});}async handleSubmit(event) {event.preventDefault();const {item, csrfToken} = this.state;await fetch('/api/group', {method: (item.id) ? 'PUT' : 'POST',headers: {'X-XSRF-TOKEN': csrfToken,'Accept': 'application/json','Content-Type': 'application/json'},body: JSON.stringify(item),credentials: 'include'});this.props.history.push('/groups');}render() {...}
}export default withCookies(withRouter(GroupEdit));

完成所有这些更改之后,您应该能够重新启动Spring Boot和React,并见证计划自己的JUG Tour的荣耀!

配置Maven以使用Spring Boot构建和打包React

要使用Maven构建和打包React应用,可以使用frontend-maven-plugin和Maven的配置文件将其激活。 将版本的属性和<profiles>部分添加到pom.xml

<properties>...<frontend-maven-plugin.version>1.6</frontend-maven-plugin.version><node.version>v10.6.0</node.version><yarn.version>v1.8.0</yarn.version>
</properties><profiles><profile><id>dev</id><activation><activeByDefault>true</activeByDefault></activation><properties><spring.profiles.active>dev</spring.profiles.active></properties></profile><profile><id>prod</id><build><plugins><plugin><artifactId>maven-resources-plugin</artifactId><executions><execution><id>copy-resources</id><phase>process-classes</phase><goals><goal>copy-resources</goal></goals><configuration><outputDirectory>${basedir}/target/classes/static</outputDirectory><resources><resource><directory>app/build</directory></resource></resources></configuration></execution></executions></plugin><plugin><groupId>com.github.eirslett</groupId><artifactId>frontend-maven-plugin</artifactId><version>${frontend-maven-plugin.version}</version><configuration><workingDirectory>app</workingDirectory></configuration><executions><execution><id>install node</id><goals><goal>install-node-and-yarn</goal></goals><configuration><nodeVersion>${node.version}</nodeVersion><yarnVersion>${yarn.version}</yarnVersion></configuration></execution><execution><id>yarn install</id><goals><goal>yarn</goal></goals><phase>generate-resources</phase></execution><execution><id>yarn test</id><goals><goal>yarn</goal></goals><phase>test</phase><configuration><arguments>test</arguments></configuration></execution><execution><id>yarn build</id><goals><goal>yarn</goal></goals><phase>compile</phase><configuration><arguments>build</arguments></configuration></execution></executions></plugin></plugins></build><properties><spring.profiles.active>prod</spring.profiles.active></properties></profile>
</profiles>

在使用时,将活动配置文件设置添加到src/main/resources/application.yml

spring:profiles:active: @spring.profiles.active@security:

添加./mvnw spring-boot:run -Pprod之后,您应该可以运行./mvnw spring-boot:run -Pprod并且您的应用程序可以看到您的应用程序在http://localhost:8080

注意:如果您无法登录,则可以尝试在隐身窗口中打开您的应用程序。

Spring Security的OAuth 2.0与OIDC支持

在撰写这篇文章时,我与Rob Winch (Spring Security Lead)合作,以确保我有效地使用了Spring Security。 我开始使用Spring Security的OAuth 2.0支持及其@EnableOAuth2Sso批注。 Rob鼓励我改用Spring Security的OIDC支持,这对使一切正常发挥了作用。

随着Spring Boot 2.1和Spring Security 5.1的里程碑和发行版的发布,我将更新此帖子以删除不再需要的代码。

了解有关Spring Boot和React的更多信息

我希望您喜欢本教程,了解如何使用React,Spring Boot和Spring Security进行CRUD。 您可以看到Spring Security的OIDC支持非常强大,并且不需要大量配置。 添加CSRF保护并将Spring Boot + React应用打包为单个工件也很酷!

您可以在GitHub上的https://github.com/oktadeveloper/okta-spring-boot-react-crud-example上找到本教程中创建的示例。

我们还编写了其他一些很棒的Spring Boot和React教程,如果您有兴趣的话可以查看它们。

  • 使用Spring Boot和React进行Bootiful开发
  • 构建一个React Native应用程序并使用OAuth 2.0进行身份验证
  • 使用Jenkins X和Kubernetes将CI / CD添加到您的Spring Boot应用程序
  • 15分钟内通过用户身份验证构建React应用程序

如有任何疑问,请随时在下面发表评论,或在我们的Okta开发者论坛上向我们提问。 如果您想查看更多类似的教程,请在Twitter上关注我们!

“我喜欢编写身份验证和授权代码。” 〜从来没有Java开发人员。 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证。

``使用React和Spring Boot构建简单的CRUD应用程序''最初于2018年7月19日发布在Okta开发者博客上。

翻译自: https://www.javacodegeeks.com/2018/07/react-spring-boot-build-crud-app.html

使用React和Spring Boot构建一个简单的CRUD应用相关推荐

  1. java jsf_使用Java和JSF构建一个简单的CRUD应用

    java jsf 使用Okta的身份管理平台轻松部署您的应用程序 使用Okta的API在几分钟之内即可对任何应用程序中的用户进行身份验证,管理和保护. 今天尝试Okta. JavaServer Fac ...

  2. 使用Java和JSF构建一个简单的CRUD应用

    使用Okta的身份管理平台轻松部署您的应用程序 使用Okta的API在几分钟之内即可对任何应用程序中的用户进行身份验证,管理和保护. 今天尝试Okta. JavaServer Faces(JSF)是用 ...

  3. 使用React Native和Spring Boot构建一个移动应用

    "我喜欢编写身份验证和授权代码." 〜从来没有Java开发人员. 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证. Reac ...

  4. 阿里微服务专家自己手写Spring Boot 实现一个简单的自动配置模块

    为了更好的理解 Spring Boot 的 自动配置和工作原理,我们自己来实现一个简单的自动配置模块. 假设,现在项目需要一个功能,需要自动记录项目发布者的相关信息,我们如何通过 Spring Boo ...

  5. 使用spring boot实现一个简单的项目——⽤户管理功能

    Spring Boot之用户管理功能 趁着这几天有时间跟大家分享一个使用spring boot实现的一个简单的项目,也开启了我第一次在CSDN上面写博客,相信这是个新的篇章.好了,废话不多说,直接上真 ...

  6. 课表排班java_初学OptaPlanner-02- 基于Spring Boot实现一个简单课程表排班的实例

    Spring Boot Java quick start 01. 排班目标 作出一个简单的课程表timetable,示例如下: 时间表的类图 02. Opta的常用注解说明, 关键实体类说明 @Pla ...

  7. 使用Angular,Ionic 4和Spring Boot构建移动应用

    朋友不允许朋友写用户身份验证. 厌倦了管理自己的用户? 立即尝试Okta的API和Java SDK. 在几分钟之内即可对任何应用程序中的用户进行身份验证,管理和保护. 我是Ionic的忠实粉丝. 几年 ...

  8. 使用Spring Boot构建RESTFul服务

    每个人都在谈论微服务,例如WSO2微服务框架 , Spring Boot等.由于我已经很长时间没有从事任何与Spring相关的项目了,所以我想到了使用Spring Boot实现一个简单的RESTFul ...

  9. Spring微服务实战第2章 使用Spring Boot构建微服务

    第2章 使用Spring Boot构建微服务 基于微服务的架构具有以下特点. 有约束的--微服务具有范围有限的单一职责集.微服务遵循UNIX的理念,即应用程序是服务的集合,每个服务只做一件事,并只做好 ...

最新文章

  1. PHP上传文件缺省目录,帝国cms默认图片、附件上传路径/d/file/怎么修改
  2. mac下导出kindle单词本的单词
  3. php lalaogu cn,php安装编译时错误合集
  4. windows SVN服务器软件
  5. 原理篇 | 推荐系统之矩阵分解模型
  6. 不同对象的通话是时长​
  7. mnist手写数字数据集_mnist手写数据集(1. 加载与可视化)
  8. RNN 循环神经网络系列 5: 自定义单元
  9. Python运行Google App Engineer时出现的UnicodeDecodeError错误解决方案
  10. GB/T2659-2000,ISO 3166-1:1997,ISO 3166-1:2006国家和地区代码列表(已整理)
  11. 用Python实现一个简单的批量无水印快手抖音批量下载器
  12. 机器学习处理信号分离_机器学习和深度学习现如今能应用在雷达信号处理,或者信号处理的哪些方面?...
  13. 【数据集收集】可用于深度学习模型的遥感数据集(持续更新,最后更新时间2020-06)
  14. linux新建目录自动777权限,linux 777权限目录可疑进程检测
  15. php蘑菇街商城源码,php源码:dedecms精仿蘑菇街(mogujie.com)源码,时尚购物社区源码...
  16. python 逐行调试工具_常用的 Python 调试工具,Python开发必读-乾颐堂
  17. 2021年计算机考研408操作系统真题(客观题)
  18. 一天设计100张海报?so easy
  19. Codeforces 1593C Save More Mice
  20. 51nod 1238 最小公倍数之和 V3

热门文章

  1. CF1120D Power Tree(树形DP/构造+差分+最小生成树)
  2. 动态规划训练19、最短路 [Help Jimmy POJ - 1661 ]
  3. 详解Vue中watch的高级用法
  4. JVM参数设置、分析
  5. MySQL week()函数
  6. Java类加载器总结
  7. ConcurrentHashMap的红黑树实现分析
  8. python正则获取网页标签里面的内容
  9. .sync的一个用法
  10. 【git】如何在github上推送并部署自己的项目