javascript
使用React和Spring Boot构建一个简单的CRUD应用
“我喜歡編寫身份驗證和授權代碼?!??從來沒有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
- 依賴項 : JPA , H2 , Web , Lombok
單擊生成項目 ,下載后展開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應用程序非???#xff0c;但是構建安全的應用程序甚至更酷。 為此,您需要添加身份驗證,以便用戶必須先登錄才能查看/修改組。 為簡化起見,您可以使用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:3000和http://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);您應該在此組件中注意一些事項:
更新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应用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 阿里巴巴集团计划在土耳其投资 20 亿美
- 下一篇: 三月是什么月 三月还有什么别称