1. 概述
React 是Facebook开源的一款基于组件的JavaScript界面框架。借助React,我们可以轻松构建复杂的Web应用程序。 在本文中,我们将使Spring Security与React实现登录页面。
我们将利用先前示例中已有的Spring Security配置。 因此,我们将在上一篇文章使用Spring Security实现表单登录的基础上进行构建。
2. 搭建React
首先,使用React命令行工具,create-react-app
来创建一个React项目:
create-react-app react
在react/package.json
配置文件中,我们会得到如下的配置:
{
"name": "react",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^16.4.1",
"react-dom": "^16.4.1",
"react-scripts": "1.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
然后,在pom.xml
中添加 frontend-maven-plugin插件,帮助我们直接通过Maven构建我们的React应用:
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.6</version>
<configuration>
<nodeVersion>v8.11.3</nodeVersion>
<npmVersion>6.1.0</npmVersion>
<workingDirectory>src/main/webapp/WEB-INF/view/react</workingDirectory>
</configuration>
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
</execution>
<execution>
<id>npm run build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build</arguments>
</configuration>
</execution>
</executions>
</plugin>
最新版本请访问 Mavn仓库.
当我们运行mvn compile
时,此插件将下载nodejs
和npm
,安装所需的nodejs
依赖,以及最后为我们编译React项目。
有几个配置我们需要在这里解释下。 我们指定了nodejs和npm的版本,以便插件知道要下载哪个版本。
我们的React登录页面将在Spring中用作静态页面,因此我们将src/main/webapp/WEB-INF/view/react
设置为npm的工作目录。
3. Spring Security 配置
在深入研究React之前,我们将更新Spring配置,以便处理React应用程序的静态资源:
@EnableWebMvc
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(
ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("/WEB-INF/view/react/build/static/");
registry.addResourceHandler("/*.js")
.addResourceLocations("/WEB-INF/view/react/build/");
registry.addResourceHandler("/*.json")
.addResourceLocations("/WEB-INF/view/react/build/");
registry.addResourceHandler("/*.ico")
.addResourceLocations("/WEB-INF/view/react/build/");
registry.addResourceHandler("/index.html")
.addResourceLocations("/WEB-INF/view/react/build/index.html");
}
}
请注意,我们添加的登录页面“ index.html”为静态html页面,而非JSP动态页面。
接下来,为了访问这些静态资源,我们还需要修改Spring Security配置。
不同于之前表单登录文章中使用的“login.jsp”,这里我们使用“index.html”作为登录页面:
@Configuration
@EnableWebSecurity
@Profile("!https")
public class SecSecurityConfig
extends WebSecurityConfigurerAdapter {
//...
@Override
protected void configure(final HttpSecurity http)
throws Exception {
http.csrf().disable().authorizeRequests()
//...
.antMatchers(
HttpMethod.GET,
"/index*", "/static/**", "/*.js", "/*.json", "/*.ico")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/index.html")
.loginProcessingUrl("/perform_login")
.defaultSuccessUrl("/homepage.html",true)
.failureUrl("/index.html?error=true")
//...
}
}
从上面的代码片段中可以看到,登录将发送POST请求到/perform_login
,如果登录验证成功,Spring会将我们重定向到/homepage.html
,否则重定向到`/index.html?error=true”。
4. React 组件
下面开始编写React代码,创建相关表单组件。
注意我们使用的是ES6 (ECMAScript 2015)语法。
4.1. Input输入框
让我们从一个Input组件开始,该组件封装了Form
中<input/>
标签。
react/src/Input.js
:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
class Input extends Component {
constructor(props){
super(props)
this.state = {
value: props.value? props.value : '',
className: props.className? props.className : '',
error: false
}
}
//...
render () {
const {handleError, ...opts} = this.props
this.handleError = handleError
return (
<input {...opts} value={this.state.value}
onChange={this.inputChange} className={this.state.className} />
)
}
}
Input.propTypes = {
name: PropTypes.string,
placeholder: PropTypes.string,
type: PropTypes.string,
className: PropTypes.string,
value: PropTypes.string,
handleError: PropTypes.func
}
export default Input
如上所示,我们将<input/>
标签包装到React控件组件中,目的是方便状态管理以及验证输入字段。
React中可以使用PropTypes, 进行类型检查。具体来说,我们使用Input.propTypes = {…}
来验证用户传递的属性的类型。
请注意,PropType检查仅适用于开发。 PropType确保组件接收到的数据类型是有效的
4.2. Form表单
接下来,我们在Form.js
文件中创建一个通用的Form
组件,该组件组合了多个Input组件,我们可以以此为基础建立登录表单。
在Form
组件中,我们接收HTML<input />
标签的属性,并从中创建Input
组件。
然后将Input
组件和错误消息提示插入到From
中:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Input from './Input'
class Form extends Component {
//...
render() {
const inputs = this.props.inputs.map(
({name, placeholder, type, value, className}, index) => (
<Input key={index} name={name} placeholder={placeholder} type={type} value={value}
className={type==='submit'? className : ''} handleError={this.handleError} />
)
)
const errors = this.renderError()
return (
<form {...this.props} onSubmit={this.handleSubmit} ref={fm => {this.form=fm}} >
{inputs}
{errors}
</form>
)
}
}
Form.propTypes = {
name: PropTypes.string,
action: PropTypes.string,
method: PropTypes.string,
inputs: PropTypes.array,
error: PropTypes.string
}
export default Form
现在让我们看一下如何实现字段验证和登录错误提示:
class Form extends Component {
constructor(props) {
super(props)
if(props.error) {
this.state = {
failure: 'wrong username or password!',
errcount: 0
}
} else {
this.state = { errcount: 0 }
}
}
handleError = (field, errmsg) => {
if(!field) return
if(errmsg) {
this.setState((prevState) => ({
failure: '',
errcount: prevState.errcount + 1,
errmsgs: {...prevState.errmsgs, [field]: errmsg}
}))
} else {
this.setState((prevState) => ({
failure: '',
errcount: prevState.errcount===1? 0 : prevState.errcount-1,
errmsgs: {...prevState.errmsgs, [field]: ''}
}))
}
}
renderError = () => {
if(this.state.errcount || this.state.failure) {
const errmsg = this.state.failure
|| Object.values(this.state.errmsgs).find(v=>v)
return <div className="error">{errmsg}</div>
}
}
//...
}
上面代码中,我们定义了handleError
方法来管理表单的错误状态。回想一下,在Input
字段验证中也用到了。实际上,handleError()
作为render()
方法中的回调函数传递给Input
组件。
我们使用renderError()
构造错误消息。请注意,Form
的构造函数使用error属性。此属性指示登录操作是否失败。
然后是Form表单提交处理:
class Form extends Component {
//...
handleSubmit = (event) => {
event.preventDefault()
if(!this.state.errcount) {
const data = new FormData(this.form)
fetch(this.form.action, {
method: this.form.method,
body: new URLSearchParams(data)
})
.then(v => {
if(v.redirected) window.location = v.url
})
.catch(e => console.warn(e))
}
}
}
我们将所有Form表单字段封装到FormData
中,然后使用fetch API提交请求。
我们不要忘记我们在Spring Security中配置了successUrl
和failureUrl
,这意味着无论请求是否成功,响应都需要重定向。
这就是为什么我们需要在响应回调中处理重定向。
4.3. Form表单渲染
现在我们已经创建好了所有组件,继续把他们放到DOM
中。HTML基础结构如下(react/public/index.html
):
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
</head>
<body>
<div id="root">
<div id="container"></div>
</div>
</body>
</html>
最后,在react/src/index.js
中,将Form
渲染到id为container
的<div/>
下:
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import Form from './Form'
const inputs = [{
name: "username",
placeholder: "username",
type: "text"
},{
name: "password",
placeholder: "password",
type: "password"
},{
type: "submit",
value: "Submit",
className: "btn"
}]
const props = {
name: 'loginForm',
method: 'POST',
action: '/perform_login',
inputs: inputs
}
const params = new URLSearchParams(window.location.search)
ReactDOM.render(
<Form {...props} error={params.get('error')} />,
document.getElementById('container'))
目前Form表单有2个输入框:username
和password
以及一个提交按钮。
在这里,我们将一个额外的error
属性传递给Form组件,因为我们要在重定向到登录失败URL:/index.html?error=true
后处理登录错误。
现在,我们已经完成了使用React构建Spring Security登录页面。最后我们需要做的是运行mvn compile
。
在此过程中,Maven插件将帮助构建React应用程序,并将打包后的结果放到src/main/webapp/WEB-INF/view/react/build
中。
5. 总结
在本文中,我们介绍了如何构建一个React登录应用,以及如何与Spring Security后端进行交互。 一个更复杂的应用程序将涉及使用React Router
路由或Redux
进行状态管理,但这超出了本文的范围。
惯例, 完整源码实现可从GitHub上获取。要本地运行, 在根目录执行mvn jetty:run
,然后访问http://localhost:8080