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时,此插件将下载nodejsnpm,安装所需的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中配置了successUrlfailureUrl,这意味着无论请求是否成功,响应都需要重定向。

这就是为什么我们需要在响应回调中处理重定向。

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个输入框:usernamepassword以及一个提交按钮。

在这里,我们将一个额外的error属性传递给Form组件,因为我们要在重定向到登录失败URL:/index.html?error=true后处理登录错误。

form login error

现在,我们已经完成了使用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