Spring Security Login Page with React – 使用React的Spring安全登录页面

最后修改: 2018年 7月 16日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

React is a component-based JavaScript library built by Facebook. With React, we can build complex web applications with ease. In this article, we’re going to make Spring Security work together with a React Login page.

React是一个由Facebook构建的基于组件的JavaScript库。通过React,我们可以轻松地构建复杂的Web应用程序。在这篇文章中,我们将使Spring Security与React登录页面一起工作。

We’ll take advantage of the existing Spring Security configurations of previous examples. So we’ll build on top of a previous article about creating a Form Login with Spring Security.

我们将利用以前的例子中现有的Spring Security配置的优势。因此,我们将在之前关于创建使用Spring Security的表单登录的文章的基础上进行。

2. Set up React

2.设置React

First, let’s use the command-line tool create-react-app to create an application by executing the command “create-react-app react”.

首先,让我们使用命令行工具create-react-app来创建一个应用程序,执行命令”create-react-app react”

We’ll have a configuration like the following in react/package.json:

我们将在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"
    }
}

Then, we’ll use the frontend-maven-plugin to help build our React project with Maven:

然后,我们将使用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>

The latest version of the plugin can be found here.

该插件的最新版本可以在这里找到。

When we run mvn compile, this plugin will download node and npm, install all node module dependencies and build the react project for us.

当我们运行mvn compile时,这个插件将下载nodenpm,安装所有node模块依赖,并为我们构建react项目。

There are several configuration properties we need to explain here. We specified the versions of node and npm, so that the plugin will know which version to download.

这里我们需要解释几个配置属性。我们指定了nodenpm的版本,这样插件就能知道要下载哪个版本。

Our React login page will serve as a static page in Spring, so we use “src/main/webapp/WEB-INF/view/react” as npm‘s working directory.

我们的React登录页面将作为Spring的静态页面,所以我们使用”src/main/webapp/WEB-INF/view/react“作为npm的工作目录。

3. Spring Security Configuration

3.Spring安全配置

Before we dive into the React components, we update the Spring configuration to serve the static resources of our React app:

在我们深入研究React组件之前,我们先更新Spring的配置,为我们的React应用提供静态资源。

@EnableWebMvc
@Configuration
public class MvcConfig extends WebMvcConfigurer {

    @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");
    }
}

Note that we add the login page “index.html” as a static resource instead of a dynamically served JSP.

注意,我们将登录页面“index.html”添加为静态资源,而不是动态服务的JSP。

Next, we update the Spring Security configuration to allow access to these static resources.

接下来,我们更新Spring Security配置,允许访问这些静态资源。

Instead of using “login.jsp” as we did in the previous form login article, here we use “index.html” as our Login page:

我们没有像在上一篇表单登录文章中那样使用“login.jsp”,而是在这里使用“index.html”作为我们的Login页。

@Configuration
@EnableWebSecurity
@Profile("!https")
public class SecSecurityConfig {

    //...

    @Bean
    public SecurityFilterChain filterChain(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")
          //...
    }
}

As we can see from the snippet above when we post form data to “/perform_login“, Spring will redirect us to “/homepage.html” if the credentials match successfully and to “/index.html?error=true” otherwise.

从上面的片段中我们可以看到,当我们将表单数据发布到”/perform_login“时,如果证书匹配成功,Spring会将我们重定向到”/homepage.html“,否则会重定向到”/index.html? error=true“。

4. React Components

4.React组件

Now let’s get our hands dirty on React. We’ll build and manage a form login using components.

现在让我们在React上动手吧。我们将使用组件构建和管理一个表单登录。

Note that we’ll use ES6 (ECMAScript 2015) syntax to build our application.

注意,我们将使用ES6(ECMAScript 2015)语法来构建我们的应用程序。

4.1. Input

4.1.输入

Let’s start with an Input component that backs the <input /> elements of the login form in react/src/Input.js:

让我们从一个Input组件开始,它支持<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

As seen above, we wrap the <input /> element into a React controlled component to be able to manage its state and perform field validation.

如上所述,我们将<input />元素包装成一个React控制的组件,以便能够管理其状态并执行字段验证。

React provides a way to validate the types using PropTypes. Specifically, we use Input.propTypes = {…} to validate the type of properties passed in by the user.

React提供了一种使用PropTypes验证类型的方法。具体来说,我们使用Input.propTypes = {…}来验证用户传入的属性类型。

