Spring Security 简明教程
Spring Security - Form Login with Database
Contents
-
Introduction and Overview
-
Basic components of Spring Security AuthenticationFilterAuthenticationManagerAuthenticationProviderUserDetailsServicePasswordEncoderSpring Security ContextForm LoginLogin with a DatabaseLogin Attempts Limit
-
Getting Started (Practical Guide)
Introduction and Overview
除了提供各种内置认证和授权选项之外,Spring Security 还允许我们尽可能地自定义认证流程,从自定义登录页面到我们自己的自定义认证提供程序和认证过滤器,我们几乎可以自定义认证流程的每一个方面。我们可以定义自己的认证流程,该流程可以从使用用户名和密码的基本认证到使用令牌和 OTP 的复杂认证,例如双因素认证。此外,我们可以使用各种数据库(包括关系型和非关系型数据库),使用各种密码编码器,将恶意用户锁定在账户之外,等等。
In addition to providing various inbuilt authentication and authorization options, Spring Security allows us to customize our authentication process as much as we want. Starting from a custom login page to our very own customized authentication providers and authentication filters, we can pretty much customize every aspect of the authentication process. We can define our own authentication process which can range from basic authentication using a username and a password to a complex one such as two-factor authentication using tokens and OTP’s. Also, we can use various databases – both relational and non-relational, use various password encoders, lock malicious users out of their accounts, and so on.
今天,我们将讨论三种此类自定义,即自定义表单登录、数据库提供的认证和限制登录尝试次数。虽然这些是很基本的用例,但它们仍然可以让我们更深入地了解 Spring Security 的认证和授权流程。我们还将设置一个注册页面,用户可以通过该页面注册我们应用程序。
Today, we are going to discuss three such customizations, namely – custom form-login, a database provided authentication, and limiting login attempts. Though these are pretty basic use-cases, yet these still will let us have a closer look into Spring Security’s authentication and authorization process. We are also going to set up a registration page through which the users will be able to register themselves with our application.
首先,让我们了解一下 Spring Security 的架构。它从 servlet 过滤器开始。这些过滤器会拦截请求,对请求执行操作,然后将请求传递给过滤器链中的下一个过滤器或请求处理程序,或者在请求不满足某些条件时会阻止请求。在这一过程中,Spring Security 可以认证请求并在请求上执行各种认证检查。它还可以通过不允许未认证或恶意请求访问受保护的资源来阻止这些请求,从而保护其不受访问。因此,我们的应用程序和资源将受到保护。
First of all, let’s take a look at the architecture of Spring Security. It starts with servlet filters. These filters intercept requests, perform operations on them, and then pass the requests on to next filters in the filter chain or request handlers or block them if they do not meet certain conditions. It is during this process that Spring Security can authenticate requests and perform various authentication checks on the requests. It can also prevent unauthenticated or malicious requests from accessing our protected resources by not allowing them to pass through. Thus our application and resources stay protected.
Components of Spring Security Architecture
正如我们在上图中看到的,Spring Security 的基本组件如下。我们将在进行过程中对其进行简要讨论。我们还将讨论它们在身份验证和授权过程中的作用。
The basic components of Spring Security, as we can see in the above diagram are given below. We shall discuss them briefly as we go along. We shall also discuss their roles in the authentication and authorization process.
AuthenticationFilter
这是拦截请求并尝试对其进行身份验证的过滤器。在 Spring Security 中,它将请求转换为一个 Authentication Object,并将身份验证委托给 AuthenticationManager。
This is the filter that intercepts requests and attempts to authenticate it. In Spring Security, it converts the request to an Authentication Object and delegates the authentication to the AuthenticationManager.
AuthenticationManager
这是用于身份验证的主要策略界面。它使用单一方法 authenticate() 来对请求进行身份验证。authenticate() 方法执行身份验证,并在身份验证成功后返回一个 Authentication Object,或者在身份验证失败的情况下抛出 AuthenticationException。如果该方法无法判断,它将返回 null。此过程中的身份验证过程将委托给 AuthenticationProvider,我们将在后面讨论它。
It is the main strategy interface for authentication. It uses the lone method authenticate() to authenticate the request. The authenticate() method performs the authentication and returns an Authentication Object on successful authentication or throw an AuthenticationException in case of authentication failure. If the method can’t decide, it will return null. The process of authentication in this process is delegated to the AuthenticationProvider which we will discuss next.
AuthenticationProvider
AuthenticationManager 由 ProviderManager 实现,后者将进程委托给一个或多个 AuthenticationProvider 实例。实现 AuthenticationProvider 接口的任何类都必须实现两个方法——authenticate() 和 supports()。首先,让我们谈谈 supports() 方法。它用于检查我们的 AuthenticationProvider 实现类是否支持特定的身份验证类型。如果支持,则返回 true,否则返回 false。接下来是 authenticate() 方法。身份验证发生在这里。如果支持身份验证类型,则开始身份验证进程。在这里,此类可以使用 UserDetailsService 实现的 loadUserByUsername() 方法。如果找不到用户,它可能会抛出 UsernameNotFoundException。
The AuthenticationManager is implemented by the ProviderManager which delegates the process to one or more AuthenticationProvider instances. Any class implementing the AuthenticationProvider interface must implement the two methods – authenticate() and supports(). First, let us talk about the supports() method. It is used to check if the particular authentication type is supported by our AuthenticationProvider implementation class. If it is supported it returns true or else false. Next, the authenticate() method. Here is where the authentication occurs. If the authentication type is supported, the process of authentication is started. Here is this class can use the loadUserByUsername() method of the UserDetailsService implementation. If the user is not found, it can throw a UsernameNotFoundException.
另一方面,如果找到了用户,那么会使用用户的认证详细信息来对用户进行认证。例如,在基本认证方案中,可能会使用数据库中的密码来检查用户提供的密码。如果发现它们彼此匹配,则是一个成功场景。然后,我们可以从方法中返回一个认证对象,该对象将存储在安全上下文中,我们将在后面讨论安全上下文。
On the other hand, if the user is found, then the authentication details of the user are used to authenticate the user. For example, in the basic authentication scenario, the password provided by the user may be checked with the password in the database. If they are found to match with each other, it is a success scenario. Then we can return an Authentication object from this method which will be stored in the Security Context, which we will discuss later.
UserDetailsService
它是 Spring Security 的核心接口之一。任何请求的认证主要取决于 UserDetailsService 接口的实现。它最常用于基于数据库的认证中以检索用户数据。数据通过实现唯一的 loadUserByUsername() 方法来检索,在该方法中,我们可以提供逻辑以获取用户的用户详细信息。如果找不到用户,该方法将抛出 UsernameNotFoundException。
It is one of the core interfaces of Spring Security. The authentication of any request mostly depends on the implementation of the UserDetailsService interface. It is most commonly used in database backed authentication to retrieve user data. The data is retrieved with the implementation of the lone loadUserByUsername() method where we can provide our logic to fetch the user details for a user. The method will throw a UsernameNotFoundException if the user is not found.
PasswordEncoder
在 Spring Security 4 之前,使用 PasswordEncoder 是可选的。用户可以使用基于内存的认证来存储明文密码。但是 Spring Security 5 强制要求使用 PasswordEncoder 来存储密码。这会使用其许多实现之一对用户的密码进行编码。其最常见的实现是 BCryptPasswordEncoder。此外,出于开发目的,我们可以使用 NoOpPasswordEncoder 的一个实例。它将允许密码以明文存储。但它不应该用于生产或实际应用程序。
Until Spring Security 4, the use of PasswordEncoder was optional. The user could store plain text passwords using in-memory authentication. But Spring Security 5 has mandated the use of PasswordEncoder to store passwords. This encodes the user’s password using one its many implementations. The most common of its implementations is the BCryptPasswordEncoder. Also, we can use an instance of the NoOpPasswordEncoder for our development purposes. It will allow passwords to be stored in plain text. But it is not supposed to be used for production or real-world applications.
Spring Security Context
这是在成功认证时存储当前已认证用户详细信息的位置。然后,整个应用程序在会话期间都可以使用认证对象。因此,如果我们需要用户名或任何其他用户详细信息,则需要首先获得 SecurityContext。这是通过 SecurityContextHolder 一个提供程序类来完成的,它提供对安全上下文的访问。我们可以分别使用 setAuthentication() 和 getAuthentication() 方法来存储和检索用户详细信息。
This is where the details of the currently authenticated user are stored on successful authentication. The authentication object is then available throughout the application for the session. So, if we need the username or any other user details, we need to get the SecurityContext first. This is done with the SecurityContextHolder, a helper class, which provides access to the security context. We can use the setAuthentication() and getAuthentication() methods for storing and retrieving the user details respectively.
继续,现在让我们讨论我们将用于我们应用程序的三个自定义实现。
Moving on, let’s now discuss the three custom implementations we are going to use for our application.
Form Login
当我们将 Spring Security 添加到现有的 Spring 应用程序时,它会添加一个登录表单并设置一个虚拟用户。这是自动配置模式下的 Spring Security。在此模式下,它还设置了默认过滤器、身份验证管理器、身份验证提供程序等。此设置是内存中身份验证设置。我们可以覆盖此自动配置以设置我们自己的用户和身份验证流程。我们还可以设置我们自定义的登录方法,例如自定义登录表单。Spring Security 仅需要了解登录表单的详细信息,如 - 登录表单的 URI、登录处理 URL 等。然后,它将为应用程序呈现我们的登录表单,并执行身份验证过程以及其他提供的配置或 Spring 自己的实现。
When we add Spring Security to an existing Spring application it adds a login form and sets up a dummy user. This is Spring Security in auto-configuration mode. In this mode, it also sets up the default filters, authentication-managers, authentication-providers, and so on. This setup is an in-memory authentication setup. We can override this auto-configuration to set up our own users and authentication process. We can also set up our custom login method like a custom login form. Spring Security only has to made aware of the details of the login form like – the URI of the login form, the login processing URL, etc.. It will then render our login form for the application and carry out the process of authentication along with the other provided configurations or Spring’s own implementation.
此自定义表单设置只需遵守特定规则即可集成到 Spring Security 中。我们需要一个用户名参数和一个密码参数,并且这两个参数的名称应分别为“username”和“password”,因为这是默认的名称。在此自定义设置中,如果我们为这些字段使用自己的参数名称,则必须使用 usernameParameter() 和 passwordParameter() 方法通知 Spring Security 这些更改。同样,对于我们对登录表单或表单登录方法所做的任何更改,我们都必须使用适当的方法通知 Spring Security 这些更改,以便它可以将这些更改集成到认证流程中。
This custom form setup will only have to abide by certain rules to be integrated with Spring Security. We need to have a username parameter and a password parameter and the parameter names should be “username” and “password” since those are the default names. In case, we use our own parameter names for these fields in the custom we have to inform Spring Security of those changes using the usernameParameter() and passwordParameter() methods. Similarly, for every change we do to the login form or the form login method, we will have to inform Spring Security of those changes with appropriate methods so that it can integrate them with the authentication process.
Login with a Database
正如我们所讨论的,Spring Security 默认情况下自动提供一种内存认证实现。我们可以通过对存储在数据库中的用户详细信息进行认证来覆盖此认证实现。在这种情况下,在认证用户时,我们可以针对认证检查用户提供的凭据与数据库中的凭据是否一致。我们还可以允许新用户在我们应用程序中注册,并将他们的凭据存储在同一数据库中。此外,我们可以提供更改或更新其密码、角色或其他数据的方法。因此,这为我们提供了持久的用户数据,可长期使用。
As we discussed, Spring Security automatically provides an in-memory authentication implementation by default. We can override this by authenticating users whose details are stored in a database. In this case, while authenticating a user, we can verify the credentials provided by the user against those in the database for authentication. We can also let new users register in our application and store their credentials in the same database. Also, we can provide methods to change or update their passwords or roles or other data. As a result, this provides us with persistent user data which can be used for longer periods of time.
Login Attempts Limit
为了限制我们应用程序中的登录尝试次数,我们可以使用 Spring Security 的 isAccountNonLocked 属性。Spring Security 的 UserDetails 为我们提供了该属性。我们可以设置一种认证方法,其中,如果任何用户或其他人多次提供不正确的凭据,我们可以锁定他们的账户。即使用户提供了正确的凭据,Spring Security 也将禁用对已锁定用户的认证。这是 Spring Security 提供的一项内置功能。我们可以将不正确的登录尝试次数存储在我们的数据库中。然后,对于每次不正确的认证尝试,我们都可以更新并检查数据库表。当此类尝试的次数超过给定次数时,我们可以将用户锁定在账户之外。因此,用户在账户解锁之前将无法再次登录。
To limit login attempts in our application we can use Spring Security’s isAccountNonLocked property. Spring Security’s UserDetails provides us with that property. We can set up an authentication method wherein, if any user or someone else provides incorrect credentials for more than a certain number of times, we can lock their account. Spring Security disables authentication for a locked user even if the user provides correct credentials. This is an in-built feature provided by Spring Security. We can store the number of incorrect login attempts in our database. Then against each incorrect authentication attempt, we can update and check with the database table. When the number of such attempts exceeds a given number, we can lock the user out of their account. Consequently, the user will not be able to log in again until their account is unlocked.
Getting Started (Practical Guide)
现在让我们从我们的应用程序开始。我们需要为此应用程序的工具如下所列:
Let’s start with our application now. The tools we will be needing for this application are listed below −
-
A Java IDE − preferable STS 4, but Eclipse, IntelliJ Idea or any other IDE will do.
-
MySql Server Community Edition − We need to download and install MySql Community Server in our system. We can go to the official website by clicking here.
-
MySql Workbench − It is a GUI tool that we can use to interact with MySql databases.
Database Setup
首先,让我们设置数据库。我们将为此应用程序使用一个 MySql 数据库实例。 MySql Server Community Edition 可免费下载和使用。我们将使用 MySql Workbench 连接到我们的 MySql Server 并创建一个名为“spring”的数据库,以与我们的应用程序一起使用。
Let’s set up the database first. We will use a MySql database instance for this application. MySql Server Community Edition is available for free download and use. We will use MySql Workbench to connect with our MySql Server and create a database called “spring” to use with our application.
然后,我们将创建两个表(users 和 attempts),以持久化我们的用户和登录尝试。如前所述,注册我们应用程序的用户详细信息将存储在 users 表中。任何用户登录尝试的次数都将在 attempts 表中针对他的用户名存储。通过这种方式,我们可以跟踪尝试并采取必要的措施。
Then we will create two tables – users and attempts– to persist our users and login attempts. As mentioned earlier, the details of the users registering with our application will be stored in the users table. The number of login attempts by any user will be stored in the attempts table against his username. This way we can track the attempts and take necessary action.
让我们了解一下用于设置我们的 users 表和 attempts 表的 SQL。
Let’s take a the look at the SQL to setup our users table and attempts table.
CREATE TABLE users (
username VARCHAR(45) NOT NULL , password VARCHAR(45) NOT NULL ,
account_non_locked TINYINT NOT NULL DEFAULT 1 ,
PRIMARY KEY (username)
);
CREATE TABLE attempts (
id int(45) NOT NULL AUTO_INCREMENT,
username varchar(45) NOT NULL, attempts varchar(45) NOT NULL, PRIMARY KEY (id)
);
我们现在可以向我们的应用程序添加一个虚拟用户。
We can now add a dummy user to our application.
INSERT INTO users(username,password,account_non_locked)
VALUES ('user','12345', true);
Project Setup
和往常一样,我们将使用 Spring Initializer 设置我们的项目。我们将创建一个 Maven 项目,使用 Spring Boot 版本 2.3.2。让我们将我们的项目命名为 formlogin(我们可以选择任何我们想要的名称),并将组 ID 命名为 com.tutorial.spring.security。此外,我们将为此项目使用 Java 版本 8。
As usual, we will use the Spring Initializer to setup our project. We are going to create a Maven project, with Spring Boot version 2.3.2. Let’s name our project formlogin(we can choose any name we want) and group id as com.tutorial.spring.security. Furthermore, we will use Java version 8 for this project.
Dependencies
现在,对于依赖项,我们将为这个演示尽可能地让我们的应用程序保持简单。我们将专注于我们今天想要探索的功能。因此,我们将选择最少的依赖项,以帮助我们快速设置和启动应用程序。让我们了解一下依赖项:
Now, coming to the dependencies, we are going to keep our application as simple as possible for this demo. We will keep our focus on the features we want to explore today. So we will choose the minimum number of dependencies that will help us set up our application and get it up and running quickly. Let’s go through the dependencies −
-
Spring Web − It bundles all dependencies related to web development including Spring MVC, REST, and an embedded Tomcat Server.
-
Spring Security − For the implementation of security features provided by Spring Security.
-
Thymeleaf − A server-side Java template engine for HTML5/XHTML/XML.
-
Spring Data JPA − In addition to using all features defined by JPA specification, Spring Data JPA adds its own features such as the no-code implementation of the repository pattern and the creation of database queries from the method name.
-
Mysql Driver − For the MySQL database driver.
有了这五个依赖项,我们现在就可以设置我们的项目了。让我们单击生成按钮。这将下载我们的项目为 zip 文件。我们可以将其解压到我们选择的文件夹中。然后我们在 IDE 中打开项目。我们将为此使用 Spring Tool Suite 4。
With these five dependencies, we can set up our project now. Let’s click on the generate button. This will download our project as a zip file. We can extract it to a folder of our choice. Then we open the project in our IDE. We will be using Spring Tool Suite 4 for this. example.
让我们将我们的项目加载到 STS 中。我们的 IDE 需要一些时间才能下载依赖项并对其进行验证。让我们看看我们的 pom.xml 文件。
Let’s load our project into STS. It will take a little time for our IDE to download the dependencies and validating them. Let’s take a look at our pom.xml file.
pom.xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/>
<!-- lookup parent from repository -->
</parent>
<groupId>com.tutorial.spring.security</groupId>
<artifactId>formlogin</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>formlogin</name>
<description>Demo project for Spring Boot</description>
<properties> <java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime<scope> <optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test<artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
我们可以看到此处列出了我们的项目详细信息以及依赖项。
We can see that our project details along with our dependencies are enlisted here.
Data Source
我们将在 application.properties 文件中配置我们的数据源。由于我们将使用本地 MySQL DB 作为数据源,因此我们在此处提供本地 DB 实例的 url、用户名和密码。我们已将数据库命名为“spring”。
We will configure our data source in the application.properties file. As we will be using our local MySQL DB as the data source, so we provide the url, username, and password of our local DB instance here. We have named our database as “spring”.
spring.datasource.url=jdbc:mysql://localhost:3306/spring
spring.datasource.username=root
spring.datasource.password=root
Entities
让我们现在创建我们的实体。我们从 User 实体开始,它包含三个字段 - 用户名、密码和 accountNonLocked。此 User 类还实现了 Spring Security 的 UserDetails 接口。此类提供核心用户信息。它用于存储稍后可封装到 Authentication 对象中的用户数据。不建议直接实现接口。但对于我们的案例,由于这是一个演示基于数据库的登录的简单应用程序,我们直接在这里实现了此接口,以保持简单。我们可以通过在我们的 User 实体周围使用包装器类来实现此接口。
Let’s create our entities now. We start with the User entity which contains three fields – username, password, and accountNonLocked. This User class also implements the UserDetails interface of Spring Security. This class provides core user information. It is used to store user data which can be later encapsulated into Authentication objects. It is not recommended to implement the interface directly. But for our case, since this is a simple application to demonstrate the login with a database, we have implemented this interface directly here to keep thingssimple. We can implement this interface by using a wrapper class around our User entity.
User.java
User.java
package com.tutorial.spring.security.formlogin.model;
import java.util.Collection;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
@Entity
@Table(name = "users")
public class User implements UserDetails {
/**
*
*/
private static final long serialVersionUID = 1L;
@Id
private String username;
private String password; @Column(name = "account_non_locked")
private boolean accountNonLocked;
public User() {
}
public User(String username, String password, boolean accountNonLocked) {
this.username = username;
this.password = password;
this.accountNonLocked = accountNonLocked;
}
@Override
public Collection< extends GrantedAuthority> getAuthorities() {
return List.of(() -> "read");
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override public boolean isCredentialsNonExpired() {
return true;
}
@Override public boolean isEnabled() {
return true;
}
public void setAccountNonLocked(Boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
}
public boolean getAccountNonLocked() {
return accountNonLocked;
}
}
此处应注意 accountNonLocked 字段。Spring Security 中的每个用户默认情况下都已解锁。为了覆盖该属性并当用户超过允许尝试次数时将其锁定在帐户之外,我们将使用此属性。如果用户超过允许的不成功尝试次数,我们将使用此属性将其锁定在帐户之外。而且,在每次身份验证尝试期间,我们将使用 isAccountNonLocked() 方法以及凭据检查此属性以对用户进行身份验证。具有锁定帐户的任何用户将不被允许验证到应用程序中。
The accountNonLocked field is to be noted here. Every User in Spring Security has the account unlocked by default. To override that property and to lock the users out of their accounts once they exceed the permissible number of attempts, we shall be using this property. If the user exceeds the number of invalid attempts permissible, we shall use this property to lock him out of his account. Also, during every authentication attempt, we shall be checking this property with the isAccountNonLocked() method along with the credentials to authenticate the user. Any user with a locked account will not be allowed to authenticate into the application.
对于 UserDetails 接口的其他方法,我们可以简单地提供一个实现来暂时返回 true,因为我们不会在此应用程序中探索这些属性。
For the other methods of the UserDetails interface, we can simply provide an implementation that returns true for now as we shall not be exploring these properties for this application.
对于此用户的权限列表,让我们暂时为他分配一个虚拟角色。我们也不会将此属性用于此应用程序。
For the list of authorities for this user, let’s assign him a dummy role for now. We shall not be using this property either for this application.
Attempts.java
Attempts.java
继续,让我们创建 Attempts 实体来持久化我们的无效尝试计数。如在数据库中创建的那样,我们在这里有三个字段 - username、一个名为 attempts 的整数以记录尝试次数以及标识符。
Moving on, let’s create our Attempts entity to persist our invalid attempts count. As created in the database, we will have the three fields here – username, an integer named attempts to keep counts of the number of attempts, and an identifier.
package com.tutorial.spring.security.formlogin.model;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Attempts {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String username;
private int attempts;
/**
* @return the id
*/
public int getId() {
return id;
}
/**
* @param id the id to set
*/
public void setId(int id) {
this.id = id;
}
/**
* @return the username
*/
public String getUsername() {
return username;
}
/**
* @param username the username to set
*/
public void setUsername(String username) {
this.username = username;
}
/**
* @return the attempts
*/
public int getAttempts() {
return attempts;
}
/**
* @param attempts the attempts to set
*/
public void setAttempts(int attempts) {
this.attempts = attempts;
}
}
Repositories
我们已经创建了实体,让我们创建存储库来存储和检索数据。我们将创建两个存储库,每个实体类一个。对于这两个存储库接口,我们将扩展 JpaRepository,它为我们提供了内置实现来保存和检索来自我们在 application.properties 文件中配置的数据库的数据。我们还可以在此处添加我们的方法或查询(除了提供的查询之外)。
We have created the entities, let’s create the repositories to store and retrieve data. We will have two repositories, one for each entity class. For both the repository interfaces, we will extend the JpaRepository which provides us with in-built implementations to save and retrieve data from the database configured in our application.properties file. We can also add our methods or queries here in addition to the provided ones.
UserRepository.java
UserRepository.java
package com.tutorial.spring.security.formlogin.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.tutorial.spring.security.formlogin.model.User;
@Repository public interface UserRepository extends JpaRepository<User, String> {
Optional<User> findUserByUsername(String username);
}
如前所述,我们已添加在此处按用户名检索用户的方法。这将返回我们的用户详细信息,包括用户名、密码和帐户锁定状态。
As discussed, we have added our method to retrieve a user by username here. This will return our user details including username, password and account locked status.
AttemptsRepository.java
AttemptsRepository.java
package com.tutorial.spring.security.formlogin.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.tutorial.spring.security.formlogin.model.Attempts;
@Repository
public interface AttemptsRepository extends JpaRepository<Attempts, Integer> {
Optional<Attempts> findAttemptsByUsername(String username);
}
同样地,对于 Attempts,在我们的 AttemptsRepository 中,我们添加了一个自定义方法 findAttemptsByUsername(String username) 使用用户名来获取有关用户尝试的数据。这会向我们返回一个 Attempts 对象,其中包含用户名和用户执行过的身份验证尝试的失败数目。
Similarly, for the Attempts, in our AttemptsRepository, we have added a custom method findAttemptsByUsername(String username) to get data about user attempts using the username. This will return us an Attempts object with the username and the number of failed authentication attempts the user has made.
Configuration
鉴于我们准备使用一个自定义登录表单,所以我们必须覆盖 Spring Security 的默认配置。要做到这一点,我们创建一个配置类,用来扩展 Spring Security 的 WebSecurityConfigurerAdapter 类。
Since we are going to use a custom login form, we have to override the default configuration of Spring Security. To do this we create our configuration class which extends the WebSecurityConfigurerAdapter class of Spring Security.
package com.tutorial.spring.security.formlogin.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class ApplicationConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests().antMatchers("/register**")
.permitAll() .anyRequest().authenticated()
.and()
.formLogin() .loginPage("/login")
.permitAll()
.and()
.logout() .invalidateHttpSession(true)
.clearAuthentication(true) .permitAll();
}
}
这里我们做了两件事:
Here we did two things−
-
First, we have specified the implementation of the PasswordEncoder interface that we are going to use. We have used an instance of BCryptPasswordEncoder to encode our passwords for this example. The PasswordEncoder interface has many implementations and we can use any of them. We have chosen BCryptPasswordEncoder here as it the most commonly used implementation. It uses the very strong BCrypt hashing algorithm to encode the passwords. It does so by incorporating a salt to protect against rainbow table attacks. In addition to this, bcrypt is an adaptive function: over time, the iteration count can be increased to make it slower, so it remains resistant to brute-force search attacks even with increasing computation power.
-
Secondly, we have overridden the configure() method to provide our implementation of the login method.
Security Setup
现在,我们将设置我们的身份验证过程。我们准备使用数据库和用户账户锁定机制来设置身份验证。
Now, we will setup our authentication process. We are going to setup authentication using a database and locking of user accounts.
我们首先创建我们的 UserDetailsService 实现。正如我们之前讨论的,我们需要针对使用数据库的身份验证提供我们的自定义实现。这是因为我们了解,Spring Security 默认情况下只提供了一个内存身份验证实现。因此,我们需要使用基于数据库的过程覆盖该实现。要做到这一点,我们需要覆盖 UserDetailsService 的 loadUserByUsername() 方法。
Let’s create our implementation of UserDetailsService first. As we have discussed before, we need to provide our custom implementation for authentication using a database. This is because, Spring Security, as we know, only provides an in-memory authentication implementation by default. Therefore, we need to override that implementation with our database based process. To do so, we need to override the loadUserByUsername() method of UserDetailsService.
UserDetailsService
package com.tutorial.spring.security.formlogin.security;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.stereotype.Service;
import com.tutorial.spring.security.formlogin.model.User;
import com.tutorial.spring.security.formlogin.repository.UserRepository;
@Service
public class SecurityUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userRepository.findUserByUsername(username)
.orElseThrow(() -< new UsernameNotFoundException("User not present"));
return user;
}
public void createUser(UserDetails user) {
userRepository.save((User) user);
}
}
我们可以看到,我们在这里实现了 loadUserByUsername() 方法。我们在这里使用 UserRepository 接口从我们的数据库中获取用户。如果未找到该用户,则抛出 UsernameNotFoundException。
As we can see here, we have implemented the loadUserByUsername() method here. Here we are fetching the user from our database using the UserRepository interface. If the user is not found it throws UsernameNotFoundException.
我们还有一个 createUser() 方法。我们使用此方法向我们的数据库添加那些在我们的应用程序中使用 UserRepository 进行注册的用户。
We also have a createUser() method. We will use this method to add users to our database who have registered in our application using UserRepository.
Authentication Provider
我们现在将实现我们的自定义身份验证提供程序。它将实现 AuthenticationProvider 接口。我们这里有两个需要覆盖和实现的方法:
We will now implement our custom authentication provider. It will implement the AuthenticationProvider interface. We have two methods here that we have to override and implement.
package com.tutorial.spring.security.formlogin.security;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import com.tutorial.spring.security.formlogin.model.Attempts;
import com.tutorial.spring.security.formlogin.model.User;
import com.tutorial.spring.security.formlogin.repository.AttemptsRepository;
import com.tutorial.spring.security.formlogin.repository.UserRepository;
@Component public class AuthProvider implements AuthenticationProvider {
private static final int ATTEMPTS_LIMIT = 3;
@Autowired
private SecurityUserDetailsService userDetailsService;
@Autowired private PasswordEncoder passwordEncoder;
@Autowired private AttemptsRepository attemptsRepository;
@Autowired private UserRepository userRepository;
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String username = authentication.getName();
import com.tutorial.spring.security.formlogin.repository.UserRepository;
@Component public class AuthProvider implements AuthenticationProvider {
private static final int ATTEMPTS_LIMIT = 3;
@Autowired private SecurityUserDetailsService userDetailsService;
@Autowired private PasswordEncoder passwordEncoder;
@Autowired private AttemptsRepository attemptsRepository;
@Autowired private UserRepository userRepository;
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String username = authentication.getName();
Optional<Attempts>
userAttempts = attemptsRepository.findAttemptsByUsername(username);
if (userAttempts.isPresent()) {
Attempts attempts = userAttempts.get();
attempts.setAttempts(0); attemptsRepository.save(attempts);
}
}
private void processFailedAttempts(String username, User user) {
Optional<Attempts>
userAttempts = attemptsRepository.findAttemptsByUsername(username);
if (userAttempts.isEmpty()) {
Attempts attempts = new Attempts();
attempts.setUsername(username);
attempts.setAttempts(1);
attemptsRepository.save(attempts);
} else {
Attempts attempts = userAttempts.get();
attempts.setAttempts(attempts.getAttempts() + 1);
attemptsRepository.save(attempts);
if (attempts.getAttempts() + 1 >
ATTEMPTS_LIMIT) {
user.setAccountNonLocked(false);
userRepository.save(user);
throw new LockedException("Too many invalid attempts. Account is locked!!");
}
}
}
@Override public boolean supports(Class<?> authentication) {
return true;
}
}
-
authenticate() − This method returns a fully authenticated object including credentials on successful authentication. This object is then stored in the SecurityContext. To perform authentication we will use the loaduserByUsername() method of the SecurityUserDetailsService class of our Application. Here we perform multiple things −
-
supports() − We also have the supports method that checks if our authentication type is supported by our AuthenticationProvider implementation class. It returns true, false, or null if it matches, doesn’t match, or if it can’t decide respectively. We have hardcoded it to be true for now.
Controller
现在让我们创建我们的控制器包。它将包含我们的 HelloController 类。使用此控制器类,我们将我们的视图映射到端点,并在命中断点时提供那些视图。我们也将在该组件中自动装配 PasswordEncoder 和 UserDetailsService 类。这些注入的依赖项将用于创建我们的用户。我们现在创建我们的端点。
Now let’s create our controller package. It will contain our HelloController class. Using this controller class we will map our views to the endpoints and serve those views when the respective endpoints are hit. We will also autowire the PasswordEncoder and the UserDetailsService classes in this component. These injected dependencies will be used in creating our user. Let’s now create our endpoints.
package com.tutorial.spring.security.formlogin.controller;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.tutorial.spring.security.formlogin.model.User;
import com.tutorial.spring.security.formlogin.security.SecurityUserDetailsService;
@Controller
public class HelloController {
@Autowired private SecurityUserDetailsService userDetailsManager;
@Autowired
private PasswordEncoder passwordEncoder;
@GetMapping("/")
public String index() {
return "index";
}
@GetMapping("/login")
public String login(HttpServletRequest request, HttpSession session) {
session.setAttribute(
"error", getErrorMessage(request, "SPRING_SECURITY_LAST_EXCEPTION")
);
return "login";
}
@GetMapping("/register")
public String register() {
return "register";
}
@PostMapping(
value = "/register",
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = {
MediaType.APPLICATION_ATOM_XML_VALUE, MediaType.APPLICATION_JSON_VALUE }
)
public void addUser(@RequestParam Map<String, String> body) {
User user = new User(); user.setUsername(body.get("username"));
user.setPassword(passwordEncoder.encode(body.get("password")));
user.setAccountNonLocked(true); userDetailsManager.createUser(user);
}
private String getErrorMessage(HttpServletRequest request, String key) {
Exception exception = (Exception) request.getSession().getAttribute(key);
String error = "";
if (exception instanceof BadCredentialsException) {
error = "Invalid username and password!";
} else if (exception instanceof LockedException) {
error = exception.getMessage();
} else {
error = "Invalid username and password!";
}
return error;
}
}
-
index ("/") – This endpoint will serve the index page of our application. As we have configured earlier, we shall be protecting this page and allow only authenticated users will be able to access this page.
-
login ("/login") – This will be used to serve our custom login page, as mentioned earlier. Any unauthenticated user will be redirected to this endpoint for authentication.
-
register("/register") (GET) – We will have two “register” endpoints for our application. One will be to serve the registration page. The other one will be to handle the registration process. So, the former one will use an Http GET and the latter will be a POST endpoint.
-
register("/register") (POST) – We will use this endpoint to handle the user registration process. We will get the user name and password from the parameters. Then we will encode the password using the passwordEncoder that we have @Autowired into this component. We also set user account as unlocked at this point. We will then save this user data in our users table with the createUser() method.
除上述内容之外,我们还有一个 getErrorMessage() 方法。它用于确定最近抛出的异常,以在我们的登录模版中添加一条消息。这样,我们可以了解身份验证错误并显示适当的消息。
In addition to the above, we have the getErrorMessage() method. It is used to determine the last thrown exception to add a message in our login template. This way, we can be aware of authentication errors and display proper messages.
Resources
我们已经创建了我们的端点,现在唯一要做的事情就是创建我们的视图。
We have created our endpoints, the only thing left is to create our views.
首先,我们将创建我们的索引页。该页面只有在验证成功后才允许用户访问。该页面可以访问 Servlet 请求对象,我们可以使用该对象来显示已登录用户的用户名。
First, we will create our index page. This page will be accessible to users only on successful authentication. This page has access to the Servlet request object using which we can display the user name of the logged in user.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>
Hello World!
</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out"/>
</form>
</body>
<html>
接下来,我们将创建我们的登录视图。该视图显示我们的自定义登录表单,其中带有用户名和密码字段。如果注销或身份验证失败,此视图也将呈现,并且会针对每个案例显示适当的消息。
Next, we create our login view. This display our custom login form with the username and password fields. This view will also be rendered in case of a logout or failed authentication and will display appropriate messages for each case.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<div th:if="${param.error}">
<p th:text="${session.error}" th:unless="${session == null}">[...]</p>
</div>
<div th:if="${param.logout}">You have been logged out.</div>
<form th:action="@{/login}" method="post>
<div>
<label> User Name : <input type="text" name="username" /> </label>
</div>
<div>
<label> Password: <input type="password" name="password" /> </label>
</div>
<div>
<input type="submit" value="Sign In" /> </div>
</form>
</body>
</html>
现在,我们将创建所需的视图,即注册视图。此视图将允许用户使用该应用程序注册自己。该用户信息将存储在数据库中,然后用于验证。
Moving, we create our required view, the register view. This view will let users register themselves with the application. This user data will be stored in the database which will then be used for authentication.
<!DOCTYPE html>
<html>
<head>
<meta charset="ISO-8859-1">
<title>Insert title here</title>
</head>
<body>
<form action="/register" method="post">
<div class="container">
<h1>Register</h1>
<p>Please fill in this form to create an account.</p>
<hr>
<label for="username">
<b>Username</b>
</label>
<input type="text" placeholder="Enter Username" name="username" id="username" required>
<label for="password"><b>Password</b></label>
<input type="password" placeholder="Enter Password" name="password" id="password" required>
<button type="submit" class="registerbtn">Register</button>
</div>
</form>
</body>
</html>
Final Project Structure
我们的最终项目结构应与此类似。
Our final project structure should look something similar to this.
Running the Application
Running the Application
然后,我们可以在 SpringBootApp 中将该应用程序作为 SpringBootApp 运行。当我们在浏览器中转到 localhost:8080 时,它会将我们重定向回登录页面。
We can then run the application as a SpringBootApp. When we go localhost:8080 on our browser it will redirect us back to the login page.
在验证成功后,它将带我们进入带有问候语的索引视图。
On successful authentication it will take us the index view with a greeting.
由于我们只允许在帐户被锁定之前尝试三次失败,因此在第三次验证失败后,用户将被锁定,并且消息将显示在屏幕上。
Since, we have allowed only three failed attempts before the account gets locked, so on the third failed authentication the user get locked and the message is displayed on the screen.
在进入 /register 端点后,我们还可以注册新用户。
On hitting the /register endpoint we can also register a new user.
Conclusion
Conclusion
从今天的文章中,我们学习了如何使用基于注释的配置使用数据库为登录使用自定义表单。我们还学习了如何防止多次登录尝试失败。在进行此操作时,我们已经了解了如何实现我们自己的 AuthenticationProvider 和 UserDetailsService 以使用我们自定义验证过程验证用户。
From today’s article, we have learned how to use a custom form for login using a database using an annotation-based configuration. We have also learned how to prevent multiple failed login attempts. While doing we have seen how we can implement our own AuthenticationProvider and UserDetailsService to authenticate users using our custom authentication process.
Spring Security - Form Login, Remember Me and Logout
Introduction and Overview
Spring Security 带有许多内置功能和工具,方便我们使用。在此示例中,我们将讨论其中三个有趣且有用的功能 −
Spring Security comes with a ton of built-in features and tools for our convenience. In this example, we are going to discuss three of those interesting and useful features −
-
Form-login
-
Remember Me
-
Logout
Form Login
基于表单的登录是 Spring Security 提供支持的用户名/密码验证的一种形式。这是通过 HTML 表单提供的。
Form-based login is one form of Username/password authentication that Spring Security provides support for. This is provided through an Html form.
每当用户请求受保护的资源时,Spring Security 都会检查该请求的验证。如果请求未经验证/授权,用户将被重定向到登录页面。登录页面必须以某种方式由应用程序呈现。Spring Security 默认情况下将提供该登录表单。
Whenever a user requests a protected resource, Spring Security checks for the authentication of the request. If the request is not authenticated/authorized, the user will be redirected to the login page. The login page must be somehow rendered by the application. Spring Security provides that login form by default.
此外,如果需要任何其他配置,必须明确提供,如下所示:
Moreover, any other configuration, if needed, must be explicitly provided as given below −
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.formLogin(
form -> form .loginPage("/login")
.permitAll()
);
}
这段代码要求在模板文件夹中存在一个 login.html 文件,该文件在进入 /login 时将被返回。此 HTML 文件应包含一个登录表单。此外,请求应为指向 /login 的 POST 请求。用户名和密码的参数名称分别为“username”和“password”。除此之外,还需在表单中包含 CSRF 令牌。
This code requires a login.html file to be present in the templates folder which would be returned on hitting the /login. This HTML file should contain a login form. Furthermore, the request should be a post request to /login. The parameter names should be “username” and “password” for username and password respectively. In addition to this, a CSRF Token also needs to be included with the form.
一旦我们完成练习代码,上面的代码片段将更加清晰。
The above code snippet will be clearer once we are done with code exercise.
Remember Me
此类验证需要向浏览器发送 Remember-me cookie。此 cookie 会存储用户的信息/验证负责人,并且存储在浏览器中。因此,当会话开始时,网站可以记住用户身份。Spring Security 为此操作提供了必要的实现。一个使用哈希来保护基于 cookie 的令牌的安全性,而另一个使用数据库或其他持久存储机制来存储生成的令牌。
This type of authentication requires a remember-me cookie to be sent to the browser. This cookie stores user information/authentication principal and it is stored in the browser. So, the website can remember the identity of the user next time when the session is started. Spring Security has the necessary implementations in place for this operation. One uses hashing to preserve the security of cookie-based tokens while the other uses a database or other persistent storage mechanism to store the generated tokens.
Logout
默认的 URL /logout 通过下列方式将用户注销 −
The default URL /logout logs the user out by−
-
Invalidating the HTTP Session
-
Cleaning up any RememberMe authentication that was configured
-
Clearing the SecurityContextHolder
-
Redirect to /login?logout
WebSecurityConfigurerAdapter 自动将注销功能应用到 Spring Boot 应用程序。
WebSecurityConfigurerAdapter automatically applies logout capabilities to the Spring Boot application.
Getting Started (Practical Guide) 和往常一样,我们先访问 start.spring.io。在这里我们选择一个 maven 项目。我们将项目命名为“formlogin”并选择所需的 Java 版本。针对此示例我选择了 Java 8。我们还继续添加以下依赖关系:
Getting Started (Practical Guide) As usual, we shall start by going to start.spring.io. Here we choose a maven project. We name the project “formlogin” and choose the desired Java version. I am choosing Java 8 for this example. We also go on to add the following dependencies −
-
Spring Web
-
Spring Security
-
Spring Boot DevTools
Thymeleaf 是 Java 的模板引擎。它使我们能够快速开发用于在浏览器中渲染的静态或动态网页。它已得到极大的扩展,它允许我们定义和定制精细处理的模板。除此之外,我们可以通过点击 link 来了解更多有关 Thymeleaf 的信息。
Thymeleaf is a templating engine for Java. It allows us to quickly develop static or dynamic web pages for rendering in the browser. It is extremely extensible and allows us to define and customize the processing of our templates in fine detail. In addition to this, we can learn more about Thymeleaf by clicking this link.
让我们开始生成并下载项目。然后我们将它解压到我们选择的文件夹中并使用任何 IDE 来打开它。我将使用 Spring Tools Suite 4 。它可以从 https://spring.io/tools 网站免费下载,且已针对 spring 应用进行优化。
Let’s move on to generate our project and download it. We then extract it to a folder of our choice and use any IDE to open it. I shall be using Spring Tools Suite 4. It is available for free downloading from the https://spring.io/tools website and is optimized for spring applications.
让我们看一看我们的 pom.xml 文件。它应该与以下内容类似 −
Let’s take a look at our pom.xml file. It should look something similar to this −
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath />
<!-- lookup parent from repository -->
</parent>
<groupId> com.spring.security</groupId>
<artifactId>formlogin</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>formlogin</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
让我们在默认包下的 /src/main/java 文件夹中创建一个包。我们将其命名为 config,因为我们将在此处放置所有配置类。所以,名称应类似于此 – com.tutorial.spring.security.formlogin.config。
Let’s create a package in our folder /src/main/java under the default package. We shall be naming it as config as we would place all our configuration classes here. So, the name should look something similar to this – com.tutorial.spring.security.formlogin.config.
The Configuration Class
package com.tutorial.spring.security.formlogin.config;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import com.spring.security.formlogin.AuthFilter;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
protected UserDetailsService userDetailsService() {
UserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
UserDetails user = User.withUsername("abby")
.password(passwordEncoder().encode("12345"))
.authorities("read") .build();
userDetailsManager.createUser(user);
return userDetailsManager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); };
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() .authorizeRequests().anyRequest()
.authenticated() .and()
.formLogin()
.and()
.rememberMe()
.and() .logout() .logoutUrl("/logout")
.logoutSuccessUrl("/login") .deleteCookies("remember-me");
}
}
Code Breakdown
在我们的配置包内部,我们创建了 WebSecurityConfig 类。此类扩展了 Spring Security 的 WebSecurityConfigurerAdapter。我们将在安全配置中使用此类,因此,让我们用 @Configuration 注释对此类进行注释。因此,Spring Security 知道将此类视为配置类。正如我们所看到的,Spring 极大地简化了配置应用程序。
Inside of our config package, we have created the WebSecurityConfig class. This class extends the WebSecurityConfigurerAdapter of Spring Security. We shall be using this class for our security configurations, so let’s annotate it with an @Configuration annotation. As a result, Spring Security knows to treat this class a configuration class. As we can see, configuring applications have been made very easy by Spring.
让我们看一看我们的配置类。
Let’s take a look at our configuration class.
-
First, we shall create a bean of our UserDetailsService class by using the userDetailsService() method. We shall be using this bean for managing our users for this application. Here, to keep things simple, we shall use an InMemoryUserDetailsManager instance to create a user. This user, along with our given username and password, will contain a simple “read” authority.
-
Now, let’s look at our PasswordEncoder. We shall be using a BCryptPasswordEncoder instance for this example. Hence, while creating the user, we used the passwordEncoder to encode our plaintext password like this
.password(passwordEncoder().encode("12345"))
-
After the above steps, we move on to our next configuration. Here, we override the configure method of WebSecurityConfigurerAdapter class. This method takes HttpSecurity as a parameter. We shall be configuring this to use our form login and logout, as well as a remember-me function.
Http安全配置
Http Security Configuration
我们可以观察到所有这些功能均在 Spring Security 中提供。让我们详细研究以下部分 −
We can observe that all these functionalities are available in Spring Security. Let’s study the below section in detail −
http.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.and()
.rememberMe()
.and()
.logout()
.logoutUrl("/logout") .logoutSuccessUrl("/login") .deleteCookies("remember-me");
此处有一些需要说明的点 −
There are a few points to note here −
-
We have disabled csrf or Cross-Site Request Forgery protection As this is a simple application only for demonstration purposes, we can safely disable this for now.
-
Then we add configuration which requires all requests to be authenticated. As we shall see later, we will have a single “/” endpoint for the index page of this application, for simplicity.
-
After that, we shall be using the formLogin() functionality of Spring Security as mentioned above. This generates a simple login page.
-
Then, we use the rememberMe() functionality of Spring Security. This will perform two things.
-
And lastly, we have the logout() functionality. For this too, a default functionality has been provided by Spring security. Here it performs two important functions −
The Protected Content (Optional)
现在,我们将创建一个虚拟的索引页面,供用户登录后查看。它还将包含一个“注销”按钮。
We shall now create a dummy index page now for the user to view when he logs in. It will also contain a logout button.
在`./src/main/resources/templates`中,我们添加index.html 文件。然后添加一些 Html 内容。
In our /src/main/resources/templates, we add a index.html file.Then add some Html content to it.
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
<title>Hello, world!</title>
</head>
<body>
<h1>Hello, world!</h1> <a href="logout">logout</a>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"
integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
</body>
</html>
This content is from Bootstrap 4 getting started template.
我们还向文件添加
We also add
<a href="logout">logout</a>
以便用户可以使用此链接注销应用程序。
to our file, so as the user can log out of the application using this link.
资源控制器
The Resource Controller
我们创建了受保护的资源,现在添加控制器来提供此资源。
We have created the protected resource, we now add the controller to serve this resource.
package com.tutorial.spring.security.formlogin.controllers;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller public class AuthController {
@GetMapping("/") public String home() { return "index"; }
}
如我们所见,这是一个非常简单的控制器。它只有一个 get 端点,在启动应用程序时提供我们的 index.html 文件。
As we can see, it is a very simple controller. It only has a get endpoint which serves our index.html file when the start our application.
Running the application
让我们作为 Spring Boot 应用程序运行该应用程序。当应用程序启动时,我们可以在浏览器中转到 http://localhost:8080 。它应该要求我们输入用户名和密码。此外,我们还应该能够看到记住我复选框。
Let’s run the application as a Spring Boot Application. We can go to http://localhost:8080 on our browser when the application starts. It should ask us for username and password. Additionally, we shall also be able to see the remember-me checkbox.
Login Page
现在,如果我们提供我们在 WebSecurity 配置文件中配置的用户信息,我们将能够登录。此外,如果我们勾选记住我复选框,我们将在浏览器的开发者工具部分看到记住我 cookie。
Now, if we provide the user information as we had configured in our WebSecurity config file, we shall be able to log in. Also, if we tick the remember-me checkbox, we shall be able to see the remember-me cookie in our browser’s developer tools section.
如我们所见,cookie 与我们的登录请求一起发送。
As we can see the cookie is sent along with our login request.
此外,网页中还包含一个用于注销的链接。在单击该链接后,我们将注销我们的应用程序并返回到我们的登录页面。
Also, included in the web page is a link for log out. On clicking the link, we shall be logged out of our application and sent back to our login page.
Spring Security - Taglib
Introduction and Overview
在 JSP 中使用 Spring MVC 应用程序时,我们可以使用 Spring Security 标签来应用安全约束以及访问安全信息。Spring Security 标签库为这样的操作提供了基本支持。使用这样的标签,我们可以根据用户的角色或权限控制向用户显示的信息。此外,我们可以在我们的表单中包含 CSRF 保护功能。
In Spring MVC applications using JSP, we can use the Spring Security tags for applying security constraints as well as for accessing security information. Spring Security Tag library provides basic support for such operations. Using such tags, we can control the information displayed to the user based on his roles or permissions. Also, we can include CSRF protection features in our forms.
为了使用 Spring 安全标签,我们必须在 JSP 文件中声明安全标记库。
To use Spring security tags, we must have the security taglib declared in our JSP file.
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
现在,我们可以使用带有“sec”前缀的 Spring 安全标签。现在让我们了解一下标签的使用。
Now, we can use Spring Security tags with the “sec” prefix. Let’s now see the usage of the tags.
The authorize Tag
The authorize Tag
我们将讨论的第一个标签是 authorize 标签。让我们看一下一些使用示例。
The first tag we will be discussing is the authorize tag. Let’s check out some usage examples.
<sec:authorize access="!isAuthenticated()"> Login </sec:authorize>
<sec:authorize access="isAuthenticated()"> Logout </sec:authorize>
<sec:authorize access="hasRole('ADMIN')"> Hello Admin. </sec:authorize>
如我们所见,我们可以使用此标签根据访问或角色来隐藏或显示部分信息。为了评估角色或访问,我们还可以使用以下 Spring Security Expressions −
As we can see, we can use this tag to hide or show sections of information based on access or roles. To evaluate roles or access we also use the following Spring Security Expressions −
-
hasRole(“ADMIN”) − evaluates to true if the current user has the admin role.
-
hasAnyRole(‘ADMIN’,’USER’) − evaluates to true if the current user has any of the listed roles
-
isAnonymous() − evaluates to true if the current user is an anonymous user
-
isRememberMe() − evaluates to true if the current user is a remember-me user
-
isFullyAuthenticated() − evaluates to true if the user is authenticated and is neither anonymous nor a remember-me user
如我们所见,access 属性是指定 web 安全表达式的。然后,Spring Security 会评估表达式。评估通常委托给在应用程序上下文中定义的 SecurityExpressionHandler<FilterInvocation>。如果返回 true,则用户可以访问该部分中给出的信息。
As we can see, the access attribute is where the web-security expression is specified. Then, Spring Security evaluates the expression The evaluation is generally delegated to SecurityExpressionHandler<FilterInvocation>, which is defined in the application context. If it returns true, then the user can get access to the information given in that section.
如果我们使用 Spring Security 的 Permission Evaluator 将 authorize 标签与 Spring Security 权限评估器一起使用,我们还可以检查用户权限,如下所示 −
If we use the authorize tag with Spring Security ‘s Permission Evaluator, we can also check user permissions as given below −
<sec:authorize access="hasPermission(#domain,'read') or hasPermission(#domain,'write')">
This content is visible to users who have read or write permission.
</sec:authorize>
我们还可以允许或限制用户点击内容中的特定链接。
We can also allow or restrict the user from clicking on certain links within our content.
<sec:authorize url="/admin">
This content will only be visible to users who are authorized to send requests to the "/admin" URL.
</sec:authorize>
The authentication tag
The authentication tag
当我们需要访问存储在 Spring Security 上下文中的当前身份验证对象时,我们可以使用 authentication 标记。然后,我们可以使用它来直接在我们的 JSP 页面中呈现对象的属性。例如,如果我们想要在页面中呈现 Authentication 对象的 principal 属性,我们可以按照如下所示进行操作 −
When we want access to the current Authentication object stored in the Spring Security Context, we can use the authentication tag. Then we can use it to render properties of the object directly in our JSP page. For example, if we want to render the principal property of the Authentication object in our page, we can do it as follows −
<sec:authentication property="principal.username" />
The csrfInput Tag
The csrfInput Tag
当启用 CSRF 保护时,我们可以使用 csrfInput 标记插入带 CSRF 保护令牌正确值的隐藏表单字段。如果未启用 CSRF 保护,此标记不输出任何内容。
We can use the csrfInput tag to insert a hidden form field with the correct values for the CSRF protection token when CSRF protection is enabled. If CSRF protection is not enabled, this tag outputs nothing.
我们可以将此标记放置在 HTML <form></form> 块中,以及其他输入字段。但是,我们不能将此标记放置在 <form:form></form:form> 块中,因为 Spring Security 会自动在这些标记中插入 CSRF 表单字段,还会自动处理 Spring 表单。
We can place the tag within the HTML <form></form> block along with other input fields. However, we must not place the tag within the <form:form></form:form> block as Spring Security automatically inserts a CSRF form field within those tags and also takes care of Spring forms automatically.
<form method="post" action="/do/something">
<sec:csrfInput />
Username:<br />
<input type="text" username="username" />
...
</form>
The csrfMetaTags Tag
The csrfMetaTags Tag
我们可以使用此标记插入包含 CSRF 保护标记表单字段和标题名称及 CSRF 保护标记值的元标记。这些元标记可用于在应用程序的 Javascript 中使用 CSRF 保护。但是,此标记仅在我们已在应用程序中启用了 CSRF 保护时才起作用,否则此标记不输出任何内容。
We can use this tag to insert meta tags which contain the CSRF protection token form field and header names and CSRF protection token value. These meta tags can be useful for employing CSRF protection within Javascript in our application. However, this tag only works when we have enabled CSRF protection in our application, otherwise, this tag outputs nothing.
<html>
<head>
<title>CSRF Protection in Javascript</title>
<sec:csrfMetaTags />
<script type="text/javascript" language="javascript">
var csrfParam = $("meta[name='_csrf_param']").attr("content");
var csrfToken = $("meta[name='_csrf']").attr("content");
</script>
</head>
<body>
...
</body>
</html>
Getting Started (Practical Guide)
Getting Started (Practical Guide)
现在我们已经讨论了标记,让我们构建一个应用程序来演示如何使用标记。我们将使用 Spring Tool Suite 4 作为我们的 IDE。此外,我们将使用 Apache Tomcat 服务器来服务我们的应用程序。所以,让我们开始吧。
Now that we have discussed the tags, let’s build an application to demonstrate the usage of the tags. We shall be using Spring Tool Suite 4 as our IDE. Additionally, we shall be using the Apache Tomcat server to serve our application. So, let’s get started.
Setting up the Application
Setting up the Application
让我们在 STS 中创建一个简单的 Maven 项目。我们可以将我们的应用程序命名为 taglibsdemo,并将其打包为 .war 文件。
Let’s create a simple Maven Project in STS. We can name our application as taglibsdemo, and package it as a .war file.
完成应用程序的设置后,它应该具有类似于此的结构。
When we have finished setting up our application it should have a structure similar to this.
The pom.xml file
The pom.xml file
我们将向我们的应用程序添加以下依赖关系 −
We shall add these following dependencies to our application −
-
Spring Web MVC
-
Spring-Security-Web
-
Spring-Security-Core
-
Spring-Security-Taglibs
-
Spring-Security-Config
-
Javax Servlet Api
-
JSTL
添加这些依赖关系后,我们的 pom.xml 应类似于此 −
After adding these dependencies, our pom.xml should look similar to this −
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.tutorial.spring.security</groupId>
<artifactId>taglibsdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>5.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency> <dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
</project>
让我们为此应用程序创建我们的基本包。我们可以将其命名为 com.taglibsdemo。在包中,让我们为我们的配置文件创建另一个包。由于它将保存配置文件,我们可以将其命名为 config。
Let’s create our base package for the application. We can name it com.taglibsdemo. Within the package, let’s create another package for our configuration files. Since, it will be holding the configuration files, we can name it config.
ApplicationConfig.java
ApplicationConfig.java
让我们创建我们的第一个配置文件类 ApplicationConfig.java。
Let’s create our first configuration class ApplicationConfig.java.
package com.taglibsdemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
@EnableWebMvc
@Configuration @ComponentScan({ "com.taglibsdemo.controller"} )
public class ApplicationConfig {
@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver
viewResolver = new InternalResourceViewResolver();
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/views/");
viewResolver.setSuffix(".jsp"); return viewResolver;
}
}
我们在此分解代码 −
Let’s break down the code here −
-
@EnableWebMvc − We use @EnableWebMvc to enable Spring MVC. So, we add this annotation to an @Configuration class to import the Spring MVC configuration from WebMvcConfigurationSupport. WebMvcConfigurationSupport is the main class that provides the configuration for the MVC Java config. Not using this annotation may result in things like content-type and accept header, generally content negotiation not working. @EnableWebMvc registers a RequestMappingHandlerMapping, a RequestMappingHandlerAdapter, and an ExceptionHandlerExceptionResolver among others in support of processing requests with annotated controller methods using annotations such as @RequestMapping , @ExceptionHandler, and others.
-
@ComponentScan − @ComponentScan annotation is used to tell Spring the packages to scan for annotated components. @ComponentScan also used to specify base packages and base package classes using thebasePackageClasses or basePackages attributes of @ComponentScan.
-
InternalResourceViewResolver − To resolve the provided URI to the actual URI in the format prefix + viewname + suffix.
-
setViewClass() − To set the view class that should be used to create views.
-
setPrefix() − To set the prefix that gets prepended to view names when building a URL.
-
setSuffix() − To set the suffix that gets appended to view names when building a URL.
WebSecurityConfig.java
WebSecurityConfig.java
接下来,我们创建 WebSecurityConfig 类,它将扩展 Spring Security 的熟悉的 WebSecurityConfigurerAdapter 类。
Next we shall create our WebSecurityConfig class which will extend the familiar WebSecurityConfigurerAdapter class of Spring Security.
package com.taglibsdemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.User.UserBuilder;
@EnableWebSecurity @ComponentScan("com.taglibsdemo")
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@SuppressWarnings("deprecation") @Bean
public UserDetailsService userdetailsService() {
UserBuilder users = User.withDefaultPasswordEncoder();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(users.username("rony").password("rony123").roles("USER").build());
manager.createUser(users.username("admin").password("admin123").roles("ADMIN").build());
return manager;
}
@Override protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() .antMatchers("/index", "/").permitAll()
.antMatchers("/admin", "/user").authenticated() .and() .formLogin()
.and() .logout() .logoutRequestMatcher(
new AntPathRequestMatcher("/logout")
);
}
}
我们在此分解代码 −
Let’s break the code down here −
-
WebSecurityConfigurerAdapter − The abstract class that implements WebSecurityConfigurer WebSecurityConfigurer and allows us to override methods for security configuration.
-
@EnableWebSecurity − It enables Spring to automatically find and apply the @Configuration class to the global WebSecurity.
-
We then create a UserDetailsService Bean using the method to create users using the InMemoryUserDetailsManager instance. We create two users – one with role “USER” and another with role “ADMIN” and add them to Spring Security.
-
After that, we override the configure method with HttpSecurity as a parameter. We make our home page or index page accessible to all and admin page to be accessible when the user is authenticated. Next, we add Spring Security form login and logout.
因此,通过这些步骤,我们的安全配置就完成了。现在,我们准备进入下一步。
So, with those steps our security configuration is complete. Now, we are ready to move on to the next step.
SpringSecurityApplicationInitializer.java
SpringSecurityApplicationInitializer.java
继续,现在,我们创建 SpringSecurityApplicationInitializer.java 类,该类扩展 Spring Security 的 AbstractSecurityWebApplicationInitializer 类。
Moving on, now we shall create the SpringSecurityApplicationInitializer.java class which extends the AbstractSecurityWebApplicationInitializer class of Spring Security.
package com.taglibsdemo.config;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
public class SpringSecurityApplicationInitializer extends
AbstractSecurityWebApplicationInitializer { }
AbstractSecurityWebApplicationInitializer 是实现 Spring 的 WebApplicationInitializer 的一个抽象类。因此,如果 classpath 包含 spring-web 模块,SpringServletContainerInitializer 将初始化该类的具体实现。
AbstractSecurityWebApplicationInitializer is an abstract class that implements Spring’s WebApplicationInitializer. So, SpringServletContainerInitializer will initialize the concrete implementations of this class if the classpath contains spring-web module.
MvcWebApplicationInitializer.java
MvcWebApplicationInitializer.java
package com.taglibsdemo.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class MvcWebApplicationInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
@Override protected Class</?>[] getRootConfigClasses() {
return new Class[] {WebSecurityConfig.class};
}
@Override protected Class</?>[] getServletConfigClasses() {
return null;
}
@Override protected String[] getServletMappings() {
return new String[] {"/"};
}
}
-
AbstractAnnotationConfigDispatcherServletInitializer − This class extends WebApplicationInitializer. We need this class as a base class for initializing a Spring application in Servlet container environment.As a result, the subclass of AbstractAnnotationConfigDispatcherServletInitializer will provide the classes annotated with @Configuration, Servlet config classes and DispatcherServlet mapping pattern.
-
getRootConfigClasses() − This method must be implemented by the class extending AbstractAnnotationConfigDispatcherServletInitializer. It provides “root” application context configuration.
-
getServletConfigClasses() − This method too, must be implemented to provide DispatcherServlet application context configuration.
-
getServletMappings() − This method is used specify the servlet mapping(s) for the DispatcherServlet.
我们已经设置了配置类。现在,我们将创建我们的控制器来服务 JSP 页面。
We have set up the configuration classes. Now , we shall create our controller to serve the JSP pages.
HelloController.java
HelloController.java
package com.taglibsdemo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller public class HelloController {
@GetMapping("/")
public String index() { return "index"; }
@GetMapping("/user")
public String user() { return "admin"; }
@GetMapping("/admin")
public String admin() { return "admin"; }
}
这里,我们创建了三个端点 - “/”,“/ user ”和 “/ admin ”。正如之前在我们的配置中指定的,我们将允许对索引页面
Here, we have created three endpoints – “/”, “/user”, and “/admin”. As specified in our configuration previously, we will allow unauthorized access to the index page
“/”进行未授权的访问。另一方面,“/user”和 “/admin”端点只能进行授权访问。
“/”. On the other hand, the “/user” and “/admin” endpoints would be authorized only access.
Secure Content to serve
Secure Content to serve
继续前进,我们现在将创建 JSP 页面,该页面将在击中特定端点时提供服务。
Moving on, we shall now create the JSP pages which are to be served on hitting the specific endpoints.
为此,在我们 src/main 文件夹内创建一个名为 webapp 的文件夹。在这个文件夹中,我们创建我们的 WEB-INF 文件夹,然后和 ApplicationConfig.java 类中一样,我们添加 views 文件夹。在这里,在这个文件夹中,我们将添加视图。
For this, inside our src/main folder we create a folder called webapp. Inside this folder, we create our WEB-INF folder and further as in ApplicationConfig.java class we add the views folder. Here, in this folder we shall be adding the views.
让我们先添加我们的主页,即 index.jsp。
Let’s add our home page, i.e., index.jsp first.
<%@ page language="java" contentType="text/html;
charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="ISO-8859-1">
<title>Home Page</title>
</head>
<body>
<a href="user">User</a>
<a href="admin">Admin</a>
<br>
<br> Welcome to the Application!
</body>
</html>
然后,我们来创建一个 admin.jsp 文件。我们来把它加进去。
Then we shall create our admin.jsp file. Let’s add it.
<%@ page language="java" contentType="text/html;
charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="security"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="ISO-8859-1">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body> Welcome to Admin Page! <a href="logout"> Logout </a>
<br>
<br>
<security:authorize access="hasRole('ADMIN')"> Hello Admin!
</security:authorize>
</body>
</html>
这里,我们添加了 <%@ taglib uri="http://www.springframework.org/security/tags" prefix="security"%>。正如之前讨论过的,这将让我们可以使用 Spring 安全标记库。正如我们看到的,我们在内容周围添加了 “authorize” 标记。我们的管理员只能访问此内容。任何访问此页面的其他用户都将无法查看此内容。
here, we have added <%@ taglib uri="http://www.springframework.org/security/tags" prefix="security"%>. This is going to let us the Spring security tag libs as discussed before. As we can see, we have the added the “authorize” tag around the content. This content is will be only accessible by our admin. Any other user accessing this page will not be able to view this content.
Running the application
Running the application
我们现在右键单击项目并选择“在服务器上运行”。当服务器启动并且我们的应用程序正在运行时,我们可以在浏览器中访问 localhost:8080/taglibsdemo/ 来查看页面。
We now right click on the project and choose Run On Server. When the server starts and our application is running we can go to localhost:8080/taglibsdemo/ on our browser to view the page.
Login page
Login page
现在,如果我们单击应用程序中的用户链接,系统将要求我们登录。
Now, if we click on the User link in our application, we shall be asked to log in.
这里,正如我们在控制器中看到的,我们为用户和管理员链接提供服务。但是,如果我们的用户不是管理员,他将无法查看受我们的 “authorize” 标记保护的内容。
Here, as we can see in our controller, we are serving the admin page for bothe the user and admin links. But our user, if he is not an admin cannot view the content which is protected by our “authorize”tag.
首先以用户身份登录。
Let’s log in as the user first.
可见,“Hello Admin!” 内容对我们不可见。这是因为当前用户没有管理员角色。
We can see that the “Hello Admin!” content is not visible to us. This is because the current user doesn’t have the admin role.
现在,注销并以管理员身份登录。
Let’s logout and log in as admin now.
现在,我们能够看到受保护的内容“Hello Admin!”,因为当前用户拥有管理员角色。
We are now able to see the protected content “Hello Admin!” as the current user has the admin role.
Conclusion
Conclusion
我们学习了如何使用 Spring Security 标记库来保护我们的内容并在 Spring Security 上下文中访问当前的 Authentication 对象。
We have learnt how we can use the Spring Security tag library to protect our content and get access to the current Authentication object in Our Spring Security Context.
Spring Security - XML Configuration
Fundamentals
在本部分中,我们将讨论如何使用 XML 配置配置 Spring Security。我们将开发一个带有 Spring Security 的简单 Spring 应用程序。在此过程中,我们将详细讨论我们正在使用的各个组件。
In this we are going to discuss how to configure Spring Security with XML configuration. We shall be developing a simple Spring application with Spring Security. While doing so, we will discuss in details about each component that we are using.
Authentication and Authorization
-
Authentication − Authentication is ensuring the user or the client is who they claim to be. There are many ways in which Spring Security enables us to perform authentication. Spring Security supports Basic Authentication, LDAP authentication, JDBC authentication, etc.
-
Authorization − Ensuring if the user has permission for the action. If our application is a complex one, with different kinds of users such as admins, regular users, other less privileged users, we need to maintain access control in our application. For example, a guest user should not be able to access admin content. So, to control access to various resources within our application, we need to check if a user has permission to access that resource.
以上主题是 Spring Security 的两个主要组件。Spring Security 为我们提供了实现应用程序中的身份验证和授权的各种内置功能。我们可以根据需要更改这些功能,以便非常快速地保护应用程序。此外,Spring Security 还允许对之前提到的功能进行大量自定义,以实现我们自己复杂的身份验证和授权。
The above topics are the two main components of Spring Security. Spring security provided us with various in-built features to implement authentication and authorization in our application. We can use these features with our changes to secure an application very quickly. In addition to this, Spring Security also allows plenty of customizations to the features mentioned before to implement our own complex authentications and authorizations.
Getting Started (Practical Guide)
让我们看一个使用内置 Spring Security 功能的基本示例。在此示例中,我们将使用 Spring Security 开箱即用的选项保护我们的应用程序。这将让我们大致了解 Spring Security 的各种组件以及如何将它们用于我们的应用程序。我们将使用 XML 来配置我们应用程序的安全功能。
Let’s look at a basic example using in-built Spring Security features. In this example, we shall be securing our application with options provided out-of-box by Spring security. This is will give us an idea of the various components of Spring Security and how we can use them for our application. We shall be using XML to configure our application’s Security features.
我们将在应用程序中使用的工具是 Spring Tool Suite 4 和 Apache Tomcat Server 9.0 。它们都可免费下载和使用。
The tools we shall be using for our application will be Spring Tool Suite 4 and Apache Tomcat Server 9.0. They are both available for free download and use.
首先,在 STS 中启动一个新的简单 Maven 项目。我们可以根据我们的选择来选择组 ID 和项目 ID。之后,单击完成。因此,我们将项目添加到我们的工作区。让我们留出一些时间让 STS 来构建和验证我们的项目。
First, let’s start a new simple Maven Project in STS. We can choose group id, artifact id as per our choice. After that, we click on Finish. As a result, we have added our project to our workspace. Let’s give STS some time to build and validate our project.
我们的项目结构最终会类似于此。
Our project structure would finally look similar to this.
接下来,添加依赖项。我们将选择以下依赖项。
Next, let’s add the dependencies. We are going to choose the following dependencies.
-
Spring Web MVC
-
Spring-Security-Web
-
Spring-Security-Core
-
Spring-Security-Config
-
Javax Servlet API
pom.xml
pom.xml
添加这些依赖项后,我们就可以配置项目了。快来看看我们的 pom.xml 文件。
With these dependencies added, we are ready to configure our project. Let’s take a look at our pom.xml file.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd>
<modelVersion>4.0.0</modelVersion>
<groupId>com.tutorial.spring.security</groupId>
<artifactId>xmlconfigurationdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>Spring Security with XML configuration</name> <description>Spring Security with XML configuration</description>
<properties>
<maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.source>1.8</maven.compiler.source> </properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.2.RELEASE<version>
</dependency> <dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
</project>
Controller and views
Controller and views
首先,我们将创建我们的控制器。因此,创建一个名为 controller 的包,并将我们的 HomeController 类添加到包中。
First, We are going to create our controller. So, let’s create a package called controller and add our HomeController class to the package.
package com.tutorial.spring.security.xmlconfigurationdemo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller public class HomeController { @GetMapping("/")
public String index() { return "index"; }
@GetMapping("/admin")
public String admin() { return "admin"; } }
此处,我们有两个端点——“index”和“admin”。虽然所有人均可访问索引页,但我们将保护我们的“admin”页。
Here, we have two endpoints – “index” and “admin”. While the index page is will be accessible to all, we will protect our “admin” page.
由于我们已经创建了路由,那么也来添加页面吧。
Since, we have created the routes, let’s also add the pages.
在我们的 /src/main/webapp 文件夹中,让我们创建一个名为 WEB-INF 的文件夹。然后,在该文件夹内部,我们将创建一个名为 views 的文件夹,在此处我们将创建我们的视图。
In our /src/main/webapp folder, let’s create a folder called WEB-INF. Then inside it, we will create a folder called views where we will create our views.
让我们创建我们的第一个视图 -
Let’s create our first view−
<%@ page language="java" contentType="text/html;
charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="ISO-8859-1"> <title>Insert title here</title>
</head>
<body>
<h2>Welcome to Spring Security!</h2>
</body>
</html>
然后,我们创建我们的管理视图。
Then we create our admin view.
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<DOCTYPE html>
<html>
<head>
<meta charset="ISO-8859-1"> <title>Insert title here</title>
</head>
<body>
Hello Admin
</body>
</html>
接下来,让我们配置我们的应用程序。
Moving on, let’s configure our application.
配置。
Configurations.
web.xml
现在,让我们添加我们第一个 xml 文件 - web.xml 文件。
Now, let’s add our first xml file – the web.xml file.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xml>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app-config.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value> /WEB-INF/security-config.xml </param-value>
</context-param>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
代码分解
Code breakdown
-
Dispatcher Servlet − The first servlet we have declared here is the Dispatcher servlet. The dispatcher servlet is the entry point of any Spring MVC application and is at the core of the entire Spring MVC framework design. It intercepts all HTTP requests and dispatches them to registered handlers for processing a web request. It also provides convenient mapping and exception handling facilities. The order in which servlets are loaded depends on “load-on-startup” value. Servlets with a lower value of “load-on-startup” are loaded before the ones with a higher value.
-
contextConfigLocation − It is a string that indicates where context(s) can be found. This string represents a path to a file where our configurations can be loaded.
-
servlet-mapping − We use Servlet Mapping to tell Spring Container which request to route to which servlet. In our case, we are routing all our requests to our “spring” Dispatcher servlet.
-
listener − The classes that listen to certain types of events, and trigger an appropriate functionality when that event occurs. Each listener is bound to an event. In our case, we will create a root web-application context for the web-application with the ContextLoaderListener. This is then put in the ServletContext that can be used to load and unload the spring-managed beans.
-
filter − Spring uses Filters to process requests before handing them over to the Dispatcher Servlet and also used to process responses after they are dispatched. The DelegatingFilterProxy links the application context to the web.xml file. The requests that are coming to this application will pass through our filter which we named “spring SecurityFilterChain” before they reach their controllers. This is where Spring Security can take over the request and perform operations on it before passing it on to the next set of filters or handlers.
security-config.xml
接下来,我们将创建我们的 security-config.xml 文件。
Next we will create our security-config.xml file.
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<http auto-config="true">
<intercept-url pattern="/admin"
access="hasRole('ROLE_ADMIN')" /> </http>
<authentication-manager>
<authentication-provider>
<user-service>
<user name="admin" password="{noop}1234" authorities="ROLE_ADMIN" />
</user-service>
</authentication-provider> </authentication-manager>
<beans:bean id ="passwordEncoder"
class = "org.springframework.security.crypto.password.NoOpPasswordEncoder"
factory-method = "getInstance">
</beans:bean>
</beans:beans>
代码分解
Code breakdown
-
http element − The parent of all web-related namespace functionality. Here, we can configure which URLs to intercept, what permissions are required, which type of login to use, and all such configuration.
-
auto-config − Setting this attribute to true automatically sets up form-login, basic login, and logout functionalities. Spring Security generates them by using standard values and the features enabled.
-
intercept-url − It sets the pattern of the URLs that we want to protecte, using the access attribute.
-
access − It specifies which users are permitted to access the URL specified by the pattern attribute. It is done on the basis of the roles and permissions of a user. We can use SPEL with this attribute.
-
authentication-manager − The <authentication-manager> is used to configure users, their passwords, and roles in the application. These users will be one who can access the protected parts of the application given they have the appropriate roles. A DaoAuthenticationProvider bean will be created by the <authentication-provider< and the <user-service< element will create an InMemoryDaoImpl. All authentication-provider elements will allow the users to be authenticated by providing the user information to the authentication-manager.
-
password-encoder − This will register a password encoder bean. To keep things simple here we have used the NoOpPasswordEncoder.
接着,我们创建最后的一个配置文件 - app-config 文件。在这里,我们将添加视图解析器代码并定义我们的基本包。
Moving on we create out last configuration file – the app-config file. Here we are going to add our view resolver code and define our base package.
app-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<mvc:annotation-driven />
<context:component-scan
base-package="com.tutorial.spring.security.xmlconfigurationdemo.controller">
</context:component-scan>
<context:annotation-config>
</context:annotation-config>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
</beans>
在这里,正如我们可以看到的那样,我们正在注册我们之前创建的视图。为此,我们使用 InternalResourceViewResolver 类,它将提供的 URI 映射到实际 URI。
Here, as we can see we are registering our views that we created earlier. For this, we are using the InternalResourceViewResolver class which will map the provided URI to the actual URI.
例如,使用上述配置,如果我们请求 URI “/admin”,DispatcherServlet 将请求转发到
For example, using the above configuration, if we request the URI “/admin“, DispatcherServlet will forward the request to the
prefix + viewname + suffix = /WEB-INF/views/admin.jsp 视图。
prefix + viewname + suffix = /WEB-INF/views/admin.jsp view.
Running the application
Running the application
通过此简单配置,我们的应用已准备就绪。我们可以右键单击该项目并选择在服务器上运行。我们可以选择我们的 Tomcat 服务器。当服务器启动时,我们可以转到 localhost:8080/xmlconfigurationdemo 以与我们的应用交互。
With this simple configuration, we have our application ready to be served. We can right-click on the project and choose Run on Server. We can choose our Tomcat server. When the server starts, we can go to localhost:8080/xmlconfigurationdemo to interact with our application.
如果我们输入正确的凭据,我们将能够登录并看到所需的那些内容。
If, we enter the correct credentials we shall be able to login and see our desired content.
Spring Security - OAuth2
OAuth 2.0 Fundamentals
OAuth 2.0 由IETF OAuth 工作组开发,并于 2012 年 10 月发布。它作为一种开放授权协议,允许第三方应用代表资源所有者获取对 HTTP 服务的有限访问权限。它可以做到这一点,同时不会透露用户的身份或长期凭据。第三方应用本身也可以代表其使用它。OAuth 的工作原理包括将用户身份验证委托给承载用户帐户的服务,并授权第三方应用访问用户的帐户。
OAuth 2.0 was developed by IETF OAuth Working Group and published in October of 2012. It serves as an open authorization protocol for enabling a third party application to get limited access to an HTTP service on behalf of the resource owner. It can do so while not revealing the identity or the long-term credentials of the user. A third-party application itself can also use it on its behalf. The working principle of OAuth consists of the delegation of user authentication to a service hosting the user account and authorizing the third-party application access to the account of the user.
让我们考虑一个例子。假设我们想登录网站 “clientsite.com”。我们可以通过 Facebook、Github、Google 或 Microsoft 登录。我们选择上述给出的任何选项,并将被重定向到相应网站进行登录。如果登录成功,系统将询问我们是否要允许 clientsite.com 访问其请求的特定数据。我们选择我们所需的选项,然后根据我们对第三方资源的操作,用授权代码或错误代码重定向到 clientsite.com,我们的登录是否成功取决于我们的操作。这是 OAuth 2 的基本工作原理。
Let us consider an example. Let us say we want to login to a website “clientsite.com”. We can sign in via Facebook, Github, Google or Microsoft. We select any options of the options given above, and we are redirected to the respective website for login. If login is successful, we are asked if we want to give clientsite.com access to the specific data requested by it. We select our desired option and we are redirected to clientsite.com with an authorization code or error code and our login is successful or not depending on our action in the third-party resource. This is the basic working principle of OAuth 2.
OAuth 系统中有五个关键参与者。我们来列出它们:
There are five key actors involved in an OAuth system. Let’s list them out −
-
User / Resource Owner − The end-user, who is responsible for the authentication and for providing consent to share resources with the client.
-
User-Agent − The browser used by the User.
-
Client − The application requesting an access token.
-
Authorization Server − The server that is used to authenticate the user/client. It issues access tokens and tracks them throughout their lifetime.
-
Resource Server − The API that provides access to the requested resource. It validates the access tokens and provides authorization.
Getting Started
我们将使用 Spring Security 和 OAuth 2.0 开发一个 Spring Boot 应用来说明上述内容。我们现在将使用内存数据库开发一个基本应用来存储用户凭据。该应用将使我们很容易理解 OAuth 2.0 与 Spring Security 的工作原理。
We will be developing a Spring Boot Application with Spring Security and OAuth 2.0 to illustrate the above. We will be developing a basic application with an in-memory database to store user credentials now. The application will make it easy for us to understand the workings of OAuth 2.0 with Spring Security.
让我们使用 Spring Initializer 在 Java 8 中创建一个 maven 项目。我们从访问 start.spring.io 开始。我们使用以下依赖项生成一个应用:
Let’s use the Spring initializer to create a maven project in Java 8. Let’s start by going to start.spring.io. We generate an application with the following dependencies−
-
Spring Web
-
Spring Security
-
Cloud OAuth2
-
Spring Boot Devtools
利用上述配置,我们点击生成按钮来生成一个项目。项目将以一个 zip 文件的形式下载。我们解压它到一个文件夹中。然后我们可以在我们选择的 IDE 中打开项目。这里我使用 Spring Tools Suite,因为它针对 spring 应用进行了优化。我们也可以根据需要使用 Eclipse 或 IntelliJ Idea。
With the above configuration, we click on the Generate button to generate a project. The project will be downloaded in a zip file. We extract the zip to a folder. We can then open the project in an IDE of our choice. I am using Spring Tools Suite here as it is optimized for spring applications. We can also use Eclipse or IntelliJ Idea as we wish.
因此,我们在 STS 中打开项目,让它下载依赖项。然后我们可以在我们的包资源管理器窗口中看到项目结构。它应该与以下屏幕截图相似。
So, we open the project in STS, let the dependencies get downloaded. Then we can see the project structure in our package explorer window. It should resemble the screenshot below.
如果我们打开 pom.xml 文件,我们可以查看依赖项和其他与项目相关的信息。它应该看起来像这样。
If we open the pom.xml file we can view the dependencies and other details related to the project. It should look something like this.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.tutorial</groupId>
<artifactId>spring.security.oauth2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring.security.oauth2</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR6</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot<groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot<groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <exclusions> <exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
<dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement><build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
现在,到我们的应用程序的基础包,即 com.tutorial.spring.security.oauth2 ,让我们添加一个名为 config 的新包,我们将在其中添加我们的配置类。
Now, to the base package of our application, i.e., com.tutorial.spring.security.oauth2, let’s add a new package named config where we shall add our configuration classes.
让我们创建一个扩展 Spring Security 中的 WebSecurityConfigurerAdapter 类以管理客户端应用程序用户的第一个配置类 UserConfig 。我们使用 @Configuration 注解来注释类以告诉 Spring 它是一个配置类。
Let’s create our first configuration class, UserConfig which extends the WebSecurityConfigurerAdapter class of Spring Security to manage the users of the client application. We annotate the class with @Configuration annotation to tell Spring that it is a configuration class.
package com.tutorial.spring.security.oauth2.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
@Configuration public class UserConfig extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService() {
UserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
UserDetails user = User.withUsername("john")
.password("12345") .authorities("read")
.build(); userDetailsManager.createUser(user); return userDetailsManager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
然后,我们添加一个 UserDetailsService 的 bean 来检索用于认证和授权的用户详细信息。为了将其放入 Spring 上下文中,我们使用 @Bean 对其进行注释。为了使本教程简单易懂,我们使用一个 InMemoryUserDetailsManager 实例。对于一个真实世界的应用程序,我们可以使用其他实现,比如 JdbcUserDetailsManager 来连接到数据库等等。为了能够轻松地为本示例创建用户,我们使用扩展 UserDetailsService 的 UserDetailsManager 接口,并且拥有诸如 createUser() 、 updateUser() 等等的方法。然后,使用构建器类创建一个用户。我们给他一个用户名、密码和一个 “read” 权限,暂时如此。然后,使用 createUser() 方法,我们添加新创建的用户并返回 UserDetailsManager 的实例,从而将其放入 Spring 上下文。
We then add a bean of the UserDetailsService to retrieve the user details for authentication and authorization. To put it in the Spring context we annotate it with @Bean. To keep this tutorial simple and easy to understand, we use an InMemoryUserDetailsManager instance. For a real-world application, we can use other implementations like JdbcUserDetailsManager to connect to a database and so on. To be able to create users easily for this example we use the UserDetailsManager interface which extends the UserDetailsService and has methods like createUser(), updateUser() and so on. Then, we create a user using the builder class. We give him a username, password and a “read” authority for now. Then, using the createUser() method, we add the newly created user and return the instance of UserDetailsManager thus putting it in the Spring context.
为了能够使用我们定义的 UserDetailsService,必须在 Spring 上下文中提供一个 PasswordEncoder bean。同样,为了现在保持简单,我们使用 NoOpPasswordEncoder。NoOpPasswordEncoder 应该不要在生产环境的真实世界应用程序中使用,因为它不安全。NoOpPasswordEncoder 不对密码进行编码,它仅在开发或测试场景或概念验证中有用。我们应该一直使用 Spring Security 提供的其他高度安全选项,其中最流行的是 BCryptPasswordEncoder,我们将在我们一系列的教程中稍后使用它。为了将其放入 Spring 上下文中,我们使用 @Bean 对这个方法进行注释。
To be able to use the UserDetailsService defined by us, it is necessary to provide a PasswordEncoder bean in the Spring context. Again, to keep it simple for now we use the NoOpPasswordEncoder. The NoOpPasswordEncoder should not be used otherwise for real-world applications for production as it is not secure. NoOpPasswordEncoder does not encode the password and is only useful for developing or testing scenarios or proof of concepts. We should always use the other highly secure options provided by Spring Security, the most popular of which is the BCryptPasswordEncoder, which we will be using later in our series of tutorials. To put it in the Spring context we annotate the method with @Bean.
然后,我们覆盖 WebSecurityConfigurerAdapter 的 AuthenticationManager bean 方法,它返回 authenticationManagerBean 以将认证管理者放入 Spring 上下文。
We then override the AuthenticationManager bean method of WebSecurityConfigurerAdapter, which returns the authenticationManagerBean to put the authentication manager into the Spring context.
现在,为了添加客户端配置,我们添加一个名为 AuthorizationServerConfig 的新配置类,它扩展 Spring Security 中的 AuthorizationServerConfigurerAdapter 类。 AuthorizationServerConfigurerAdapter * class is used to configure the authorization server using the spring security oauth2 module. We annotate this class with @Configuration as well. To add the authorization server functionality to this class we need to add the *@EnableAuthorizationServer 注解以便应用程序可以作为一个授权服务器表现。
Now, to add the client configurations we add a new configuration class named AuthorizationServerConfig which extends AuthorizationServerConfigurerAdapter class of Spring Security. The AuthorizationServerConfigurerAdapter * class is used to configure the authorization server using the spring security oauth2 module. We annotate this class with @Configuration as well. To add the authorization server functionality to this class we need to add the *@EnableAuthorizationServer annotation so that the application can behave as an authorization server.
package com.tutorial.spring.security.oauth2.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; @Configuration @EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired private AuthenticationManager authenticationManager;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() .withClient("oauthclient1") .secret("oauthsecret1") .scopes("read") .authorizedGrantTypes("password") }
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
}
用于检查 oauth 令牌,Spring Security oauth 暴露两个端点—— /oauth/check_token and /oauth/token_key 。这些端点在缺省情况下受 denyAll() 限制。 tokenKeyAccess() 和 checkTokenAccess() 方法打开这些端点供使用。
For checking oauth tokens, Spring Security oauth exposes two endpoints – /oauth/check_token and /oauth/token_key. These endpoints are protected by default behind denyAll(). tokenKeyAccess() and checkTokenAccess() methods open these endpoints for use.
我们将 UserConfig 类中配置的 AuthenticationManager bean 作为依赖项在这里自动装配,稍后我们将使用它。
We autowire the AuthenticationManager bean we configured in the UserConfig class as a dependency here which we shall be using later.
然后,我们覆盖 AuthorizationServerConfigurerAdapter 的 configure() 方法中的两个以提供客户端详细信息服务的一个内存实现。第一个方法使用 ClientDetailsServiceConfigurer 作为参数,顾名思义,允许我们为授权服务器配置客户端。这些客户端表示将能够使用该授权服务器的功能的应用程序。由于这是一个用于学习 OAuth2 实现的基本应用程序,我们暂时将事情保持简单并使用具有以下属性的内存实现——
We then override two of the configure() methods of the AuthorizationServerConfigurerAdapter to provide an in-memory implementation of the client details service. The first method which uses the ClientDetailsServiceConfigurer as a parameter, as the name suggests, allows us to configure the clients for the authorization server. These clients represent the applications that will be able to use the functionality of this authorization server. Since this is a basic application for learning the implementation of OAuth2, we will keep things simple for now and use an in-memory implementation with the following attributes −
-
clientId − the id of the client. Required.
-
secret − the client secret, required for trusted clients
-
scope − the limiting scope of the client, in other words, client permissions. If left empty or undefined, the client is not limited by any scope.
-
authorizedGrantTypes − the grant types that the client is authorized to use. The grant type denotes the way by which the client obtains the token from the authorization server. We will be using the “password” grant type as it is the simplest. Later, we shall be using another grant type for another use-case.
在 “password” 授权授予类型中,用户需要向我们的客户端应用程序提供他的/她的用户名、密码和范围,它然后使用那些凭证以及它的凭证用于我们想要令牌的授权服务器。
In “password” authorization grant type, the user needs to provide his/her username, password and scope to our client application, which then uses those credentials along with its credentials for the authorization server we want the tokens from.
我们覆盖的另一个 configure() 方法,使用 AuthorizationServerEndpointsConfigurer 作为参数,用于将 AuthenticationManager 附加到授权服务器配置。
The other configure() method that we overrode, uses AuthorizationServerEndpointsConfigurer as a parameter, is used to attach the AuthenticationManager to authorization server configuration.
利用这些基本配置,我们的授权服务器可以使用了。让我们继续启动它并使用它。我们将使用 Postman ( h ttps://www.postman.com/downloads/ ) 来发出我们的请求。
With these basic configurations, our Authorization server is ready to use. Let’s go ahead and start it and use it. We will be using Postman ( https://www.postman.com/downloads/ ) for making our requests.
使用 STS 时,我们能够启动我们的应用程序并开始在我们的控制台中看到日志。当该应用程序启动后,我们可以在控制台中找到我们的应用程序所公开的 oauth2 端点。在所有这些端点中,我们现在将使用以下令牌 -
When using STS, we can launch our application and start seeing see the logs in our console. When the application starts, we can find the oauth2 endpoints exposed by our application in the console. Of those endpoints, we will be using the following the below token for now −
/oauth/token – for obtaining the token.
/oauth/token – for obtaining the token.
如果我们在这里查看 postman 快照,我们可以注意到一些事情。我们列出它们。
If we check the postman snapshot here, we can notice a few things. Let’s list them down below.
-
The URL − Our Spring Boot Application is running at port 8080 of our local machine, so the request is pointed to [role="bare"]http://localhost:8080. The next part is /oauth/token, which we know, is the endpoint exposed by OAuth for generating the token.
-
The query params− Since this is a “password” authorization grant type, the user needs to provide his/her username, password and scope to our client application, which then uses those credentials along with its credentials to the authorization server we want the tokens from.
-
Client Authorization − The Oauth system requires the client to be authorized to be able to provide the token. Hence, under the Authorization header, we provide the client authentication information, namely username and password that we configured in our application.
我们仔细查看查询参数和授权标头 -
Let’s take a closer look at the query params and the authorization header −
查询参数
The query params
客户端凭证
Client credentials
如果一切正确,我们将能够在响应中看到我们生成的令牌以及 200 ok 状态。
If everything is correct, we shall be able to see our generated token in the response along with a 200 ok status.
响应
The response
我们可以通过输入错误的凭证或不输入凭证来测试我们的服务器,我们将收到一个错误,说明该请求未经授权或具有错误的凭证。
We can test our server, by putting wrong credentials or no credentials, and we will get back an error which would say the request is unauthorized or has bad credentials.
这就是我们的基本 oauth 授权服务器,它使用密码授权类型生成并提供密码。
This is our basic oauth authorization server, that uses the password grant type to generate and provide a password.
下一步,我们实现更安全且更常见的 oauth2 身份验证应用程序,即使用授权码授权类型。我们为此目的更新我们当前的应用程序。
Next, let’s implement a more secure, and a more common application of the oauth2 authentication, i.e. with an authorization code grant type. We will update our current application for this purpose.
授权授权类型在这样的意义上与密码授权类型不同,即用户不必与该客户端应用程序共享他的凭证。他仅与授权服务器共享凭证,作为回报,授权代码被发送到客户端,它使用该代码来验证客户端。它比密码授权类型更安全,因为用户凭证不会与客户端应用程序共享,因此用户的的信息保持安全。客户端应用程序无法访问任何重要的用户信息,除非得到用户的批准。
The authorization grant type is different from the password grant type in the sense that the user doesn’t have to share his credentials with the client application. He shares them with the authorization server only and in return authorization code is sent to the client which it uses to authenticate the client. It is more secure than the password grant type as user credentials are not shared with the client application and hence the user’s information stays safe. The client application doesn’t get access to any important user information unless approved by the user.
在一些简单的步骤中,我们便可以在我们的应用程序中设置具有基本 oauth 服务器和授权授权类型的服务器。我们来看看如何执行。
In a few simple steps, we can set up a basic oauth server with an authorization grant type in our application. Let’s see how.
package com.tutorial.spring.security.oauth2.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired private AuthenticationManager authenticationManager;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("oauthclient1")
.secret("oauthsecret1")
.scopes("read") .authorizedGrantTypes("password")
.and() .withClient("oauthclient2") .secret("oauthsecret2")
.scopes("read") .authorizedGrantTypes("authorization_code")
.redirectUris("http://locahost:9090");
}
@Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
}
我们为该操作添加了一个第二个客户端 oauthclient2,指定新的机密和读取作用域。在该客户端中,我们已将授予类型更改为授权代码。我们还添加了一个重定向 URI,以便授权服务器能够回调客户端。所以,重定向 URI 基本上是客户端的 URI。
Let’s add a second client for this operation oauthclient2 for this operation with a new secret and read scope. Here we have changed the grant type to authorization code for this client. We also added a redirect URI so that the authorization server can callback the client. So, basically the redirect URI is the URI of the client.
现在,我们必须在用户和授权服务器之间建立连接。我们必须为授权服务器设置一个接口,用户可以在其中输入凭证。我们使用 Spring Security 的 formLogin() 实施来实现该功能,同时使用保持简单。我们还要确保所有请求都得到身份验证。
Now, we have to establish a connection between the user and the authorization server. We have to set an interface for the authorization server where the user can provide the credentials. We use the formLogin() implementation of Spring Security to achieve that functionality while keeping things simple. We also make sure that all requests are authenticated.
package com.tutorial.spring.security.oauth2.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
@SuppressWarnings("deprecation") @Configuration
public class UserConfig extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService() {
UserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
UserDetails user = User.withUsername("john")
.password("12345") .authorities("read") .build();
userDetailsManager.createUser(user); return userDetailsManager;
}
@Bean public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override protected void configure(HttpSecurity http) throws Exception {
http.formLogin(); http.authorizeRequests().anyRequest().authenticated();
}
}
这就完成了我们为授权授权类型所做的设置。现在,要测试我们的设置并启动我们的应用程序。我们使用 [role="bare"] [role="bare"]http://localhost:8080/oauth/authorize?response_type=code&client_id=oauthclient2&scope=read 启动我们的浏览器。我们将被重新定向到 Spring Security 的默认表单登录页面。
This completes our setup for the authorization grant type. Now to test our setup and launch our application. We launch our browser at [role="bare"]http://localhost:8080/oauth/authorize?response_type=code&client_id=oauthclient2&scope=read. We will redirected to the default form login page of Spring Security.
在此,响应类型代码表示授权服务器将返回访问代码,客户端将使用该代码登录。我们使用用户凭据时,我们将会被问及是否要授予客户端要求的权限,屏幕与下面所示的屏幕类似。
Here, the response type code implies that the authorization server will return an access code which will be used by the client to log in. When we use the user credentials we will be asked if I want to grant the permissions asked by the client, in a similar screen as shown below.
如果我们批准并点击授权,我们就应当看到我们被重定向到给定的重定向网址以及访问代码。在我们的案例中,我们被重定向到 [role="bare"] [role="bare"]http://locahost:9090/?code=7Hibnw ,正如我们在应用程序中指定的。我们现在可以使用 Postman 中的代码作为客户端登录到授权服务器。
If we approve and click Authorize we shall see we are redirected to our given redirect url along with the access code. In our case the we are redirected to [role="bare"]http://locahost:9090/?code=7Hibnw, as we specified in the application. We can use the code now as a client in Postman to login to the authorization server.
正如我们这里所看到的,我们在 URL 中使用了从授权服务器收到的代码,将 grant_type 用作 authorization_code,将 scope 用作 read。我们充当客户端并提供了在我们的应用程序中配置的客户端凭据。在我们做出此请求时,我们收回了我们的 access_token,我们可以进一步使用它。
As we can see here, we have used the code received from the authorization server in our URL, and the grant_type as authorization_code and scope as read. We acted as the client and provided the client credentials as configured in our application. When we make this request we get back our access_token which we can use further.
因此,我们已经看到了如何使用 OAuth 2.0 配置 Spring Security。应用程序非常简单易懂,有助于我们比较容易地理解该过程。我们已经使用了两类授权授权类型,并且已经看到了如何使用它们来为我们的客户端应用程序获取访问令牌。
So, we have seen how we can configure Spring Security with OAuth 2.0. The application is pretty simple and easy to understand and helps us understand the process fairly easily. We have used two kinds of authorization grant types and seen how we can use them to acquire access tokens for our client application.
Spring Security - JWT
Contents
-
JWT Introduction and overview
-
Getting started with Spring Security using JWT(Practical Guide)
JWT Introduction and overview
JSON Web Token or JWT ,正如它通常被称为的那样,是一种开放的 Internet 标准(RFC 7519),用于在各方之间以紧凑的方式安全地传输受信任的信息。令牌包含编码为 JSON 对象并且使用私有密钥或公钥/私钥对进行数字签名的声明。它们是自包含的和可验证的,因为它们是数字签名的。JWT 可以被签名和/或加密。签名令牌验证令牌中包含的声明的完整性,而加密令牌向其他方隐藏声明。
JSON Web Token or JWT, as it is more commonly called, is an open Internet standard (RFC 7519) for securely transmitting trusted information between parties in a compact way. The tokens contain claims that are encoded as a JSON object and are digitally signed using a private secret or a public key/private key pair. They are self-contained and verifiable as they are digitally signed. JWT’s can be signed and/or encrypted. The signed tokens verify the integrity of the claims contained in the token, while the encrypted ones hide the claims from other parties.
JWT 也可以用于交换信息,尽管它们更常用于授权,因为它们提供了比基于内存随机令牌的会话管理的更多优势。其中最大的优势是能够将身份验证逻辑委托给诸如 AuthO 等第三方服务器。
JWT’s can also be used for the exchange of information though they more commonly used for authorization as they offer a lot of advantages over session management using in-memory random tokens. The biggest of them being the enabling the delegation of authentication logic to a third-party server like AuthO etc.
JWT 令牌分为 3 部分,即标题、有效载荷和签名,格式如下:
A JWT token is divided into 3 parts namely – header, payload, and signature in the format of
[Header].[Payload].[Signature]
-
Header − The Header of a JWT token contains the list cryptographic operations that are applied to the JWT. This can be the signing technique, metadata information about the content-type and so on. The header is presented as a JSON object which is encoded to a base64URL. An example of a valid JWT header would be
{ "alg": "HS256", "typ": "JWT" }
在此,“ alg ”向我们提供了有关所用算法类型的的信息,并且 “typ 给我们该信息的类型。
Here, “alg” gives us information about the type of algorithm used and “typ gives us the type of the information.
-
Payload − The payload part of JWT contains the actual data to be transferred using the token. This part is also known as the “claims” part of the JWT token. The claims can be of three types – registered, public and private.
-
The registered claims are the ones which are recommended but not mandatory claims such as iss(issuer), sub(subject), aud(audience) and others.
-
Public claims are those that are defined by those using the JWTs.
-
Private claims or custom claims are user-defined claims created for the purpose of sharing the information between the concerned parties.
有效载荷对象的一个示例可能如下所示。
Example of a payload object could be.
{ "sub": "12345", "name": "Johnny Hill", "admin": false }
有效载荷对象,如同标题对象一样,也被 base64Url 编码,并且此字符串形成 JWT 的第二部分。
The payload object, like the header object is base64Url encoded as well and this string forms the second part of the JWT.
-
Signature− The signature part of the JWT is used for the verification that the message wasn’t changed along the way. If the tokens are signed with private key, it also verifies that the sender is who it says it is. It is created using the encoded header, encoded payload, a secret and the algorithm specified in the header. An example of a signature would be.
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
如果我们放入标题、有效载荷和签名,我们就会得到一个如下所示的令牌。
If we put the header, payload and signature we get a token as given below.
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6I
kpvaG4gRG9lIiwiYWRtaW4iOmZhbHNlfQ.gWDlJdpCTIHVYKkJSfAVNUn0ZkAjMxskDDm-5Fhe
WJ7xXgW8k5CllcGk4C9qPrfa1GdqfBrbX_1x1E39JY8BYLobAfAg1fs_Ky8Z7U1oCl6HL63yJq_
wVNBHp49hWzg3-ERxkqiuTv0tIuDOasIdZ5FtBdtIP5LM9Oc1tsuMXQXCGR8GqGf1Hl2qv8MCyn
NZJuVdJKO_L3WGBJouaTpK1u2SEleVFGI2HFvrX_jS2ySzDxoO9KjbydK0LNv_zOI7kWv-gAmA
j-v0mHdJrLbxD7LcZJEGRScCSyITzo6Z59_jG_97oNLFgBKJbh12nvvPibHpUYWmZuHkoGvuy5RLUA
现在,该令牌可以使用 Bearer 架构在 Authorization 标头中使用,如下所示。
Now, this token can be used in the Authorization header using the Bearer schema as.
Authorization − Bearer <令牌>
Authorization − Bearer <token>
使用 JWT 令牌进行授权是其最常见的应用。令牌通常在服务器中生成,然后发送到存储在会话存储或本地存储中的客户端。若要访问受保护的资源,客户端会将 JWT 发送在上面给出的头部中。我们将在下面的部分中看到 Spring Security 中的 JWT 实现。
The use of JWT token for authorization is the most common of its applications. The token is usually generated in the server and sent to the client where it is stored in the session storage or local storage. To access a protected resource the client would send the JWT in the header as given above. We will see the JWT implementation in Spring Security in the section below.
Getting Started with Spring Security using JWT
我们将要开发的应用程序将使用 JWT 处理基本的用用户身份验证和授权。让我们从前往 start.spring.io 开始,我们将在其中使用以下依赖项创建一个 Maven 应用程序。
The application we are going to develop will handle basic user authentication and authorization with JWT’s. Let’s get started by going to start.spring.io where we will create a Maven application with the following dependencies.
-
Spring Web
-
Spring Security
我们生成项目,并在下载后将其解压缩到我们选择的文件夹中。然后,我们可以使用我们选择的任意 IDE。我将使用 Spring Tools Suite 4,因为它最适合 Spring 应用程序。
We generate the project and when it is downloaded, we extract it to a folder of our choice. We can then use any IDE of our choice. I am going to use Spring Tools Suite 4 as it is most optimized for Spring applications.
除了上述依赖项之外,我们还要从 Maven 中央存储库中包含来自 io.jsonwebtoken 的 jwt 依赖项,因为它不包含在 Spring 转换器中。此依赖项处理了涉及 JWT 的所有操作,包括构建令牌、解析它以索取所有权等等。
Apart from the above-mentioned dependencies we are also going to include the jwt dependency from io.jsonwebtoken from the Maven central repository as it is not included in the spring initializer. This dependency takes care of all operations involving the JWT including building the token, parsing it for claims and so on.
<dependency>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
我们的 pom.xml 文件现在应该类似于此。
Our pom.xml file should now look similar to this.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE<version>
<relativePath />
<!-- lookup parent from repository -->
</parent>
<groupId>com.spring.security</groupId>
<artifactId>jwtbasic</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>jwtbasic</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test<scope>
<dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
在设置完项目后,我们准备创建公开获取端点的 Hello Controller 控制器类。
Now that our project is set up we are going to create our controller class Hello Controller which exposes a Get endpoint.
package com.spring.security.jwtbasic.controllers;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
现在,我们准备创建一个名为 config 的包,在其中添加扩展 Spring Security 的 WebSecurityConfigurerAdapter 类的配置类。这将为我们提供项目配置和应用程序安全所需的所有必需函数和定义。目前,我们通过实现生成相同的方法提供 BcryptPasswordEncoder 实例。我们使用 @Bean 来注释该方法,以将其添加到我们的 Spring Context。
Now we are going to create a package called config where we add the configuration class that extends the WebSecurityConfigurerAdapter class of Spring Security. This will provide us with all the required functions and definitions for project configuration and security of our application. For now, we provide the BcryptPasswordEncoder instance by implementing a method that generates the same. We annotate the method with @Bean to add to our Spring Context.
package com.spring.security.jwtbasic.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.spring.security.jwtbasic.jwtutils.JwtAuthenticationEntryPoint;
import com.spring.security.jwtbasic.jwtutils.JwtFilter;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
JWT 包括一个我们将在 application.properties 文件中定义的秘密,如下所示。
The JWT includes a secret which we will define in our application.properties file as given below.
secret=somerandomsecret
现在,让我们创建一个名为 jwtutils 的包。此包将包含与 JWT 操作相关的所有类和接口,其中包括。
Now let’s create a package called jwtutils. This package is going to contain all classes and interface related to JWT operations, which will include.
-
Generating token
-
Validating token
-
Checking the signature
-
Verifying claims and permissions
在此包中,我们创建第一个类,称为 Token Manager。此类将负责使用 io.jsonwebtoken.Jwts 创建和验证令牌。
In this package, we create our first class called Token Manager. This class will be responsible for the creation and validation of tokens using io.jsonwebtoken.Jwts.
package com.spring.security.jwtbasic.jwtutils;
import java.io.Serializable;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
@Component
public class TokenManager implements Serializable {
/**
*
*/
private static final long serialVersionUID = 7008375124389347049L; public static final long TOKEN_VALIDITY = 10 * 60 * 60; @Value("${secret}")
private String jwtSecret;
public String generateJwtToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder().setClaims(claims).setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + TOKEN_VALIDITY * 1000))
.signWith(SignatureAlgorithm.HS512, jwtSecret).compact();
}
public Boolean validateJwtToken(String token, UserDetails userDetails) {
String username = getUsernameFromToken(token);
Claims claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody();
Boolean isTokenExpired = claims.getExpiration().before(new Date());
return (username.equals(userDetails.getUsername()) && !isTokenExpired);
}
public String getUsernameFromToken(String token) {
final Claims claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody();
return claims.getSubject();
}
}
在此处,由于所有令牌都应具有到期日期,因此我们从令牌有效期常量开始。在此,我们希望我们的令牌在生成后 10 分钟内有效。我们会在生成令牌时使用此值。然后,使用 @Value 注释从 application.properties 文件中将我们的私钥的值提取到我们的 jwtSecret 字段中。
Here, as all tokens should have an expiration date, we start with a token validity constant. Here, we want our token to be valid for 10 minutes after generation. We will use this value when we generate our token. Then we extract the value of our singing key from the application.properties file into our jwtSecret field using the @Value annotation.
我们在此有两种方法 −
We have two methods here −
-
generateJwtToken() − This method is used to generate a token on successful authentication by the user. To create the token here we use the username, issue date of token and the expiration date of the token. This will form the payload part of the token or claims as we had discussed earlier. To generate the token we use the builder() method of Jwts. This method returns a new JwtBuilder instance that can be used to create compact JWT serialized strings.
若要设置所有权,我们使用 setClaims() 方法,然后设置每个所有权。对此令牌,我们已设置 setSubject(username)、颁发日期和到期日期。我们还可以输入自定义所有权,就像我们上面讨论的那样。这可能是我们想要的任何值,其中可能包括用户角色、用户权限等等。
To set the claims we use the setClaims() method and then set each of the claims. For this token we have setSubject(username), issue date and expiration date. We can also put our custom claims as we had discussed above. This can be any value we want which might include user role, user authorities and so on.
然后,我们设置令牌签名部分。这是使用 signWith() 方法完成的,我们设置我们希望使用的哈希算法和密钥。然后,我们使用 compact() 方法,该方法将生成 JWT 并将其序列化为根据 JWT 紧凑序列化规则生成的紧凑型 URL 安全字符串。
Then we set the signature part of the token. This is done using the signWith() method, we set the hashing algorithm we prefer to use and the secret key. Then we use thecompact() method that builds the JWT and serializes it to a compact, URL-safe string according to the JWT Compact Serialization rules.
-
validateJwtToken() − Now that the generation of the token is taken care of, we should focus on the process of validation of the token when it comes as a part of requests. To validate the token means to verify the request is an authenticated one and that the token is the one that was generated and sent to the user. Here, we need to parse the token for the claims such as username, roles, authorities, validity period etc.
若要验证令牌,我们首先需要解析它。这是通过使用 Jwts 的 parser() 方法来完成的。然后,我们需要设置用于生成令牌的加密密钥,然后使用 parseClaimsJws() 方法对令牌进行解析,以基于生成器的当前配置状态来解析紧凑型序列化 JWS 字符串,并返回结果 Claims JWS 实例。然后,使用 getBody() 方法来返回在生成令牌时使用的 allOwnership 实例。
To validate the token we need to parse it first. This is done using the parser() method of Jwts. We then need to set the signing key that we used to generate the token and then use parseClaimsJws() method on the token to parse the compact serialized JWS string based on the builder’s current configuration state and return the resulting Claims JWS instance. The getBody() method is then used to return the claims instance that was used while generating the token.
从获取的 allOwnership 实例中,我们提取主体和到期日期来验证令牌的有效性。用户名应该是用户的用户名,并且令牌不应过期。如果满足这两个条件,我们将返回 true,表示令牌有效。
From this obtained claims instance, we extract the subject and the expiry date to verify the validity of the token. The username should be the username of the user and the token should not be expired. If both these conditions are met, we return true, which signifies that the token is valid.
下面我们创建的类是 JwtUserDetailsService 。该类将扩展 Spring 安全的 UserDetailsService ,我们还将实现 loadUserByUsername() 方法,如下所示 −
The next class we would be creating is the JwtUserDetailsService. This class will extend the UserDetailsService of Spring security and we will implement the loadUserByUsername() method as given below −
package com.spring.security.jwtbasic.jwtutils;
import java.util.ArrayList;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class JwtUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if ("randomuser123".equals(username)) {
return new User("randomuser123",
"$2a$10$slYQmyNdGzTn7ZLBXBChFOC9f6kFjAqPhccnP6DxlWXx2lPk1C3G6",
new ArrayList<>());
} else {
throw new UsernameNotFoundException("User not found with username: " + username);
}
}
}
这里,由于这是一个用于演示 JWT 身份验证的简单应用程序,所以我们使用了一组用户详情,而不是使用数据库。我们已经将用户名指定为“ randomuser123 ”并对密码进行编码,将其指定为“$2a$10$slYQmyNdGzTn7ZLBXBChFOC9f6kFjAqPhccnP6DxlWXx2lPk1C3G6”,以方便我们。
Here, since this is a basic application for the sole purpose of the demonstration of JWT authentication, we have resorted to a set of our user details, instead of using a database. We have given the username as “randomuser123” and encoded the password, which is “password” as “$2a$10$slYQmyNdGzTn7ZLBXBChFOC9f6kFjAqPhccnP6DxlWXx2lPk1C3G6” for our convenience.
接下来,我们为请求和响应模型创建类。这些模型决定了身份验证的请求和响应格式。下面给出的第一个快照是请求模型。我们将会接受两个属性——请求中的用户名和密码。
Next, we create classes for our Request and Response models. These models determine how our request and response formats would be for authentication. The first snapshot given below is the request model. As we can see, we shall be accepting two properties – username and password in our request.
package com.spring.security.jwtbasic.jwtutils.models;
import java.io.Serializable;
public class JwtRequestModel implements Serializable {
/**
*
*/
private static final long serialVersionUID = 2636936156391265891L;
private String username;
private String password;
public JwtRequestModel() {
}
public JwtRequestModel(String username, String password) {
super();
this.username = username; this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
以下是成功身份验证的响应模型的代码。我们将会在成功身份验证后将令牌发送回用户。
Below is the code for response model on successful authentication. As we can see, we will be sending the token back to the user on successful authentication.
package com.spring.security.jwtbasic.jwtutils.models;
import java.io.Serializable;
public class JwtResponseModel implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private final String token;
public JwtResponseModel(String token) {
this.token = token;
}
public String getToken() {
return token;
}
}
对于身份验证,我们创建一个控制器,如下所示。
For authentication now, let’s create a controller as given below.
package com.spring.security.jwtbasic.jwtutils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.spring.security.jwtbasic.jwtutils.models.JwtRequestModel;
import com.spring.security.jwtbasic.jwtutils.models.JwtResponseModel;
@RestController
@CrossOrigin
public class JwtController {
@Autowired
private JwtUserDetailsService userDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenManager tokenManager;
@PostMapping("/login")
public ResponseEntity<> createToken(@RequestBody JwtRequestModel
request) throws Exception {
try {
authenticationManager.authenticate(
new
UsernamePasswordAuthenticationToken(request.getUsername(),
request.getPassword())
);
} catch (DisabledException e) {
throw new Exception("USER_DISABLED", e);
} catch (BadCredentialsException e) {
throw new Exception("INVALID_CREDENTIALS", e);
}
final UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
final String jwtToken = tokenManager.generateJwtToken(userDetails);
return ResponseEntity.ok(new JwtResponseModel(jwtToken));
}
}
如果我们浏览代码,会发现我们自动装配了三个依赖关系,即 JwtUserDetailsService 、AuthenticationManager 和 TokenManager 。虽然我们已经看到了上述 JwtUserDetailsService 和 TokenManager 类的实现,但身份验证管理器 bean 是我们将在 WebSecurityConfig 类中创建的 bean。
If we go through the code we can see that, we have autowired three dependencies namely, JwtUserDetailsService, AuthenticationManager and TokenManager. While we have already seen the implementation of JwtUserDetailsService and TokenManager classes above, the authentication manager bean is one we shall be creating in our WebSecurityConfig class.
AuthenticationManager 类将负责我们的身份验证。我们将使用 UsernamePasswordAuthenticationToken 模型对请求进行身份验证。如果身份验证成功,我们将会为用户生成一个 JWT,该 JWT 可以发送在后续请求的 Authorization 标头中以获取任何资源。
AuthenticationManager class will take care of our authentication. We shall be using the UsernamePasswordAuthenticationToken model for authentication of the request. If authentication succeeds we shall generate a JWT for the user, which can be sent in the Authorization header of the subsequent requests to get any resource.
我们使用 JwtUserDetailsService 类的 loadUserByUsername() 方法和 TokenManager 类的 generateJwtToken() 。
As we can see, we are using the loadUserByUsername() method of our JwtUserDetailsService class and the generateJwtToken() from TokenManager class.
生成的 JWT 在身份验证成功后作为响应发送给用户,如上所述。
This generated JWT is sent to the user as a response on successful authentication as mentioned above.
现在是时候创建 Filter。过滤器类将用于跟踪我们的请求并检测它们是否在标头中包含有效的令牌。如果令牌有效,我们将让请求继续;否则,我们会发送 401 错误 (Unauthorized) 。
Now it’s time we created our Filter. The filter class will be used to track our requests and detect if they contain the valid token in the header. If the token is valid we let the request proceed otherwise we send a 401 error (Unauthorized).
package com.spring.security.jwtbasic.jwtutils;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import io.jsonwebtoken.ExpiredJwtException;
@Component
public class JwtFilter extends OncePerRequestFilter {
@Autowired
private JwtUserDetailsService userDetailsService;
@Autowired
private TokenManager tokenManager;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String tokenHeader = request.getHeader("Authorization");
String username = null;
String token = null;
if (tokenHeader != null && tokenHeader.startsWith("Bearer ")) {
token = tokenHeader.substring(7);
try {
username = tokenManager.getUsernameFromToken(token);
} catch (IllegalArgumentException e) {
System.out.println("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
System.out.println("JWT Token has expired");
}
} else {
System.out.println("Bearer String not found in token");
}
if (null != username &&SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (tokenManager.validateJwtToken(token, userDetails)) {
UsernamePasswordAuthenticationToken
authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null,
userDetails.getAuthorities());
authenticationToken.setDetails(new
WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
filterChain.doFilter(request, response);
}
}
我们可以看到,我们在其中也自动装配了 JwtUserDetailsService 和 TokenManager 类。我们扩展了 SpringSecurity 的 OncePerRequestFilter ,该 OncePerRequestFilter 确保对每个请求运行该过滤器。我们已经向 OncePerRequestFilter 类的重写方法 doFilterInternal() 提供了我们的实现。
As we can see above, we have autowired the JwtUserDetailsService and TokenManager classes here as well. We have extended the OncePerRequestFilter of SpringSecurity which makes sure the filter is run for every request. We have provided our implementation to the overridden method doFilterInternal() of the OncePerRequestFilter class.
这里的方法从标头中提取令牌,并借助我们的 TokenManager 类的 validateJwtToken() 方法对其进行验证。在验证过程中,它会检查用户名和到期日期。如果这两个值都已验证,我们会在 Spring Security 上下文中保存身份验证,并让代码继续进行过滤器链中的下一个过滤器。如果任何验证失败,或者令牌有问题,或者找不到令牌,我们将抛出相应的异常,并在阻止请求继续的同时发送回适当的响应。
The method here extracts the token from the header and validates it with the help of validateJwtToken() method of our TokenManager class. During validation, it checks for the username and the expiration date. If both the values are valid, we save the authentication in our Spring Security context and let the code proceed to the next filter in our filter chain. If any of the validation fails or there is an issue with the token or if the token is not found we throw the appropriate exceptions and send back an appropriate response while blocking the request from moving ahead.
为我们的请求创建了过滤器后,现在我们创建 JwtAutheticationEntryPoint 类。该类扩展 Spring 的 AutenticationEntryPoint 类,并以发送回客户端的错误代码 401 拒绝每个未经身份验证的请求。我们重写了 AuthenticationEntryPoint 类的 commence() 方法来实现这一点。
Having created the filter for our requests, we now create the JwtAutheticationEntryPoint class. This class extends Spring’s AuthenticationEntryPoint class and rejects every unauthenticated request with an error code 401 sent back to the client. We have overridden the commence() method of AuthenticationEntryPoint class to do that.
package com.spring.security.jwtbasic.jwtutils;
import java.io.IOException;
import java.io.Serializable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint,
Serializable {
@Override
public void commence(HttpServletRequest request, HttpServletResponse
response,
AuthenticationException authException) throws
IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
"Unauthorized");
}
}
现在,让我们回到 WebSecurityConfig 类,完成剩余的配置。如果我们记得,我们会在 Jwt 控制器类中用到 AuthenticationManager bean,并将刚才创建的过滤器添加到我们的配置中。我们还会配置需要身份验证的请求和不需要身份验证的请求。我们还要向请求添加 AuthenticationEntryPoint ,以发送回 401 错误响应。由于在使用 jwt 时我们不需要维护会话变量,因此我们可以使会话 STATELESS 。
Now, let’s get back to our WebSecurityConfig class and finish the rest of our configuration. If we remember, we are going to require our AuthenticationManager bean for our Jwt controller class and add the filter we just created to our configuration. We are also going to configure which requests are to be authenticated and which are not to be. We shall also add the AuthenticationEntryPoint to our requests to send back the 401 error response. Since, we also do not need to maintain session variables while using jwt we can make our session STATELESS.
package com.spring.security.jwtbasic.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.spring.security.jwtbasic.jwtutils.JwtAuthenticationEntryPoint;
import com.spring.security.jwtbasic.jwtutils.JwtFilter;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtFilter filter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws
Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
}
}
我们可以看到,我们已经完成了所有这些,现在我们的应用程序可以运行了。让我们启动应用程序,并使用 postman 来执行我们的请求。
As we can see, we have done all of that, and now our application is ready to go. Let’s start the application and use postman for making our requests.
这里我们已经发出了第一个请求以获取令牌,我们可以看到,在提供了正确的用户名/密码组合后我们就收到了令牌。
Here we have made our first request to get the token, and as we can see on providing the correct username/password combination we get back our token.
现在,在标头中使用该令牌,让我们调用 /hello 端点。
Now using that token in our header, let’s call the /hello endpoint.
我们可以看到,由于请求已通过身份验证,所以我们收到了期望的响应。现在,如果我们篡改令牌或不发送 Authorization 标头,我们将收到应用程序中配置的 401 错误。这可确保使用 JWT 保护我们的请求。
As we can see, since the request is authenticated, we get the desired response back. Now, if we tamper with the token or do not send the Authorization header, we will get a 401 error as configured in our application. This ensures that the protection our request using the JWT.