Note that PropType validation works for development only. PropType validation is to check that all the assumptions that we’re making about our components are being met.

请注意,PropType验证仅适用于开发。PropType验证是为了检查我们对组件的所有假设是否得到满足。

It’s better to have it rather than getting surprised by random hiccups in production.

与其在生产中被随机出现的小插曲吓一跳,不如拥有它。

4.2. Form

4.2 形式

Next, we’ll build a generic Form component in the file Form.js that combines multiple instances of our Input component on which we can base our login form.

接下来,我们将在文件Form.js中建立一个通用的表单组件,它结合了我们的Input组件的多个实例,我们可以在此基础上建立登录表单。

In the Form component, we take attributes of HTML <input/> elements and create Input components from them.

Form组件中,我们采用HTML <input/>元素的属性,并从中创建Input组件。

Then the Input components and validation error messages are inserted into the Form:

然后,输入组件和验证错误信息被插入表格:

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

Now let’s take a look at how we manage field validation errors and login error:

现在让我们来看看我们如何管理字段验证错误和登录错误。

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>
        }
    }

    //...

}

In this snippet, we define the handleError function to manage the error state of the form. Recall that we also used it for Input field validation. Actually, handleError() is passed to the Input Components as a callback in the render() function.

在这个片段中,我们定义了handleError函数来管理表单的错误状态。回顾一下,我们也曾将其用于Input字段验证。实际上,handleError()是作为render()函数的回调传递给Input Components的。

We use renderError() to construct the error message element. Note that Form’s constructor consumes an error property. This property indicates if the login action fails.

我们使用renderError()来构造错误信息元素。请注意,Form的构造函数会消耗一个error属性。这个属性表示登录动作是否失败。

Then comes the form submission handler:

然后是表单提交处理程序。

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))
        }
    }
}

We wrap all form fields into FormData and send it to the server using the fetch API.

我们将所有表单字段包装成FormData,并使用fetch API将其发送到服务器。

Let’s not forget our login form comes with a successUrl and failureUrl, meaning that no matter if the request is successful or not, the response would require a redirection.

不要忘了我们的登录表单带有successUrlfailureUrl,意味着无论请求是否成功,响应都需要重定向。

That’s why we need to handle redirection in the response callback.

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

4.3. Form Rendering

4.3 表格渲染

Now that we’ve set up all the components we need, we can continue to put them in the DOM. The basic HTML structure is as follows (find it under react/public/index.html):

现在我们已经设置了所有需要的组件,我们可以继续把它们放在DOM中。基本的HTML结构如下(在react/public/index.html下找到它)。

<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- ... -->
  </head>
  <body>

    <div id="root">
      <div id="container"></div>
    </div>

  </body>
</html>

Finally, we’ll render the Form into the <div/> with id “container” in react/src/index.js:

最后,我们将把表单渲染到<div/>中,ID为”container”react/src/index.js

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'))

So our form now contains two input fields: username and password, and a submit button.

所以我们的表单现在包含两个输入字段。用户名密码,以及一个提交按钮。

Here we pass an additional error attribute to the Form component because we want to handle login error after redirection to the failure URL: /index.html?error=true.

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

form login error

Now we’ve finished building a Spring Security login application using React. The last thing we need to do is to run mvn compile.

现在我们已经完成了使用React构建一个Spring Security登录应用程序。我们需要做的最后一件事是运行mvn compile

During the process, the Maven plugin will help build our React application and gather the build result in src/main/webapp/WEB-INF/view/react/build.

在这个过程中,Maven插件将帮助我们构建React应用,并将构建结果收集到src/main/webapp/WEB-INF/view/react/build

5. Conclusion

5.总结

In this article, we’ve covered how to build a React login app and let it interact with a Spring Security backend. A more complex application would involve state transition and routing using React Router or Redux, but that’d be beyond the scope of this article.

在这篇文章中,我们已经介绍了如何构建一个React登录应用程序,并让它与Spring Security后端进行交互。更复杂的应用程序将涉及使用React RouterRedux的状态转换和路由,但这将超出本文的范围。

As always, the full implementation can be found over on GitHub. To run it locally, execute mvn jetty:run in the project root folder, then we can access the React login page at http://localhost:8080.

一如既往,完整的实现可以在GitHub上找到。要在本地运行它,请在项目根文件夹中执行mvn jetty:run,然后我们可以访问React登录页面http://localhost:8080