Personal blog :www.zhenganwen.top, surprise at the end of the article!

Environment to prepare

All example code in this article is hosted in the code cloud: gitee.com/zhenganwen/…

Surprise at the end!

The development environment

  • JDK1.8
  • Maven

The project structure

  • spring-security-demo

    Parent project, used as a dependency for the entire project

  • security-core

    Security authentication core modules, security-browser and security-app are built based on it

  • security-browser

    The PC browser is authorized mainly through Session

  • security-app

    Mobile Authorization

  • security-demo

    Apply security-browser and security-app

Rely on

spring-security-demo

Add Spring dependencies auto-compatible dependencies and compiled plug-ins

<packaging>pom</packaging>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.spring.platform</groupId>
            <artifactId>platform-bom</artifactId>
            <version>Brussels-SR4</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Dalston.SR2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>2.3.2</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin>
    </plugins>
</build>
Copy the code

security-core

Add dependencies such as persistence, OAuth authentication, Social authentication, and Commons utility classes, some of which are just added for later use

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.social</groupId>
        <artifactId>spring-social-config</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.social</groupId>
        <artifactId>spring-social-core</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.social</groupId>
        <artifactId>spring-social-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.social</groupId>
        <artifactId>spring-social-web</artifactId>
    </dependency>
    <dependency>
        <groupId>commons-lang</groupId>
        <artifactId>commons-lang</artifactId>
    </dependency>
    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
    </dependency>
    <dependency>
        <groupId>commons-beanutils</groupId>
        <artifactId>commons-beanutils</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.16.22</version>
        <scope>compile</scope>
    </dependency>
</dependencies>
Copy the code

security-browser

Add security-core and cluster management dependencies

<dependencies>
    <dependency>
        <groupId>top.zhenganwen</groupId>
        <artifactId>security-core</artifactId>
        <version>1.0 the SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session</artifactId>
    </dependency>
</dependencies>
Copy the code

security-app

Add the security – the core

<dependencies>
    <dependency>
        <groupId>top.zhenganwen</groupId>
        <artifactId>security-core</artifactId>
        <version>1.0 the SNAPSHOT</version>
    </dependency>
</dependencies>
Copy the code

security-demo

Temporarily reference security-browser for PC authentication

<artifactId>security-demo</artifactId>
<dependencies>
    <dependency>
        <groupId>top.zhenganwen</groupId>
        <artifactId>security-browser</artifactId>
        <version>1.0 the SNAPSHOT</version>
    </dependency>
</dependencies>
Copy the code

configuration

Add the startup class in security-demo as follows

package top.zhenganwen.securitydemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/ * * *@author zhenganwen
 * @date 2019/8/18
 * @desc SecurityDemoApplication
 */
@SpringBootApplication
@RestController
public class SecurityDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecurityDemoApplication.class, args);
    }

    @RequestMapping("/hello")
    public String hello(a) {
        return "hello spring security"; }}Copy the code

Add mysql connection information based on error message

Spring. The datasource. The driver - class - name = com. Mysql. JDBC. Driver spring. The datasource. Url = JDBC: mysql: / / 127.0.0.1:3306 / test? useUnicode=yes&characterEncoding=UTF-8&useSSL=false spring.datasource.username=root spring.datasource.password=123456Copy the code

Session cluster sharing and Redis are not needed at the moment. Disable them first

spring.session.store-type=none
Copy the code
@SpringBootApplication(exclude = {RedisAutoConfiguration.class,RedisRepositoriesAutoConfiguration.class})
@RestController
public class SecurityDemoApplication {
Copy the code

We then find that we can start successfully, however, accessing /hello to find that it prompts us to log in, which is Spring Security’s default authentication policy in effect, so we disable it as well

security.basic.enabled = false
Copy the code

Restart/Hello. Hello Spring Security is displayed. The environment is set up successfully

Restful

Restful VS traditional

Restful is an HTTP interface authoring style, not a standard or specification. The main differences between using Restful style and the traditional approach are as follows

  • URL
    • The traditional way is usually throughURLTo add string and query parameters that indicate interface behavior, such as/user/get? username=xxx
    • RestfulThe style recommends that a URL represent a system resource,/user/1Should indicate access to the systemidThe user is 1
  • Request way
    • The traditional way is generally throughgetSubmission, the downsidegetThe reference attaches the request parameters to the URL, which has a length limit and is not secure because the parameters are displayed in clear text on the URL without special handling. Requests that require the above two points will be usedpostsubmit
    • RestfulStyle favors the use of submission to describe request behavior, such asPOST,DELETE,PUT,GETRespond to requests to add, delete, change, or check the type
  • Medium of communication
    • In the traditional approach, the response to a request is a page, which requires multiple systems to be developed for different endpoints, and the front and back ends are logically coupled
    • RestfulStyle advocate useJSONAs the communication medium of the front and rear end, the front and rear end are separated; The response result type is identified by the response status code, such as200Indicates that the request was successfully processed,404Indicates that the corresponding resource is not found.500Indicates that the server is handling an exception.

Restful, a reference: www.runoob.com/w3cnote/res…

SpringMVC advanced features with REST services

Jar package

The above setup can already be run through the IDE and access/Hello, but production environments typically include projects as an executable JAR package that can be run directly through java-JAR.

If you run maven clean package, you will find that the jar generated in security-demo/target is only 7KB. This is because maven’s default packaging method does not include jars that depend on it and set the SpringBoot boot class. At this point we need to add a packaged plug-in to the SECURity-Demo POM

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>1.3.3. RELEASE</version>
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
    <! -- generated jar file name -->
    <finalName>demo</finalName>
</build>
Copy the code

Jar is executable, and Demo.jar. Original retains maven’s default package

Write interface test cases using MockMVC

Following the test-first principle (write test cases first, then write interfaces, and verify that the program works as we want it to), we need to leverage the Spring-boot-starter-test testing framework and its associated MockMvcAPI. To mock means to use test cases to make a program solid.

Start by adding test dependencies in security-demo

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>
Copy the code

Then create a new test class in SRC /test/ Java as follows

package top.zhenganwen.securitydemo;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.c.status;

/ * * *@author zhenganwen
 * @date 2019/8/18
 * @desc SecurityDemoApplicationTest
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class SecurityDemoApplicationTest {

    @Autowired
    WebApplicationContext webApplicationContext;

    private MockMvc mockMvc;

    @Before
    public void before(a) {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

    @Test
    public void hello(a) throws Exception {
        mockMvc.perform(get("/hello").contentType(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$").value("hello spring security")); }}Copy the code

Because you are testing an HTTP interface, you need to inject the Web container WebApplicationContext. Get (), status(), jsonPath(), jsonPath(), jsonPath(), jsonPath(), jsonPath(), jsonPath(); Application /json (so that parameters are attached to the request body as JSON, yes yes, GET request can also be attached to the request body!)

AndExpect (status().isok ()) expects a response status code of 200 (see HTTP status code), AndExpect ((jsonPath(“$”).value(” Hello Spring Security “)) Expects the JSON data for the response to be a string with the content hello Spring Security (this method relies on JSON parsing framework JsonPath, $represents JSON ontology in Java corresponding data type object, more API see: github.com/search?q=js…

The more important apis are MockMvc, MockMvcRequestBuilders, and MockMvcRequestBuilders

  • MockMvc, the callperformSpecifying the interface address
  • MockMvcRequestBuilders, construct the request (including request path, submission method, request header, request body, etc.)
  • MockMvcRequestBuildersAssert the response result, such as response status code and response body

MVC Annotation details

@RestController

Used to identify a Controller as a Restful Controller, where the return result of the method is automatically converted to JSON by SpringMVC and the response header is set to Content-Type=application/ JSON

@RequestMapping

Used to map urls to methods, and SpringMVC automatically binds request parameters to method input parameters according to their corresponding parameter names

package top.zhenganwen.securitydemo.dto;

import lombok.Data;

import java.io.Serializable;

/ * * *@author zhenganwen
 * @date 2019/8/18
 * @desc User
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

    private String username;
    private String password;
}

Copy the code
package top.zhenganwen.securitydemo.web.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import top.zhenganwen.securitydemo.dto.User;

import java.util.Arrays;
import java.util.List;

/ * * *@author zhenganwen
 * @date 2019/8/18
 * @desc UserController
 */
@RestController
public class UserController {

    @GetMapping("/user")
    public List<User> query(String username) {
        System.out.println(username);
        List<User> users = Arrays.asList(new User(), new User(), new User());
        returnusers; }}Copy the code
package top.zhenganwen.securitydemo.web.controller;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/ * * *@author zhenganwen
 * @date 2019/8/18
 * @desc UserControllerTest
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {

    @Autowired
    private WebApplicationContext webApplicationContext;

    private MockMvc mockMvc;

    @Before
    public void setUp(a) throws Exception {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

    @Test
    public void query(a) throws Exception {
        mockMvc.perform(get("/user").
                contentType(MediaType.APPLICATION_JSON_UTF8)
                .param("username"."tom"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.length()").value(3)); }}Copy the code

Through MockMvcRequestBuilders. Param to request form with the URL parameter.

Specify the submission method

RequestMapping(method = requestmethod.get) if @requestMapping (method = requestmethod.get) is set, then only GET requests will be accepted. Any other method will result in 405 unsupported Request method

@RequestParam

Required parameters

In the example above, if the request does not carry the username parameter, then the Controller parameter is given a default value of data type. If you want the request to carry this parameter or not, you can use @requestParam and specify required=true (optional, default is).

Controller

@GetMapping("/user")
public List<User> query(@RequestParam String username) {
    System.out.println(username);
    List<User> users = Arrays.asList(new User(), new User(), new User());
    return users;
}
Copy the code

ControllerTest

@Test
public void testBadRequest(a) throws Exception {
    mockMvc.perform(get("/user").
                    contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(status().is4xxClientError());
}
Copy the code

We can use is4xxClientError() to assert a request whose response status code is 400

Parameter name mapping

SpringMVC defaults to the same parameter name rule. It is worth mapping parameters. If you want to bind the value of the request parameter username to the method parameter username, you can use the name attribute or the value attribute

@GetMapping("/user")
public List<User> query(@RequestParam(name = "username") String userName) {
    System.out.println(userName);
    List<User> users = Arrays.asList(new User(), new User(), new User());
    return users;
}

@GetMapping("/user")
public List<User> query(@RequestParam("username") String userName) {
    System.out.println(userName);
    List<User> users = Arrays.asList(new User(), new User(), new User());
    return users;
}
Copy the code
@Test
public void testParamBind(a) throws Exception {
    mockMvc.perform(get("/user").
                    contentType(MediaType.APPLICATION_JSON_UTF8)
                    .param("username"."tom"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.length()").value(3));
}
Copy the code

Default Parameter Value

If you don’t want to force a request to carry a parameter, but you want the method parameter to have a defaultValue if it doesn’t receive a parameter value (for example, “” is less error-prone than null), you can use the defaultValue attribute

@GetMapping("/user")
public List<User> query(@RequestParam(required = false,defaultValue = "") String userName) {
    Objects.requireNonNull(userName);
    List<User> users = Arrays.asList(new User(), new User(), new User());
    return users;
}
Copy the code
@Test
public void testDefaultValue(a) throws Exception {
    mockMvc.perform(get("/user").
                    contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.length()").value(3));
}
Copy the code

Beans binding

If there are many parameters attached to the request, and each parameter belongs to the attribute of an Object, it is redundant to write them in the method parameter list one by one. We can uniformly encapsulate them into a Data Transportation Object (DTO), as shown in

package top.zhenganwen.securitydemo.dto;

import lombok.Data;

/ * * *@author zhenganwen
 * @date 2019/8/19
 * @desc UserCondition
 */
@Data
public class UserQueryConditionDto {

    private String username;
    private String password;
    private String phone;
}
Copy the code

SpringMVC will help us bind the request parameters to the object properties (default binding rule is consistent parameter names).

@GetMapping("/user")
public List<User> query(@RequestParam("username") String userName, UserQueryConditionDto userQueryConditionDto) {
    System.out.println(userName);
    System.out.println(ReflectionToStringBuilder.toString(userQueryConditionDto, ToStringStyle.MULTI_LINE_STYLE));
    List<User> users = Arrays.asList(new User(), new User(), new User());
    return users;
}
Copy the code

ReflectionToStringBuilder reflection tools can not rewrite the object when the toString method through reflection to help us view the object’s properties.

@Test
public void testDtoBind(a) throws Exception {
    mockMvc.perform(get("/user").
                    contentType(MediaType.APPLICATION_JSON_UTF8)
                    .param("username"."tom")
                    .param("password"."123456")
                    .param("phone"."12345678911"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.length()").value(3));
}
Copy the code

The Bean binding does not affect the @requestParam binding

And don’t worry about running aflow with @requestParam

tom
top.zhenganwen.securitydemo.dto.UserQueryConditionDto@440ef8d[
  username=tom
  password=123456
  phone=12345678911
]
Copy the code

Bean binding takes precedence over primitive type parameter binding

However, if you don’t annotate userName with @requestParam, it will receive a NULL

null
top.zhenganwen.securitydemo.dto.UserQueryConditionDto@440ef8d[
  username=tom
  password=123456
  phone=12345678911
]
Copy the code

Paging parameter binding

The spring-data family (such as spring-boot-data-redis) encapsulates a pageable DTOPageable, The paging parameters size (number of lines per page), page (current page number), sort(sort field and sort policy) that we passed are automatically bound to the automatically injected Pageable instance

@GetMapping("/user")
public List<User> query(String userName, UserQueryConditionDto userQueryConditionDto, Pageable pageable) {
    System.out.println(userName);
    System.out.println(ReflectionToStringBuilder.toString(userQueryConditionDto, ToStringStyle.MULTI_LINE_STYLE));
    System.out.println(pageable.getPageNumber());
    System.out.println(pageable.getPageSize());
    System.out.println(pageable.getSort());
    List<User> users = Arrays.asList(new User(), new User(), new User());
    return users;
}
Copy the code
@Test
public void testPageable(a) throws Exception {
    mockMvc.perform(get("/user").
                    contentType(MediaType.APPLICATION_JSON_UTF8)
                    .param("username"."tom")
                    .param("password"."123456")
                    .param("phone"."12345678911")
                    .param("page"."2")
                    .param("size"."30")
                    .param("sort"."age,desc"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.length()").value(3));
}
Copy the code
null
top.zhenganwen.securitydemo.dto.UserQueryConditionDto@24e5389c[
  username=tom
  password=123456
  phone=12345678911
]
2
30
age: DESC
Copy the code

@PathVariable

Variable placeholder

The most common Restful URL is GET /user/1 to obtain the information of the user whose ID is 1. At this time, we need to replace 1 in the path with a placeholder such as {id} when writing the interface, and dynamically bind it to the method parameter ID according to the actual URL request

@GetMapping("/user/{id}")
public User info(@PathVariable("id") Long id) {
    System.out.println(id);
    return new User("jack"."123");
}
Copy the code
@Test
public void testPathVariable(a) throws Exception {
    mockMvc.perform(get("/user/1").
                    contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.username").value("jack"));
}

1
Copy the code

When the method parameter name is the same as the URL placeholder variable name, the value attribute of @pathVariable can be omitted

Regular match

Sometimes we need fine-grained control over URL matching, for example /user/1 will match /user/{id}, but /user/ XXX will not match /user/{id}.

@GetMapping("/user/{id:\\d+}")
public User getInfo(@PathVariable("id") Long id) {
    System.out.println(id);
    return new User("jack"."123");
}
Copy the code
@Test
public void testRegExSuccess(a) throws Exception {
    mockMvc.perform(get("/user/1").
                    contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(status().isOk());
}

@Test
public void testRegExFail(a) throws Exception {
    mockMvc.perform(get("/user/abc").
                    contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(status().is4xxClientError());
}
Copy the code

@JsonView

Application scenarios

Sometimes we need to filter certain fields of the response object, for example, the password field is not displayed when all users are queried, and the password field is displayed when users are queried by ID. This can be done with the @JsonView annotation

Method of use

1. Declare view interfaces. Each interface represents the object field visibility policy when responding to data

The view is just a field containment strategy that we’re going to use when we add @jsonView

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

    /** * Common view, returns basic user information */
    public interface UserOrdinaryView {}/** * Details view, in addition to the fields contained in the normal view, also returns details such as passwords */
    public interface UserDetailsView extends UserOrdinaryView{}private String username;
    
    private String password;
}
Copy the code

Views can be inherited from each other. After a view is inherited, the fields contained in the view are inherited

2. Add a view to the field of the response object to indicate that the field is included in the view

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

    /** * Common view, returns basic user information */
    public interface UserOrdinaryView {}/** * Details view, in addition to the fields contained in the normal view, also returns details such as passwords */
    public interface UserDetailsView extends UserOrdinaryView{}@JsonView(UserOrdinaryView.class)
    private String username;
    
    @JsonView(UserDetailsView.class)
    private String password;
}
Copy the code

3. Add a view to the Controller method to indicate that the object data returned by the method will display only the fields contained in the view

@GetMapping("/user")
@JsonView(User.UserBasicView.class)
public List<User> query(String userName, UserQueryConditionDto userQueryConditionDto, Pageable pageable) {
    System.out.println(userName);
    System.out.println(ReflectionToStringBuilder.toString(userQueryConditionDto, ToStringStyle.MULTI_LINE_STYLE));
    System.out.println(pageable.getPageNumber());
    System.out.println(pageable.getPageSize());
    System.out.println(pageable.getSort());
    List<User> users = Arrays.asList(new User("tom"."123"), new User("jack"."456"), new User("alice"."789"));
    return users;
}

@GetMapping("/user/{id:\\d+}")
@JsonView(User.UserDetailsView.class)
public User getInfo(@PathVariable("id") Long id) {
    System.out.println(id);
    return new User("jack"."123");
}
Copy the code

test

@Test
public void testUserBasicViewSuccess(a) throws Exception {
    MvcResult mvcResult = mockMvc.perform(get("/user"). contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(status().isOk()) .andReturn(); System.out.println(mvcResult.getResponse().getContentAsString()); [{}"username":"tom"}, {"username":"jack"}, {"username":"alice"}]

@Test
public void testUserDetailsViewSuccess(a) throws Exception {
    MvcResult mvcResult = mockMvc.perform(get("/user/1").
                                          contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(status().isOk())
        .andReturn();
    System.out.println(mvcResult.getResponse().getContentAsString());
}

{"username":"jack"."password":"123"}
Copy the code

Stage reconstruction

Refactoring requires small, fast steps, and every time you write a piece of functionality you go back and look at what needs to be optimized

RequestMapping for both methods in the code uses /user, which can be added to the class for reuse

@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping
    @JsonView(User.UserBasicView.class)
    public List<User> query(String userName, UserQueryConditionDto userQueryConditionDto, Pageable pageable) {
        System.out.println(userName);
        System.out.println(ReflectionToStringBuilder.toString(userQueryConditionDto, ToStringStyle.MULTI_LINE_STYLE));
        System.out.println(pageable.getPageNumber());
        System.out.println(pageable.getPageSize());
        System.out.println(pageable.getSort());
        List<User> users = Arrays.asList(new User("tom"."123"), new User("jack"."456"), new User("alice"."789"));
        return users;
    }

    @GetMapping("/{id:\\d+}")
    @JsonView(User.UserDetailsView.class)
    public User getInfo(@PathVariable("id") Long id) {
        System.out.println(id);
        return new User("jack"."123"); }}Copy the code

Although it is a very detailed problem, but must have this thought and habit

Don’t forget to rerun all test cases after refactoring to make sure the refactoring didn’t change the behavior of the program

Processing request body

@requestbody maps the RequestBody to the parameters of a Java method

By default, SpringMVC does not resolve parameters in the request body and bind them to method parameters

@PostMapping
public void createUser(User user) {
    System.out.println(user);
}
Copy the code
@Test
public void testCreateUser(a) throws Exception {
    mockMvc.perform(post("/user").
                    contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"username\":\"jack\",\"password\":\"123\"}"))
        .andExpect(status().isOk());
}

User(id=null, username=null, password=null)
Copy the code

Using @requestbody, you can parse JSON data in the RequestBody into Java objects and bind them to method entry parameters

@PostMapping
public void createUser(@RequestBody User user) {
    System.out.println(user);
}
Copy the code
@Test
public void testCreateUser(a) throws Exception {
    mockMvc.perform(post("/user").
                    contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"username\":\"jack\",\"password\":\"123\"}"))
        .andExpect(status().isOk());
}

User(id=null, username=jack, password=123)
Copy the code

Date type parameter processing

If you need to bind the date-type data to the Date field of a Bean, a common solution on the web is to add a JSON message converter for formatting, which will write the Date display logic to the back end.

It would be a good idea for the back end to only store the timestamp, and only pass the timestamp to the front end, leaving the responsibility of formatting the display to the front end

@PostMapping
public void createUser(@RequestBody User user) {
    System.out.println(user);
}
Copy the code
@Test
public void testDateBind(a) throws Exception {
    Date date = new Date();
    System.out.println(date.getTime());
    mockMvc.perform(post("/user").
                    contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"username\":\"jack\",\"password\":\"123\",\"birthday\":\"" + date.getTime() + "\"}"))
        .andExpect(status().isOk());
}

1566212381139
User(id=null, username=jack, password=123, birthday=Mon Aug 19 18:59:41 CST 2019)
Copy the code

The @valid annotation validates the request parameters

Extraction check logic

In the Controller method, we often need to validate the request parameters before executing the processing logic, which is traditionally written with if judgment

@PostMapping
public void createUser(@RequestBody User user) {
    if (StringUtils.isBlank(user.getUsername())) {
        throw new IllegalArgumentException("User name cannot be empty");
    }
    if (StringUtils.isBlank(user.getPassword())) {
        throw new IllegalArgumentException("Password cannot be empty.");
    }
    System.out.println(user);
}
Copy the code

But you need to write duplicate code if you need validation elsewhere, you need to change multiple places if the validation logic changes, and you can leave your program vulnerable if you miss something. Those with a little refactoring awareness might encapsulate each validation logic with a separate method, but that would still be redundant.

SpringMVC Restful recommends using @VALID to verify parameters. In addition, if the parameters fail to pass the verification, the front-end will respond to 400 BAD Request with a status code to indicate the processing result (and the request format is not correct), rather than throwing an exception and receiving a status code of 500

We’ll start by using some constraint annotations provided by the Hibernate-Validator validation framework to constrain the Bean fields

@NotBlank
@JsonView(UserBasicView.class)
private String username;

@NotBlank
@JsonView(UserDetailsView.class)
private String password;
Copy the code

Just by adding these annotations, SpringMVC doesn’t validate for us

@PostMapping
public void createUser(@RequestBody User user) {
    System.out.println(user);
}
Copy the code
@Test
public void testConstraintValidateFail(a) throws Exception {
    mockMvc.perform(post("/user").
                    contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"username\":\"\"}"))
        .andExpect(status().isOk());
}

User(id=null, username=, password=null, birthday=null)
Copy the code

We also add the @VALID annotation to the Bean we want to validate, so SpringMVC will validate against the constraint annotation we added to the Bean and respond with 400 bad Request if the validation fails

@PostMapping
public void createUser(@Valid @RequestBody User user) {
    System.out.println(user);
}
Copy the code
@Test
public void testConstraintValidateSuccess(a) throws Exception {
    mockMvc.perform(post("/user").
                    contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"username\":\"\"}"))
        .andExpect(status().is4xxClientError());
}
Copy the code

Constraint annotations

Hibernate – Validator provides the following constraint annotations

For example, limiting the birthday value in the request parameter to a past time when creating a user

Start by adding constraint annotations to the Bean’s fields

@Past
private Date birthday;
Copy the code

Then append the @valid annotation to the Bean you want to validate

@PostMapping
public void createUser(@Valid @RequestBody User user) {
    System.out.println(user);
}
Copy the code
@Test
public void testValidatePastTimeSuccess(a) throws Exception {
    // Get the point in time one year ago
    Date date = new Date(LocalDateTime.now().plusYears(-1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    mockMvc.perform(post("/user").
                    contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"username\":\"jack\",\"password\":\"123\",\"birthday\":\"" + date.getTime() + "\"}"))
        .andExpect(status().isOk());
}

@Test
public void testValidatePastTimeFail(a) throws Exception {
    // Get the point one year from now
    Date date = new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    mockMvc.perform(post("/user").
                    contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"username\":\"jack\",\"password\":\"123\",\"birthday\":\"" + date.getTime() + "\"}"))
        .andExpect(status().is4xxClientError());
}
Copy the code

Multiplexing check logic

So, if we need to add validation to the user modification method, we just add @valid

@PutMapping("/{id}")
public void update(@Valid @RequestBody User user, @PathVariable Long id) {
    System.out.println(user);
    System.out.println(id);
}
Copy the code
@Test
public void testUpdateSuccess(a) throws Exception {
    mockMvc.perform(put("/user/1").
                    contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"username\":\"jack\",\"password\":\"789\"}"))
        .andExpect(status().isOk());
}

User(id=null, username=jack, password=789, birthday=null)
1

@Test
public void testUpdateFail(a) throws Exception {
    mockMvc.perform(put("/user/1").
                    contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"username\":\"jack\",\"password\":\" \"}"))
        .andExpect(status().is4xxClientError());
}
Copy the code

The constraint logic needs to be declared only once in the Bean with the constraint annotation, and anything else that needs to be checked with the constraint needs to be added to @VALID

BindingResult Processes the verification result

The above approach is still not perfect. We just use the response status code to tell the front end that the request data format is wrong, but we don’t specify what is wrong. We need to give the front end some more explicit information

In the example above, if the check is not passed, the method will not be executed and will return directly, and we would not be able to write a message if we wanted to insert it. In this case, we can use BindingResult, which can help us get the verification failure information and return it to the front end, and the response status code will change to 200

@PostMapping
public void createUser(@Valid @RequestBody User user,BindingResult errors) {
    if (errors.hasErrors()) {
        errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage()));
    }
    System.out.println(user);
}

@PutMapping("/{id}")
public void update(@PathVariable Long id,@Valid @RequestBody User user, BindingResult errors) {
    if (errors.hasErrors()) {
        errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage()));
    }
    System.out.println(user);
    System.out.println(id);
}
Copy the code
@Test
public void testBindingResult(a) throws Exception {
    Date date = new Date(LocalDateTime.now().plusYears(-1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    mockMvc.perform(post("/user").
                    contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"username\":\"jack\",\"password\":null,\"birthday\":\"" + date.getTime() + "\"}"))
        .andExpect(status().isOk());
}

may not be empty
User(id=null, username=jack, password=null, birthday=Sun Aug 19 20:44:02 CST 2018)

@Test
public void testBindingResult2(a) throws Exception {
    Date date = new Date(LocalDateTime.now().plusYears(-1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    mockMvc.perform(put("/user/1").
                    contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"username\":\"jack\",\"password\":null,\"birthday\":\"" + date.getTime() + "\"}"))
        .andExpect(status().isOk());
}

may not be empty
User(id=null, username=jack, password=null, birthday=Sun Aug 19 20:42:56 CST 2018)
1
Copy the code

It is important to note that BindingResult must be used with @VALID and must be placed immediately after the @VALID modified parameter in the parameter column, otherwise the following confusing result will occur

@PutMapping("/{id}")
public void update(@Valid @RequestBody User user, @PathVariable Long id, BindingResult errors) {
    if (errors.hasErrors()) {
        errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage()));
    }
    System.out.println(user);
    System.out.println(id);
}
Copy the code

In the above code, you insert an ID between the validating Bean and BindingResult, and you see that BindingResult does not work

@Test
public void testBindingResult2(a) throws Exception {
    Date date = new Date(LocalDateTime.now().plusYears(-1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    mockMvc.perform(put("/user/1").
                    contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"username\":\"jack\",\"password\":null,\"birthday\":\"" + date.getTime() + "\"}"))
        .andExpect(status().isOk());
}

java.lang.AssertionError: Status 
Expected :200
Actual   :400
Copy the code

check

Custom message

Now we can get the validation failure message from BindingResult

@PutMapping("/{id:\\d+}")
public void update(@PathVariable Long id, @Valid @RequestBody User user, BindingResult errors) {
    if (errors.hasErrors()) {
        errors.getAllErrors().stream().forEach(error -> {
            FieldError fieldError = (FieldError) error;
            System.out.println(fieldError.getField() + "" + fieldError.getDefaultMessage());
        });
    }
    System.out.println(user);
}
Copy the code
@Test
public void testBindingResult3(a) throws Exception {
    Date date = new Date(LocalDateTime.now().plusYears(-1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    mockMvc.perform(put("/user/1").
                    contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"username\":\" \",\"password\":null,\"birthday\":\"" + date.getTime() + "\"}"))
        .andExpect(status().isOk());
}

password may not be empty
username may not be empty
User(id=null, username= , password=null, birthday=Sun Aug 19 20:56:35 CST 2018)
Copy the code

However, the default message prompt is not very friendly and requires our own concatenation, so we need to customize the message prompt by specifying the message that fails validation using the Message attribute of the constraint annotation

@NotBlank(message = "User name cannot be empty")
@JsonView(UserBasicView.class)
private String username;

@NotBlank(message = "Password cannot be empty.")
@JsonView(UserDetailsView.class)
private String password;
Copy the code
@Test
public void testBindingResult3(a) throws Exception {
    Date date = new Date(LocalDateTime.now().plusYears(-1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    mockMvc.perform(put("/user/1").
                    contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"username\":\" \",\"password\":null,\"birthday\":\"" + date.getTime() + "\"}")) .andExpect(status().isOk()); } password The password cannot be empty. Username The User name cannot be empty. User(id=null, username= , password=null, birthday=Sun Aug 19 21:03:18 CST 2018)
Copy the code

Custom validation annotations

Although hibernate-Validator provides some common constraint annotations, for complex business scenarios it is necessary to define a custom constraint annotation. Sometimes a non-null or formalized validation is not enough, and a database query may be required for validation

Let’s take a look at the existing constraint annotations and create a custom “user name cannot be repeated” constraint annotation

1. Create a constraint annotation class

We want the annotation to be on some FIELD of the Bean, using @target ({FIELD}); In addition, for this annotation to work at RUNTIME, add @Retention(RUNTIME)

package top.zhenganwen.securitydemo.annotation.valid;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/ * * *@author zhenganwen
 * @date 2019/8/20
 * @desc Unrepeatable
 */
@Target({FIELD})
@Retention(RUNTIME)
public @interface Unrepeatable {
    
}

Copy the code

Refer to existing constraint annotations such as NotNull and NotBlank, which have three methods

String message() default "{org.hibernate.validator.constraints.NotBlank.message}"; Class<? >[] groups() default { }; Class<? extends Payload>[] payload() default { };Copy the code

So we declare these three methods as well

@Target({FIELD})
@Retention(RUNTIME)
public @interface Unrepeatable {
    String message(a) default"User name has been registered"; Class<? >[] groups()default{}; Class<? extends Payload>[] payload()default{}; }Copy the code

2. Write verification logic classes

They also have a @constraint annotation based on existing annotations

@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@ReportAsSingleViolation
@NotNull
public @interface NotBlank {
Copy the code

Hold Down Ctrl and click on the validateBy property to see that it needs an implementation class that ConstraintValidator, Now we need to write a ConstraintValidator custom validation logic and bind it to our Unrepeatable annotation with the validatedBy attribute

package top.zhenganwen.securitydemo.annotation.valid;

import org.springframework.beans.factory.annotation.Autowired;
import top.zhenganwen.securitydemo.service.UserService;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/ * * *@author zhenganwen
 * @date 2019/8/20
 * @desc UsernameUnrepeatableValidator
 */
public class UsernameUnrepeatableValidator implements ConstraintValidator<Unrepeatable.String> {

    @Autowired
    private UserService userService;

    @Override
    public void initialize(Unrepeatable unrepeatableAnnotation) {
        System.out.println(unrepeatableAnnotation);
        System.out.println("UsernameUnrepeatableValidator initialized===================");
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        System.out.println("the request username is " + value);
        boolean ifExists = userService.checkUsernameIfExists( value);
        // If the user name exists, reject the request and prompt that the user name is registered, otherwise process the request
        return ifExists == true ? false : true; }}Copy the code

ConstraintValidator

The generic A specifies the annotation to bind to, and T specifies the type of the field to validate. IsValid is used to compile user-defined verification logic. For example, to check whether the database has records of the user name, the value true indicates that the verification succeeds. False indicates that the verification fails
,t>

The @ComponentScan implementation class ConstraintValidator is injected into the container by Spring, so you can inject other beans into the class without annotating Component, as in this case a UserService

package top.zhenganwen.securitydemo.service;

import org.springframework.stereotype.Service;

import java.util.Objects;

/ * * *@author zhenganwen
 * @date 2019/8/20
 * @desc UserService
 */
@Service
public class UserService {

    public boolean checkUsernameIfExists(String username) {
        // select count(username) from user where username=?
        // as if username "tom" has been registered
        if (Objects.equals(username, "tom")) {
            return true;
        }
        return false; }}Copy the code

Specify validation classes on constraint annotations

Specify A set of validation classes bound to this annotation through the validatedBy attribute (these validation classes must be the implementation classes of ConstraintValidator
,t>

@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = { UsernameUnrepeatableValidator.class})
public @interface Unrepeatable {
    String message(a) default"User name has been registered"; Class<? >[] groups()default{}; Class<? extends Payload>[] payload()default{}; }Copy the code

4, test,

@PostMapping
public void createUser(@Valid @RequestBody User user,BindingResult errors) {
    if (errors.hasErrors()) {
        errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage()));
    }
    System.out.println(user);
}
Copy the code
@Test
public void testCreateUserWithNewUsername(a) throws Exception {
    Date date = new Date(LocalDateTime.now().plusYears(-1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    mockMvc.perform(post("/user").
                    contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"username\":\"alice\",\"password\":\"123\",\"birthday\":\"" + date.getTime() + "\"}"))
        .andExpect(status().isOk());
}

the request username is alice
User(id=null, username=alice, password=123, birthday=Mon Aug 20 08:25:11 CST 2018)

    
@Test
public void testCreateUserWithExistedUsername(a) throws Exception {
    Date date = new Date(LocalDateTime.now().plusYears(-1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
    mockMvc.perform(post("/user").
                    contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"username\":\"tom\",\"password\":\"123\",\"birthday\":\"" + date.getTime() + "\"}")) .andExpect(status().isOk()); } the request username is a registered User(id= Tom)null, username=tom, password=123, birthday=Mon Aug 20 08:25:11 CST 2018)
Copy the code

Delete user

@Test
public void testDeleteUser(a) throws Exception {
    mockMvc.perform(delete("/user/1").
                    contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(status().isOk());
}

java.lang.AssertionError: Status 
Expected :200
Actual   :405
Copy the code

Test first, that is, write test cases first and then write functional code. Even if we know that the functional test will not pass without writing it, the test code also needs to be checked to ensure the correctness of the test logic

Restful supports a response status code to represent the result of a request processing. For example, 200 indicates that the request is successfully deleted. If you do not need to return certain information, you do not need to add the response body

@DeleteMapping("/{id:\\d+}")
public void delete(@PathVariable Long id) {
    System.out.println(id);
    // delete user
}
Copy the code
@Test
public void testDeleteUser(a) throws Exception {
    mockMvc.perform(delete("/user/1").
                    contentType(MediaType.APPLICATION_JSON_UTF8))
        .andExpect(status().isOk());
}

1
Copy the code

Error handling

The default error handling mechanism for SpringBoot

Respond by client

When an error occurs in the request processing, SpringMVC will respond differently depending on the client type. For example, a browser accessing localhost:8080/ XXX will return the following error page

Using a Postman request, you get the following response

{
    "timestamp": 1566268880358."status": 404."error": "Not Found"."message": "No message available"."path": "/xxx"
}
Copy the code

The source code for this mechanism is in BasicErrorController (when a 4xx or 500 exception occurs, the request is forwarded to /error, and BasicErrorController determines the exception response logic)

@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
    HttpStatus status = getStatus(request);
    Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
        request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
    response.setStatus(status.value());
    ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
}

@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
    Map<String, Object> body = getErrorAttributes(request,
                                                  isIncludeStackTrace(request, MediaType.ALL));
    HttpStatus status = getStatus(request);
    return new ResponseEntity<Map<String, Object>>(body, status);
}
Copy the code

If the browser made the request, its request header would be Accept: text/ HTML… Postman sends an Accept: */* request, so errorHtml responds to the error page, and Error collects exception information and returns it as a map

Custom error pages

For the client’s browser error response, such as 404/500, we can in the SRC/main/resources/resources/write a custom error page error folder, SpringMVC returns either 404.html or 500.html in that folder when a corresponding exception occurs

Create the SRC/main/resources/resources/error folder and add 404 HTML and 500 HTML


      
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>The page can't be found</title>
</head>
<body>Sorry, I can't find the page!</body>
</html>
Copy the code

      
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Service exceptions</title>
</head>
<body>The server has an internal error</body>
</html>
Copy the code

An exception occurred while simulating the processing of a request

@GetMapping("/{id:\\d+}")
@JsonView(User.UserDetailsView.class)
public User getInfo(@PathVariable("id") Long id) {
    throw new RuntimeException(Id does not exist);
    // System.out.println(id);
    // return new User(1L, "jack", "123");
    // return null;
}
Copy the code

Visit localhost:8080/ XXX to display 404.html page, visit localhost:8080/user/1 to display 500.html page

It is important to note that customizing the exception page does not cause non-browser requests to respond to the page

Custom exception handling

For 4XX client errors, SpringMVC simply returns an error response and does not execute the Controller method; If a server of 500 throws an exception, the message field value of the exception class is returned

Default abnormal response result

For example, client error, GET /user/1

{
    "timestamp": 1566270327128."status": 500."error": "Internal Server Error"."exception": "java.lang.RuntimeException"."message": Id does not exist."path": "/user/1"
}
Copy the code

For example, a server error

@PostMapping
public void createUser(@Valid @RequestBody User user) {
    System.out.println(user);
}
Copy the code
POST	localhost:8080/user
Body	{}
Copy the code
{
    "timestamp": 1566272056042."status": 400."error": "Bad Request"."exception": "org.springframework.web.bind.MethodArgumentNotValidException"."errors": [{"codes": [
                "NotBlank.user.username"."NotBlank.username"."NotBlank.java.lang.String"."NotBlank"]."arguments": [{"codes": [
                        "user.username"."username"]."arguments": null."defaultMessage": "username"."code": "username"}]."defaultMessage": "User name cannot be empty"."objectName": "user"."field": "username"."rejectedValue": null."bindingFailure": false."code": "NotBlank"
        },
        {
            "codes": [
                "NotBlank.user.password"."NotBlank.password"."NotBlank.java.lang.String"."NotBlank"]."arguments": [{"codes": [
                        "user.password"."password"]."arguments": null."defaultMessage": "password"."code": "password"}]."defaultMessage": "Password cannot be empty."."objectName": "user"."field": "password"."rejectedValue": null."bindingFailure": false."code": "NotBlank"}]."message": "Validation failed for object='user'. Error count: 2"."path": "/user"
}
Copy the code

Customize abnormal response results

Sometimes we often need to throw an exception while processing a request to terminate processing of the request, for example

package top.zhenganwen.securitydemo.web.exception.response;

import lombok.Data;

import java.io.Serializable;

/ * * *@author zhenganwen
 * @date 2019/8/20
 * @desc IdNotExistException
 */
@Data
public class IdNotExistException extends RuntimeException {

    private Serializable id;

    public IdNotExistException(Serializable id) {
        super(Id does not exist);
        this.id = id; }}Copy the code
@GetMapping("/{id:\\d+}")
@JsonView(User.UserDetailsView.class)
public User getInfo(@PathVariable("id") Long id) {
    throw new IdNotExistException(id);
}
Copy the code

GET /user/1

{
    "timestamp": 1566270990177."status": 500."error": "Internal Server Error"."exception": "top.zhenganwen.securitydemo.exception.response.IdNotExistException"."message": Id does not exist."path": "/user/1"
}
Copy the code

By default, SpringMVC only returns the message of the exception. If we need to return the ID of IdNotExistException as well to give the front end a clearer hint, we need to customize the exception handling

  1. Custom exception handling classes need to be added@ControllerAdvice
  2. Used in an exception handling method@ExceptionHandlerDeclare what exceptions the method intercepts, all of themControllerIf one of these exceptions is thrown, the method is converted to execute
  3. The caught exception is used as an input parameter to the method
  4. Method returns the result of andControllerMethod returns the same meaning if requiredjsonYou need to add it to the method@ResponseBodyAnnotation, which is added to a class to indicate that each method has the annotation
package top.zhenganwen.securitydemo.web.exception.handler;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import top.zhenganwen.securitydemo.web.exception.response.IdNotExistException;

import java.util.HashMap;
import java.util.Map;

/ * * *@author zhenganwen
 * @date 2019/8/20
 * @desc UserControllerExceptionHandler
 */
@ControllerAdvice
@ResponseBody
public class UserControllerExceptionHandler {

    @ExceptionHandler(IdNotExistException.class)
    public Map<String, Object> handleIdNotExistException(IdNotExistException e) {
        Map<String, Object> jsonResult = new HashMap<>();
        jsonResult.put("message", e.getMessage());
        jsonResult.put("id", e.getId());
        returnjsonResult; }}Copy the code

Run the Postman GET /user/1 command to GET the following response

{
    "id": 1."message": Id does not exist
}
Copy the code

intercept

Requirements: Record the processing time of all requests

Filter Filter

Filters are standard in JavaEE and do not depend on SpringMVC, so there are two steps to using filters in SpringMVC

1, implementation,FilterInterface and injected into the Spring container

package top.zhenganwen.securitydemo.web.filter;

import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

/ * * *@author zhenganwen
 * @date 2019/8/20
 * @desc TimeFilter
 */
@Component
public class TimeFilter implements Filter {

    // Executed when the Web container starts
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("TimeFilter init");
    }

    // Executes when a request is received, before it reaches the SpringMVC entry DispatcherServlet
    // A single request is executed only once (regardless of how many request forwards occur in the period)
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException,
            ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

        String service = "【" + request.getMethod() + "" + request.getRequestURI() + "】";
        System.out.println("[TimeFilter] received service call:" + service);

        Date start = new Date();
        System.out.println("[TimeFilter] Start service execution" + service + simpleDateFormat.format(start));

        filterChain.doFilter(servletRequest, servletResponse);

        Date end = new Date();
        System.out.println("[TimeFilter] services" + service + "Executed" + simpleDateFormat.format(end) +
                ", total time:" + (end.getTime() - start.getTime()) + "ms");
    }

    // Execute when the container is destroyed
    @Override
    public void destroy(a) {
        System.out.println("TimeFilter destroyed"); }}Copy the code

2, configuration,FilterRegistrationBeanThis step is equivalent to the traditional way inweb.xmlAdd a<Filter>node

package top.zhenganwen.securitydemo.web.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.zhenganwen.securitydemo.web.filter.TimeFilter;

/ * * *@author zhenganwen
 * @date 2019/8/20
 * @desc WebConfig
 */
@Configuration
public class WebConfig {

    @Autowired
    TimeFilter timeFilter;

    // Adding this bean is equivalent to adding a Fitler node to web.xml
    @Bean
    public FilterRegistrationBean registerTimeFilter(a) {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(timeFilter);
        returnfilterRegistrationBean; }}Copy the code

3, test,

Access GET /user/1, the console log is as follows

@GetMapping("/{id:\\d+}")
@JsonView(User.UserDetailsView.class)
public User getInfo(@PathVariable("id") Long id) {
    // throw new IdNotExistException(id);
    User user = new User();
    return user;
}
Copy the code
[TimeFilter] Received service call: [GET /user/1] [TimeFilter] start service [GET /user/1] 2019-08-20 02:13:44 [GET /user/1] End Total time: 4msCopy the code

Because Filter is a standard in JavaEE, it relies only on servlet-API and does not rely on any third party libraries, so it is not aware of the existence of Controller and therefore has no way of knowing which method the request will be mapped to. SpringMVC makes up for this shortcoming by introducing interceptors

Through filterRegistrationBean. AddUrlPattern can add blocking rule to filter, blocking rule is all urls by default

@Bean
public FilterRegistrationBean registerTimeFilter(a) {
    FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
    filterRegistrationBean.setFilter(timeFilter);
    filterRegistrationBean.addUrlPatterns("/ *");
    return filterRegistrationBean;
}
Copy the code

The Interceptor Interceptor

Interceptors differ from filters in the following ways

  • FilterIt’s request-based,InterceptorIs based onControllerMultiple requests may be executed at a timeController(by forwarding), so a request is executed only onceFilterBut it may be executed multiple timesInterceptor
  • InterceptorisSpringMVCComponent in, so it knowsControllerCan get relevant information (such as the method to which the request is mapped, the method of thebeanEtc.)

Using the interceptor provided by SpringMVC also requires two steps

1, implementation,HandlerInterceptorinterface

package top.zhenganwen.securitydemo.web.interceptor;

import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.text.SimpleDateFormat;
import java.util.Date;

/ * * *@author zhenganwen
 * @date 2019/8/20
 * @desc TimeInterceptor
 */
@Component
public class TimeInterceptor implements HandlerInterceptor {

    /** * is executed before the Controller method is executed@param httpServletRequest
     * @param httpServletResponse
     * @paramHandler Handler (wrapper of the Controller method) *@returnTrue will then execute the Controller method * false will not execute the Controller method and will respond 200 *@throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        String service = "【" + handlerMethod.getBean() + "#" + handlerMethod.getMethod().getName() + "】";
        Date start = new Date();
        System.out.println([TimeInterceptor # preHandle] service + service + "Called" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(start));
        httpServletRequest.setAttribute("start", start.getTime());
        return true;
    }

    /** * is executed after the Controller method completes normal execution. If the Controller method throws an exception, this method is not executed@param httpServletRequest
     * @param httpServletResponse
     * @param handler
     * @paramThe view returned by the modelAndView Controller method *@throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        String service = "【" + handlerMethod.getBean() + "#" + handlerMethod.getMethod().getName() + "】";
        Date end = new Date();
        System.out.println([TimeInterceptor # postHandle] + service + "End of call" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(end)
                + "Total time:" + (end.getTime() - (Long) httpServletRequest.getAttribute("start")) + "ms");
    }

    /** * Will be executed regardless of whether the Controller method throws an exception@param httpServletRequest
     * @param httpServletResponse
     * @param handler
     * @paramE If the Controller method throws an exception, null * otherwise@throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler, Exception e) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        String service = "【" + handlerMethod.getBean() + "#" + handlerMethod.getMethod().getName() + "】";
        Date end = new Date();
        System.out.println([TimeInterceptor # afterCompletion] service + service + "End of call" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(end)
                + "Total time:" + (end.getTime() - (Long) httpServletRequest.getAttribute("start")) + "ms");
        if(e ! =null) {
            System.out.println("[TimeInterceptor# afterCompletion] service" + service + "Call exception:"+ e.getMessage()); }}}Copy the code

2. The configuration class inherits WebMvcConfigureAdapter and overwrites addInterceptor to add a custom interceptor

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Autowired
    TimeFilter timeFilter;

    @Autowired
    TimeInterceptor timeInterceptor;

    @Bean
    public FilterRegistrationBean registerTimeFilter(a) {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(timeFilter);
        filterRegistrationBean.addUrlPatterns("/ *");
        return filterRegistrationBean;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(timeInterceptor); }}Copy the code

Multiple calls to addInterceptor add more interceptors

3, test,

  • GET /user/1
[TimeFilter] GET /user/1 [TimeFilter] Start to execute the service [GET /user/1] 2019-08-20 02:59:00 [TimeFilter# preHandle] service [top. Zhenganwen. Securitydemo. Web. Controller. UserController @ # 2 b6a0ea9 getInfo 】 is called the 2019-08-20 02:59:00The [TimeFilter] service [GET /user/1] is executed. 2019-08-20 02:59:00 total time: 2msCopy the code
  • willpreHandleReturn value changed totrue
[TimeFilter] Receives the service call: [GET /user/1] [TimeFilter] Starts to execute the service [GET /user/1] 2019-08-20 02:59:20 [TimeFilter# preHandle] service [top. Zhenganwen. Securitydemo. Web. Controller. UserController @ # 2 b6a0ea9 getInfo 】 is called the 2019-08-20 02:59:20
[TimeInterceptor # postHandle] service [top. Zhenganwen. Securitydemo. Web. Controller. UserController @ # 2 b6a0ea9 getInfo 】 calls to end the 2019-08-20 02:59:20 Total time: 39ms
[TimeInterceptor # afterCompletion] service [top. Zhenganwen. Securitydemo. Web. Controller. UserController @ # 2 b6a0ea9 getInfo 】 calls the end of 2019-08-20 Total time: 39msThe [TimeFilter] service [GET /user/1] is executed. 2019-08-20 02:59:20 total time: 42msCopy the code
  • Throw an exception in the Controller method
@GetMapping("/{id:\\d+}")
@JsonView(User.UserDetailsView.class)
public User getInfo(@PathVariable("id") Long id) {
    throw new IdNotExistException(id);
    // User user = new User();
    // return user;
}
Copy the code
[TimeFilter] GET /user/1 [TimeFilter] Start service [GET /user/1] 2019-08-20 03:05:56 [TimeFilter# preHandle] service [top. Zhenganwen. Securitydemo. Web. Controller. UserController @ # 2 b6a0ea9 getInfo 】 is called the 2019-08-20 03:05:56
[TimeInterceptor # afterCompletion] service [top. Zhenganwen. Securitydemo. Web. Controller. UserController @ # 2 b6a0ea9 getInfo 】 calls the end of 2019-08-20 Total time: 11msThe [TimeFilter] service [GET /user/1] is executed. 2019-08-20 03:05:56 total time: 14msCopy the code

It was found that the exception printing logic in afterCompletion was not executed because the IdNotExistException was handled by our custom exception handler and not thrown. Let’s try throwing a RuntimeException instead

@GetMapping("/{id:\\d+}")
@JsonView(User.UserDetailsView.class)
public User getInfo(@PathVariable("id") Long id) {
    throw new RuntimeException("id not exist");
}
Copy the code
[TimeFilter] Receives the service call: [GET /user/1] [TimeFilter] Starts the service. [GET /user/1] 2019-08-20 03:09:38 [TimeFilter# preHandle] service [top. Zhenganwen. Securitydemo. Web. Controller. UserController @ # 2 b6a0ea9 getInfo 】 is called the 2019-08-20 03:09:38
[TimeInterceptor # afterCompletion] service [top. Zhenganwen. Securitydemo. Web. Controller. UserController @ # 2 b6a0ea9 getInfo 】 calls the end of 2019-08-20 Total time: 7ms
[TimeInterceptor# afterCompletion] service [top. Zhenganwen. Securitydemo. Web. Controller. UserController @ # 2 b6a0ea9 getInfo 】 invoke exception: id not exist

java.lang.RuntimeException: id not exist
	at top.zhenganwen.securitydemo.web.controller.UserController.getInfo(UserController.java:42)
	...

[TimeInterceptor # preHandle] [service. Org. Springframework. Boot autoconfigure. Web. BasicErrorController @ # 33 f17289 error 】 is called the 2019-08-20 03:09:38
[TimeInterceptor # postHandle] [service. Org. Springframework. Boot autoconfigure. Web. BasicErrorController @ # 33 f17289 error] call to end the 2019-08-20 03:09:38 Total time: 7ms
[TimeInterceptor # afterCompletion] [service. Org. Springframework. Boot autoconfigure. Web. BasicErrorController @ # 33 f17289 error] calls the end of 2019-08-20 Total time: 7ms
Copy the code

The method invocation sequence diagram looks like this

Slice the Aspect

Application scenarios

The Interceptor still has its limitations, that is, it can’t get input information for calling the Controller method. For example, if we need to log the requested order items to provide data for the recommendation system, then the Interceptor can’t do anything about it

DispatcherServlet -> doService -> doDispatch -> DispatcherServlet -> doDispatch

if(! mappedHandler.applyPreHandle(processedRequest, response)) {return;
}

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
Copy the code

MappedHandler. ApplyPreHandle is called HandlerInterceptor preHandle method, After that, ha.handle(processedRequest, response, mappedHandler.gethandler ()) is called to inject the request parameter processedRequest into the handler input parameter

Method of use

Aspect-oriented Program AOP is an object enhancement design pattern based on dynamic proxy, which can add pluggable functions without modifying existing code.

Using AOP in SpringMVC requires three steps

  • Write slice/aspect classes that combine pointcuts and enhancements
    • add@Component, inject the Spring container
    • add@Aspect, start the section programming switch
  • Writing pointcuts, which can be done using annotations, consists of two parts: which methods need to be enhanced and when
    • Cut to the timing
      • @BeforeBefore the method is executed
      • @AfterReturningAfter the method is executed normally
      • @AfterThrowingMethod throws an exception
      • @AfterThe method is successfully executedreturnBefore, equivalent to inreturnA paragraph was inserted beforefinally
      • @Around, can use the injected input parameterProceedingJoinPointFlexible implementation of the above four timing, its role with the interceptor methodhandlerSimilar, but with more useful runtime information
    • Pointcut, you can useexecutionFor details, see:Docs. Spring. IO/spring/docs…
  • Write enhancements,
    • Only one@AroundYou can get in, you can getProceedingJoinPointThe instance
    • By calling theProceedingJoinPointthepoint.proceed()You can call the corresponding Controller method and get the return value
package top.zhenganwen.securitydemo.web.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

/ * * *@author zhenganwen
 * @date 2019/8/20
 * @desc GlobalControllerAspect
 */
@Aspect
@Component
public class GlobalControllerAspect {

    / / top. Zhenganwen. Securitydemo. Web. Controller under the package all controller method
    @Around("execution(* top.zhenganwen.securitydemo.web.controller.*.*(..) )")
    public Object handleControllerMethod(ProceedingJoinPoint point) throws Throwable {

        // Handler corresponds to the method signature (which method of which class, what is the argument list)
        String service = "【"+point.getSignature().toLongString()+"】";
        // The value of the argument passed to handler
        Object[] args = point.getArgs();

        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        Date start = new Date();
        System.out.println("[GlobalControllerAspect] starts calling the service" + service + "Request parameters: + Arrays.toString(args) + "," + simpleDateFormat.format(start));

        Object result = null;
        try {
            // Call the actual handler and get the result
            result = point.proceed();
        } catch (Throwable throwable) {
            System.out.println("[GlobalControllerAspect] calls the service" + service + "Exception occurred, message=" + throwable.getMessage());
            throw throwable;
        }

        Date end = new Date();
        System.out.println("[GlobalControllerAspect] services" + service + "The call ends and the response is:" + result+","+simpleDateFormat.format(end)+", total time:"+(end.getTime()-start.getTime())+
                "ms");

        // Return the result of the response, not necessarily the same as the result of the handler
        returnresult; }}Copy the code

test

@GetMapping("/{id:\\d+}")
@JsonView(User.UserDetailsView.class)
public User getInfo(@PathVariable("id") Long id) {
    System.out.println("[UserController # getInfo]query user by id");
    return new User();
}
Copy the code

GET /user/1

[TimeFilter] receives a service call: [GET /user/1] [TimeFilter] Starts executing the service [GET /user/1] 2019-08-20 05:21:48 [TimeFilter# preHandle] service [top. Zhenganwen. Securitydemo. Web. Controller. UserController @ # 49433 c98 getInfo 】 is called the 2019-08-20 05:21:48[GlobalControllerAspect] began to call the service [public top. Zhenganwen. Securitydemo. Dto. User Top. Zhenganwen. Securitydemo. Web. Controller. UserController. GetInfo (Java. Lang. Long) 】 request parameters: [1], 2019-08-20 05:21:48 [UserController# getInfo]query user by id[[GlobalControllerAspect] service. Public top. Zhenganwen securitydemo. Dto. User Top. Zhenganwen. Securitydemo. Web. Controller. UserController. GetInfo (Java. Lang. Long) 】 call ends, the response results as follows: User(id=null, username=null, password=null, birthday=null), 2019-08-20 05:21:48 [TimeInterceptor# postHandle] service [top. Zhenganwen. Securitydemo. Web. Controller. UserController @ # 49433 c98 getInfo 】 calls to end the 2019-08-20 05:21:48 Total time: 4ms
[TimeInterceptor # afterCompletion] service [top. Zhenganwen. Securitydemo. Web. Controller. UserController @ # 49433 c98 getInfo 】 calls the end of 2019-08-20 Total time: 4msThe [TimeFilter] service [GET /user/1] is executed. 2019-08-20 05:21:48 total time: 6msCopy the code
[TimeFilter] Received service call: [TimeInterceptor # getInterceptor] [TimeInterceptor # getInterceptor] Service [top. Zhenganwen. Securitydemo. Web. Controller. The UserController @ # 49433 c98 getInfo 】 is called the 2019-08-20 05:24:40 [GlobalControllerAspect] began to call the service [public top. Zhenganwen. Securitydemo. Dto. User Top. Zhenganwen. Securitydemo. Web. Controller. UserController. GetInfo (Java. Lang. Long) 】 request parameters: [1]. [UserController # getInfo]query user by id [GlobalControllerAspect] public top.zhenganwen.securitydemo.dto.User Top. Zhenganwen. Securitydemo. Web. Controller. UserController. GetInfo (Java. Lang. Long) 】 an exception occurs, message=id not exist [TimeInterceptor # afterCompletion] Service [top. Zhenganwen. Securitydemo. Web. Controller. The UserController @ # 49433 c98 getInfo 】 calls to end the 2019-08-20 05:24:40 took: 2ms [TimeInterceptor#afterCompletion] Service [top. Zhenganwen. Securitydemo. Web. Controller. The UserController @ # 49433 c98 getInfo 】 invoke exception: id not exist java.lang.RuntimeException: id not exist at top.zhenganwen.securitydemo.web.controller.UserController.getInfo(UserController.java:42) ... [[# TimeInterceptor preHandle] services. Org. Springframework. Boot autoconfigure. Web. BasicErrorController @ # 445821 a6 error] is invoked 2019-08-20 05:24:40 [TimeInterceptor # postHandle] Service [org. Springframework. Boot. Autoconfigure. Web. BasicErrorController @ # 445821 a6 error] call to end the 2019-08-20 05:24:40 took: 2ms [TimeInterceptor # afterCompletion] Service [org. Springframework. Boot. Autoconfigure. Web. BasicErrorController @ # 445821 a6 error] call to end the 2019-08-20 05:24:40 time elapsed: 3 msCopy the code

conclusion

Request process

The response process

Upload and download files and Mock tests

File upload

As always, test first, but using MockMvc to simulate a fileUpload request is a little different. The request uses the static fileUpload method and sets contentType to multipart/form-data

	@Test
    public void upload(a) throws Exception {
        File file = new File("C:\\Users\\zhenganwen\\Desktop"."hello.txt");
        FileInputStream fis = new FileInputStream(file);
        byte[] content = new byte[fis.available()];
        fis.read(content);
        String fileKey = mockMvc.perform(fileUpload("/file")
                /** * name request parameter, The 'name' attribute equivalent to the  tag * originalName Name of the uploaded file * contentType The uploaded file must be specified as' multipart/form-data '* content byte array, the content of the uploaded file */
                .file(new MockMultipartFile("file"."hello.txt"."multipart/form-data", content)))
                .andExpect(status().isOk())
                .andReturn().getResponse().getContentAsString();
        System.out.println(fileKey);
    }
Copy the code

File Management Controller

package top.zhenganwen.securitydemo.web.controller;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.Date;

/ * * *@author zhenganwen
 * @date 2019/8/21
 * @desc FileController
 */
@RestController
@RequestMapping("/file")
public class FileController {

    public static final String FILE_STORE_FOLDER = "C:\\Users\\zhenganwen\\Desktop\\";

    @PostMapping
    public String upload(MultipartFile file) throws IOException {

        System.out.println("[FileController] File request parameter: + file.getName());
        System.out.println("[FileController] File name:" + file.getName());
        System.out.println("[FileController] File size:"+file.getSize()+"Byte");

        
        String fileKey = new Date().getTime() + "_" + file.getOriginalFilename();
        File storeFile = new File(FILE_STORE_FOLDER, fileKey);

        // You can use file.getInputStream to upload files to storage systems such as FastDFS and CLOUD OSS
// InputStream inputStream = file.getInputStream();
// byte[] content = new byte[inputStream.available()];
// inputStream.read(content);

        file.transferTo(storeFile);

        returnfileKey; }}Copy the code

The test results

File request parameters: file [FileController] File name: file [FileController] File size: 12 bytes 1566349460611_hello.txtCopy the code

Check the desktop and find a 1566349460611_hello. TXT and the content is hello upload

File download

The Apache IO tool package was introduced

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.5</version>
</dependency>
Copy the code

File download interface

@GetMapping("/{fileKey:.+}")
public void download(@PathVariable String fileKey, HttpServletResponse response) throws IOException {

    try (
        InputStream is = new FileInputStream(new File(FILE_STORE_FOLDER, fileKey));
        OutputStream os = response.getOutputStream()
    ) {
        // Set the response header to Application /x-download
        response.setContentType("application/x-download");
        // Set the file name in the download query box
        response.setHeader("Content-Disposition"."attachment; filename="+ fileKey); IOUtils.copy(is, os); os.flush(); }}Copy the code

Test: the browser to http://localhost:8080/file/1566349460611_hello.txt

The reason the map is written as /{fileKey:.+} instead of /{fileKey} is because SpringMVC ignores the map. The character following the symbol. + means to match any non-\ n character. Without this re, the fileKey will fetch 1566349460611_hello instead of 1566349460611_hello.txt

Process REST services asynchronously

Previously, the Tomcat thread pool would send a thread to process each request sent by the client, and the thread would be occupied until the request was processed and the response result was completed. The Tomcat thread pool becomes stretched once the amount of concurrency in the system increases, so we can use asynchronous processing.

To avoid the interference of filters, interceptors and slicing logs added above, we will comment them out for the time being

//@Component
public class TimeFilter implements Filter {
Copy the code

Suddenly, the implementation Filter seems to inherit the Filter interface and add @Component to make it work, because TimeFilter still prints logs just by commenting out the registerTimeFilter method in WebConfig

//@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
Copy the code
//@Aspect
//@Component
public class GlobalControllerAspect {
Copy the code

Callable asynchronous processing

In Controller, if you have a Callable as the return value of a method, the threads in the Tomcat thread pool respond to the result by creating a new thread that executes the Callable and returns the result back to the client

package top.zhenganwen.securitydemo.web.controller;

import org.apache.commons.lang.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;


/ * * *@author zhenganwen
 * @date 2019/8/7
 * @desc AsyncController
 */
@RestController
@RequestMapping("/order")
public class AsyncOrderController {

    private Logger logger = LoggerFactory.getLogger(getClass());

    // Create an order
    @PostMapping
    public Callable<String> createOrder(a) {
        // Generates a 12-digit order number
        String orderNumber = RandomStringUtils.randomNumeric(12);
        logger.info("[main thread] received order creation request, order number =>" + orderNumber);
        Callable<String> result = () -> {
            logger.info("[secondary thread] create order start, order number =>"+orderNumber);
            // Simulate order creation logic
            TimeUnit.SECONDS.sleep(3);
            logger.info("[secondary thread] create order complete, order number =>" + orderNumber+", return result to client");
            return orderNumber;
        };
        logger.info("[main thread] has delegated the request to a sub-thread (order number =>" + orderNumber + "), continue processing other requests");
        returnresult; }}Copy the code

Using Postman, the results are as follows

Console log:

The 2019-08-21 21:10:39. 17044-059 the INFO [nio - 8080 - exec - 2] T.Z.S.W.C ontroller. AsyncOrderController: [main thread] received create order request, Order number = > 719547514079 2019-08-21 21:10:39. 17044-059 the INFO [nio - 8080 - exec - 2] T.Z.S.W.C ontroller. AsyncOrderController: [main thread] has delegated the request to the sub-thread (order no. =>719547514079), Continue to process other requests the 2019-08-21 21:10:39. 17044-063 the INFO/MvcAsync1 T.Z.S.W.C ontroller. AsyncOrderController: [MvcAsync1] [MvcAsync1] [MvcAsync1] Vice thread T.Z.S.W.C ontroller. AsyncOrderController: [] to create order, order number = > 719547514079, return the result to the clientCopy the code

It can be seen that the main thread does not execute the Callable order task and directly runs to continue to listen for other requests. The order task is executed by a new thread MvcAsync1 started by SpringMVC. Postman’s response time also gets its return value after the Callable execution. To the client, asynchronous processing on the back end is transparent and indistinguishable from synchronous processing; But for the back end, Tomcat’s thread that listens for requests is occupied for a very short time, greatly improving its concurrency capability

DeferredResult asynchronous processing

The drawback of Callable asynchronous processing is that it can only be done asynchronously by creating sub-threads locally, but with the prevalence of microservice architectures nowadays, we often need asynchronous processing across systems. For example, in the second kill system, there is a large number of concurrent order requests. If the back end synchronizes each order request (that is, the order is processed in the request thread) and then returns the response result, the service will be suspended (there is no response after sending the order request). In this case, we might use messaging middleware, where the request thread just listens for the order request and sends a message to MQ, where the order system pulls the message (such as the order number) from MQ to process the order and returns the result to the second kill system. Seckill system set up an independent thread to listen to the order processing result message, and return the processing result to the client. As is shown in

In order to achieve a similar effect, we need to use the Future mode. We can set up a processing result named DeferredResult, and if we call its getResult, we will not get the processing result. Although the request thread continues to process the request, the client is still pending. Only when a thread calls its setResult(result), the corresponding result will be sent to the client

In this example, to reduce complexity, linkedLists in local memory are used instead of distributed messaging middleware, and local new threads are used instead of order system threads, as follows

System AsyncOrderController

package top.zhenganwen.securitydemo.web.async;

import org.apache.commons.lang.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;

import java.util.concurrent.TimeUnit;


/ * * *@author zhenganwen
 * @date 2019/8/7
 * @desc AsyncController
 */
@RestController
@RequestMapping("/order")
public class AsyncOrderController {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private DeferredResultHolder deferredResultHolder;

    @Autowired
    private OrderProcessingQueue orderProcessingQueue;

    // The system places a single request
    @PostMapping
    public DeferredResult<String> createOrder(a) {

        logger.info("[Request thread] Received order request");

        // Generates a 12-digit order number
        String orderNumber = RandomStringUtils.randomNumeric(12);

        // Create a process result credential and place it in the cache so that the thread listening to the order result message sent by the order system to MQ sets the result in the credential, which triggers the result response to the client
        DeferredResult<String> deferredResult = new DeferredResult<>();
        deferredResultHolder.placeOrder(orderNumber, deferredResult);

        // Asynchronously send an order message to MQ, assuming 200ms
        new Thread(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(500);
                synchronized (orderProcessingQueue) {
                    while (orderProcessingQueue.size() >= Integer.MAX_VALUE) {
                        try {
                            orderProcessingQueue.wait();
                        } catch (Exception e) {
                        }
                    }
                    orderProcessingQueue.addLast(orderNumber);
                    orderProcessingQueue.notifyAll();
                }
                logger.info("Send an order message to MQ with the order number {}", orderNumber);
            } catch (InterruptedException e) {
                throw newRuntimeException(e); }},"Local temporary thread - send order message to MQ")
        .start();

        logger.info("[Request thread] continues to process other requests");

        // Instead of serializing deferredResult to JSON immediately and returning it to the client, deferredResult will wait for setResult in deferredResult to be called and return the result as JSON
        returndeferredResult; }}Copy the code

Two MQ

package top.zhenganwen.securitydemo.web.async;

import org.springframework.stereotype.Component;

import java.util.LinkedList;

/ * * *@author zhenganwen
 * @date 2019/8/22
 * @descOrderProcessingQueue Order message MQ */
@Component
public class OrderProcessingQueue extends LinkedList<String> {}Copy the code
package top.zhenganwen.securitydemo.web.async;

import org.springframework.stereotype.Component;

import java.util.LinkedList;

/ * * *@author zhenganwen
 * @date 2019/8/22
 * @descOrderCompletionQueue Order processing completes MQ */
@Component
public class OrderCompletionQueue extends LinkedList<OrderCompletionResult> {}Copy the code
package top.zhenganwen.securitydemo.web.async;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/ * * *@author zhenganwen
 * @date 2019/8/22
 * @descOrderCompletionResult Information about the completion of order processing, including the order number and success */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderCompletionResult {
    private String orderNumber;
    private String result;
}
Copy the code

Credential cache

package top.zhenganwen.securitydemo.web.async;

import org.hibernate.validator.constraints.NotBlank;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.async.DeferredResult;

import javax.validation.constraints.NotNull;
import java.util.HashMap;
import java.util.Map;

/ * * *@author zhenganwen
 * @date 2019/8/22
 * @descDeferredResultHolder A credential cache for order processing results that can be retrieved at future points in time */
@Component
public class DeferredResultHolder {

    private Map<String, DeferredResult<String>> holder = new HashMap<>();

    // Place the order processing result certificate in the cache
    public void placeOrder(@NotBlank String orderNumber, @NotNull DeferredResult<String> result) {
        holder.put(orderNumber, result);
    }

    // Set the order completion result to the certificate
    public void completeOrder(@NotBlank String orderNumber, String result) {
        if(! holder.containsKey(orderNumber)) {throw new IllegalArgumentException("orderNumber not exist"); } DeferredResult<String> deferredResult = holder.get(orderNumber); deferredResult.setResult(result); }}Copy the code

Two listeners for two queues

package top.zhenganwen.securitydemo.web.async;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/ * * *@author zhenganwen
 * @date 2019/8/22
 * @desc OrderProcessResultListener
 */
@Component
public class OrderProcessingListener implements ApplicationListener<ContextRefreshedEvent> {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    OrderProcessingQueue orderProcessingQueue;

    @Autowired
    OrderCompletionQueue orderCompletionQueue;

    @Autowired
    DeferredResultHolder deferredResultHolder;

    // This method is executed when the Spring container is started or refreshed
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {

        // When the system starts, start a thread that listens for MQ order completion messages
        new Thread(() -> {

            while (true) {
                String finishedOrderNumber;
                OrderCompletionResult orderCompletionResult;
                synchronized (orderCompletionQueue) {
                    while (orderCompletionQueue.isEmpty()) {
                        try {
                            orderCompletionQueue.wait();
                        } catch (InterruptedException e) { }
                    }
                    orderCompletionResult = orderCompletionQueue.pollFirst();
                    orderCompletionQueue.notifyAll();
                }
                finishedOrderNumber = orderCompletionResult.getOrderNumber();
                logger.info("Received order processing completion message with order number: {}", finishedOrderNumber); deferredResultHolder.completeOrder(finishedOrderNumber, orderCompletionResult.getResult()); }},"Local listening thread - Listening order processing completed")
        .start();


        // Suppose the order system is listening to the thread of MQ order messages
        new Thread(() -> {

            while (true) {
                String orderNumber;
                synchronized (orderProcessingQueue) {
                    while (orderProcessingQueue.isEmpty()) {
                        try {
                            orderProcessingQueue.wait();
                        } catch (InterruptedException e) {
                        }
                    }
                    orderNumber = orderProcessingQueue.pollFirst();
                    orderProcessingQueue.notifyAll();
                }

                logger.info("Received order request, start executing order logic with order number: {}", orderNumber);
                boolean status;
                // simulate the execution of order logic
                try {
                    TimeUnit.SECONDS.sleep(2);
                    status = true;
                } catch (Exception e) {
                    logger.info("Order failed =>{}", e.getMessage());
                    status = false;
                }
                // Send a message to order completion MQ
                synchronized (orderCompletionQueue) {
                    orderCompletionQueue.addLast(new OrderCompletionResult(orderNumber, status == true ? "success" : "error"));
                    logger.info("Send order completion message, tracking number: {}",orderNumber); orderCompletionQueue.notifyAll(); }}},"Order system thread - Listening for order messages") .start(); }}Copy the code

test

The 2019-08-22 13:22:05. 21208-520 the INFO [nio - 8080 - exec - 2] T.Z.S.W eb. Async. AsyncOrderController: Received order request request thread 】 【 2019-08-22 13:22:05. 21208-521 the INFO [nio - 8080 - exec - 2] T.Z.S.W eb. The async. AsyncOrderController: Continue to process other request request thread 】 【 13:22:06 2019-08-22. 21208-022 the INFO/order system thread - listening order news T.Z.S.W eb. The async. OrderProcessingListener: After receiving the order request, start to execute the order logic. The order number is: 104691998710 the 2019-08-22 13:22:06. 21208-022 the INFO [to temporary thread - send orders to the MQ message] T.Z.S.W eb. The async. AsyncOrderController: Send an order message to MQ, tracking number: 104691998710 the 2019-08-22 13:22:08. 21208-023 the INFO/order system thread - listening order news T.Z.S.W eb. The async. OrderProcessingListener: Send order completion message, tracking number: 104691998710 the 2019-08-22 13:22:08. 21208-023 the INFO/local monitoring thread - to monitor the order processing complete T.Z.S.W eb. Async. OrderProcessingListener: Received the order processing completion message, the order number is 104691998710Copy the code

Configu reSync asynchronously handles interceptions, timeouts, and thread pool configurations

Before we extended WebMvcConfigureAdapter subclass WebConfig, we could do some configuration for asynchronous processing by overriding the configureAsyncSupport method

registerCallableInterceptors & registerDeferredResultInterceptors

The interceptors we registered by overriding the addInterceptors method are invalid for Callable and DeferredResult asynchronous processing. If you want to configure interceptors for both, you need to override them

setDefaultTimeout

Set the timeout period for asynchronous processing. If the timeout period is exceeded, the asynchronous task will respond directly without waiting for the end of the asynchronous task

setTaskExecutor

By default, SpringBoot executes asynchronous tasks by creating a new thread, after which the thread is destroyed. To execute asynchronous tasks by re-using threads (thread pools), you can pass in a custom thread pool

Front end separation

Swagger interface documentation

Swagger project can automatically generate interface documents according to the interfaces we write, which is convenient for us to separate the development of the front and back ends

Rely on

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.7.0</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.7.0</version>
</dependency>
Copy the code

Add the @@enablesWagger2 annotation to the bootstrap class SecurityDemoApplication to enable the automatic generation of interface documents, and then access localhost:8080/ swagger-uI.html

Commonly used annotations

  • The @apiOperation annotation on the Controller method describes the behavior of the method

    @GetMapping
    @JsonView(User.UserBasicView.class)
    @ApiOperation(User Enquiry Service)
    public List<User> query(String userName, UserQueryConditionDto userQueryConditionDto, Pageable pageable) {
    Copy the code
  • The @apiModelProperty annotation on the Bean field describes the meaning of the field

    @Data
    public class UserQueryConditionDto {
    
        @ApiModelProperty("Username")
        private String username;
        @ApiModelProperty("Password")
        private String password;
        @ApiModelProperty("Phone number")
        private String phone;
    }
    Copy the code
  • The @apiParam annotation on the Controller method parameter describes the meaning of the parameter

    @DeleteMapping("/{id:\\d+}")
    public void delete(@ApiParam("User id") @PathVariable Long id) {
        System.out.println(id);
    }
    Copy the code

The interface document is regenerated after the restart

WireMock

To facilitate the parallel development of the front and back end, we can use WireMock as a virtual interface server

When the back-end interface is not developed, the front-end may forge some static data (such as JSON files) as the response result of the request in a local file, which is fine when there is only one front-end terminal. However, when there are multiple front-end terminals, such as PC, H5, APP, small program, etc., each of them forges data in its own local, so it will be a bit repetitive. Moreover, each person forges data according to his own idea, which may lead to the ultimate failure of seamless connection with the real interface

At this time, the appearance of Wiremock solves this pain point. Wiremock is an independent server developed in Java, which can provide HTTP services externally. We can edit/configure the Wiremock server through the Wiremock client, so that it can provide various interfaces like Web services. There is no need to redeploy

Download & Start the Wiremock service

Wiremock can be run in jar mode, download the url, switch to its directory when the download is complete, run CMD to start the Wiremock server, –port= specify the port to run

Java jar wiremock - standalone - 2.24.1. Jar - port = 8062Copy the code

Rely on

Introduces wiremock client dependencies and their dependent HttpClient

<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
</dependency>
Copy the code

Since dependent automatic compatibility is already used in the parent project, there is no need to specify a version number. The Wiremock server is then edited through the client API to add an interface to it

package top.zhenganwen.securitydemo.wiremock;

import static com.github.tomakehurst.wiremock.client.WireMock.*;

/ * * *@author zhenganwen
 * @date 2019/8/22
 * @desc MockServer
 */
public class MockServer {

    public static void main(String[] args) {
        configureFor("127.0.0.1".8062);
        removeAllMappings();    // Remove all old configurations

        // Add the configuration that a stub represents an interface
        stubFor(
                get(urlEqualTo("/order/1")).
                        // Set the response result
                        willReturn(
                                aResponse()
                                        .withBody("{\"id\":1,\"orderNumber\":\"545616156\"}")
                                        .withStatus(200))); }}Copy the code

You can store the JSON data in resources first and then read it as a string using ClassPathResource#getFile and Fileutills #readLines

Access localhost: 8062 / order / 1:

{
    id: 1,
    orderNumber: "545616156"
}
Copy the code

With the WireMockAPI, you can configure a variety of interface services for a virtual server

Use Spring Security to develop form-based authentication

Summary

Spring Security core features

  • Certification (Who are you)
  • Empower (What can you do)
  • Attack protection (prevent fake identity, if the hacker can fake identity to log in to the system, the above two functions will not work)

This chapter content

  • Spring Security fundamentals
  • Implement user name + password authentication
  • Authentication using mobile phone number + SMS

First impressions of Spring Security

Security has a default base authentication mechanism, we comment out the configuration item security.basic.enabled=false (default is true), restart the view log will find a message

Using default security password: f84e3dea-d231-47a2-b20a-48bac8ed5f1e
Copy the code

Use default security password to log in to GET /user: F84e3dea-d231-47a2-b20a-48bac8ed5f1e user (the password will be regenerated every time we restart). After we use these two login forms, the page is redirected to the service we want to access

formLogin

From this section we will start atsecurity-browserModule to write our browser authentication logic

We can through the way of adding Configuration class (add the Configuration, and expand WebSecurityConfigureAdapter) to configure authentication way, the validation logic, etc., such as set verification method for form validation:

package top.zhenganwen.securitydemo.browser.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/ * * *@author zhenganwen
 * @date 2019/8/22
 * @desc SecurityConfig
 */
@Configuration
public class SecurityBrowserConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // Set the authentication mode to form login, and jump to form login page if accessing protected URL without login (security wrote a default login page for us)
            .formLogin()
            // Add additional configurations
            .and()
            // Authentication mode Configuration ends. Authentication rules are to be configured
            .authorizeRequests()
            // Set any request to be authenticated.anyRequest() .authenticated(); }}Copy the code

Access /user and jump to the default login page /login (the login page and login URL can be customized). The user name and password are still in the log. If the login succeeds, jump to /user

httpBasic

If the authentication mode is changed from formLogin to httpBasic, then the default configuration of Security is (equivalent to introducing the security dependency), i.e., pop-up login box

Spring Security fundamentals

Three filters

As you can see, Spring Security is essentially a chain of filters at its core, so it’s non-invasive and pluggable. There are three types of filters in the filter chain:

  • Authentication filters XxxAuthenticationFilter, as shown in green, whose class name ends with AuthenticationFilter, are used to save login information. These filters are based on our configuration dynamic effect, as we call before formLogin () is actually enabled UsernamePasswordAuthenticationFilter, Call httpBaisc () is to enable the BasicAuthenticationFilter

    Behind the two filters ExceptionTranslationFilter and FilterSecurityInterceptor closest to the Controller contains the core of the authentication logic, is enabled by default, and we also cannot disable them

  • FilterSecurityInterceptor, although the name ends in Interceptor, but actually is a Filter, it is the most close to the Controller of a Filter, it will be based on our configuration of blocking rule (what URL need to be logged in to access, Which URL requires some specific permissions to access, etc.) to access the corresponding URL request to intercept, the following is part of its source code

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        FilterInvocation fi = new FilterInvocation(request, response, chain);
        invoke(fi);
    }
    
    public void invoke(FilterInvocation fi) throws IOException, ServletException {... InterceptorStatusToken token =super.beforeInvocation(fi); . fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); . }Copy the code

    The doFilter is actually calling our Controller (since it is the end of the filter chain), but before that it calls the beforeInvocation invocation to intercept the request for identity and permission, Check failure corresponds to sell without certification (Unauthenticated) and Unauthorized anomaly (Unauthorized), these exceptions will be ExceptionTranslationFilter captured

  • ExceptionTranslationFilter, just as its name implies is analysis abnormal, some of its source code is as follows

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
    
        try {
            chain.doFilter(request, response);
        }
        catch (Exception ex) {
            // Try to extract a SpringSecurityException from the stacktrace. }}Copy the code

    It calls the chain. The doFilter is actually to FilterSecurityInterceptor, Thrown in. It will be for FilterSecurityInterceptor doFilter SpringSecurityException abnormal capture and analytical processing, Raise the Unauthenticated abnormalities, such as FilterSecurityInterceptor ExceptionTranslationFilter would be redirected to the login page or the pop-up login box (depending on what we configure the authentication filter), when we successfully logged in, Authentication filtering redirects us to the URL we originally wanted to visit

Breakpoint debugging

We can verify this with breakpoint debugging, setting the validation mode to formLogin, and then restarting the service access /user in the three filters and Controller respectively

Custom user authentication logic

Process the user information retrieval logic — UserDetailsService

So far we have logged in using user and the password generated by the startup log. This is security with a built-in user user. In actual projects, we usually have a table for storing users, which can read user information through JDBC or other storage systems. In this case, we need to define the logic for reading user information. By implementing the UserDetailsService interface, we can tell security how to obtain user information from it

package top.zhenganwen.securitydemo.browser.config;

import org.hibernate.validator.constraints.NotBlank;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.authority.AuthorityUtils;
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.Component;

import java.util.Objects;

/ * * *@author zhenganwen
 * @date 2019/8/23
 * @desc CustomUserDetailsService
 */
@Component
public class CustomUserDetailsService implements UserDetailsService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public UserDetails loadUserByUsername(@NotBlank String username) throws UsernameNotFoundException {
        logger.info("Login username:" + username);
        // In a real project you can call Dao or Repository to check whether the user exists
        if (Objects.equals(username, "admin") = =false) {
            throw new UsernameNotFoundException("User name does not exist");
        }
        
        // After the User is queried, the relevant information needs to be wrapped up as a UserDetails instance and returned to Security, where User is an implementation provided by Security
        // The third parameter needs to be passed a permission set. Here, a utility class provided by Security is used to convert semicolon-delimited permission strings into permission sets, which should be queried from the user permission table
        return new org.springframework.security.core.userdetails.User(
                "admin"."123456", AuthorityUtils.commaSeparatedStringToAuthorityList("user,admin")); }}Copy the code

After restarting the service, you can only log in using admin,123456

Handle user validation logic — UserDetails

Let’s take a look at the UserDetails interface source code

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    // It is used to compare with the password entered when the user logs in
    String getPassword(a);

    String getUsername(a);

    // Whether the account is not expired
    boolean isAccountNonExpired(a);

    // Whether the account is unfrozen
    boolean isAccountNonLocked(a);

    // The password is not expired. Some systems with high security require the account to change the password at regular intervals
    boolean isCredentialsNonExpired(a);

    // If the account is available, the field can be logically deleted
    boolean isEnabled(a);
}
Copy the code

When overriding the four methods starting with is, return true if no judgment is required, for example, the entity class of the corresponding user table is as follows

@Data
public class User{
    private Long id;
    private String username;
    private String password;
    private String phone;
    private int deleted;			//0-" normal ", 1-" deleted"
    private int accountNonLocked;	 //0-" account is not frozen ", 1-" Account is frozen"
}
Copy the code

For convenience, we can implement the UserDetails interface directly using the entity class

@Data
public class User implements UserDetails{
    private Long id;
    private String uname;
    private String pwd;
    private String phone;
    private int deleted;			
    private int accountNonLocked;

    public String getPassword(a){
        return pwd;
    }

    public String getUsername(a){
        return uname;
    }

    public boolean isAccountNonExpired(a){
        return true;
    }

    public boolean isAccountNonLocked(a){
        return accountNonLocked == 0;
    }

    public boolean isCredentialsNonExpired(a){
        return true;
    }

    public boolean isEnabled(a){
        return deleted == 0; }}Copy the code

Handle password encryption and decryption — PasswordEncoder

Generally, the password field in the user table will not store the plain text of the password but the encrypted ciphertext, so we need the support of PasswordEncoder:

public interface PasswordEncoder {
	String encode(CharSequence rawPassword);
	boolean matches(CharSequence rawPassword, String encodedPassword);
}
Copy the code

When we insert the user into the database, we need to call encode to encrypt the plaintext password and then insert; When the user logs in, Security calls matches to match the ciphertext we retrieved from the database with the plaintext password the user submitted.

Security provides a BCryptPasswordEncoder (BCryptPasswordEncoder) for this interface. We only need to configure a Bean of this class, and security will think that the password returned by the getPassword of UserDetails we return is encrypted by the Bean (so when inserting the user, pay attention to call encode of this Bean to encrypt the password before inserting the database)

@Configuration
public class SecurityBrowserConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder passwordEncoder(a) {
        return new BCryptPasswordEncoder();
    }
Copy the code
@Component
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    BCryptPasswordEncoder passwordEncoder;

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public UserDetails loadUserByUsername(@NotBlank String username) throws UsernameNotFoundException {
        logger.info("Login username:" + username);
        // In a real project you can call Dao or Repository to check whether the user exists
        if (Objects.equals(username, "admin") = =false) {
            throw new UsernameNotFoundException("User name does not exist");
        }
        // Suppose the password is as follows
        String pwd = passwordEncoder.encode("123456");
        
        return new org.springframework.security.core.userdetails.User(
                "admin", pwd, AuthorityUtils.commaSeparatedStringToAuthorityList("user,admin")); }}Copy the code

BCryptPasswordEncoder is not necessarily only used for password encryption and verification. In daily development, we can use encode method or matches method to compare whether a ciphertext is the result of a plaintext encryption

Personalize the user authentication process

Customize the login page

Use loginPage() after formLogin() to specify the loginPage, and remember to let go of the URL blocking; UsernamePasswordAuthenticationFilter default interceptor submitted to/login POST request and obtain the login information, if you want to form to fill in the action is not as/POST, You can configure loginProcessingUrl UsernamePasswordAuthenticationFilter corresponding with it

@Configuration
public class SecurityBrowserConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder passwordEncoder(a) {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // Set the authentication mode to form login, and jump to form login page if accessing protected URL without login (security wrote a default login page for us)
                .formLogin()
                .loginPage("/sign-in.html").loginProcessingUrl("/auth/login")
                .and()
                // Authentication mode Configuration ends. Authentication rules are to be configured
                .authorizeRequests()
                    The login page does not need to be blocked
                    .antMatchers("/sign-in.html").permitAll()
                    // Set any request to be authenticated.anyRequest().authenticated(); }}Copy the code

Custom login page: security – browser/SRC/main/resource/resources/sign – in the HTML


      
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>The login page</title>
</head>
<body>
<form action="/auth/login" method="post">User name:<input type="text" name="username">Password:<input type="password" name="password">
    <button type="submit">submit</button>
</form>
</body>
</html>
Copy the code

After the restart, I went to GET /user and changed to the login page sign-in. HTML we wrote and filled in admin,123456 to log in

There was an unexpected error (type=Forbidden, status=403).
Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'.
Copy the code

This is because Security has cross-site forgery request protection (CSRF) enabled by default (such login requests can also be made using HTTP client Postman, for example), and we disable it first

http
                .formLogin()
                .loginPage("/sign-in.html").loginProcessingUrl("/auth/login")
                .and()
                .authorizeRequests()
                    .antMatchers("/sign-in.html").permitAll()
                    .anyRequest().authenticated()
                .and()
                    .csrf().disable()
Copy the code

Restart the access to GET /user. After the login, the system automatically switches back to /user, and the customized login page succeeds

REST Login logic

Since we are a REST-based service, if it is a non-browser request, we should return the 401 status code telling the client that authentication is required, rather than redirecting to the login page

So instead of writing the loginPage as a loginPage path, we should redirect it to a Controller, and the Controller can tell if the user is going to the page from the browser or from a non-browser like android when it’s going to the REST service, if it’s going to the loginPage, If it is the latter, it responds with the 401 status code and JSON message

package top.zhenganwen.securitydemo.browser;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import top.zhenganwen.securitydemo.browser.support.SimpleResponseResult;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/ * * *@author zhenganwen
 * @date 2019/8/23
 * @desc AuthenticationController
 */
@RestController
public class BrowserSecurityController {

    private Logger logger = LoggerFactory.getLogger(getClass());

    // Security stores pre-jump requests in the session
    private RequestCache requestCache = new HttpSessionRequestCache();

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @RequestMapping("/auth/require")
    // This annotation sets the response status code
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public SimpleResponseResult requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {

        // Retrieve from session the URL accessed by the user before the jump
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if(savedRequest ! =null) {
            String redirectUrl = savedRequest.getRedirectUrl();
            logger.info("The request that triggers the jump to /auth/login is: {}", redirectUrl);
            if (StringUtils.endsWithIgnoreCase(redirectUrl, ".html")) {
                / / if the user is blocked by FilterSecurityInterceptor to access the HTML page jump to the auth/login and then redirected to the login page
                redirectStrategy.sendRedirect(request, response, "/sign-in.html"); }}// If a redirect to /auth/login instead of HTML is intercepted, a JSON message is returned
        return new SimpleResponseResult("User is not logged in. Please direct user to login page."); }}Copy the code
@Configuration
public class SecurityBrowserConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder passwordEncoder(a) {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http  
                .formLogin()
                .loginPage("/auth/require").loginProcessingUrl("/auth/login")
                .and()
                .authorizeRequests()
                    .antMatchers("/auth/require").permitAll()
                    .antMatchers("/sign-in.html").permitAll() .anyRequest().authenticated() .and() .csrf().disable(); }}Copy the code

Refactoring – Configure instead of Hardcode

Since our security-Browser module was developed as a reusable module, it should support custom configurations, such as when other applications introduce our security-Browser module, they should be able to configure their own login pages. If they’re not configured then use the sign-in. HTML that we provide by default, and to do that we need to provide some configuration items, Following the introduction of our security – browser such as others by adding demo. Security. The loginPage = / login HTML to their projects. The login HTML replace our sign – in. HTML

Since security-app may need to support similar configurations in the future, we define a general configuration class in security-Core to encapsulate the different configuration items of each module

Security-core classes:

package top.zhenganwen.security.core.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/ * * *@author zhenganwen
 * @date 2019/8/23
 * @descSecurityProperties encapsulates the configuration items */ for each module of the entire project
@Data
@ConfigurationProperties(prefix = "demo.security")
public class SecurityProperties {
    private BrowserProperties browser = new BrowserProperties();
}
Copy the code
package top.zhenganwen.security.core.properties;

import lombok.Data;

/ * * *@author zhenganwen
 * @date 2019/8/23
 * @descBrowserProperties encapsulates the security-Browser configuration item */
@Data
public class BrowserProperties {
    private String loginPage = "/sign-in.html";	// Provide a default login page
}
Copy the code
package top.zhenganwen.security.core;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import top.zhenganwen.security.core.properties.SecurityProperties;

/ * * *@author zhenganwen
 * @date 2019/8/23
 * @desc SecurityCoreConfig
 */
@Configuration
// Enable configuration items with the demo.security prefix in application.properties to be injected into SecurityProperties at startup
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {}Copy the code

Then in the heart of the security – browser SecurityProperties injection to come in, will be redirected to the login page of logic in the configuration file is dependent on the demo. Security. The loginPage

@RestController
public class BrowserSecurityController {

    private Logger logger = LoggerFactory.getLogger(getClass());
    private RequestCache requestCache = new HttpSessionRequestCache();
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Autowired
    private SecurityProperties securityProperties;

    @RequestMapping("/auth/require")
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public SimpleResponseResult requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {

        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if(savedRequest ! =null) {
            String redirectUrl = savedRequest.getRedirectUrl();
            logger.info("The request that triggers the jump to /auth/login is: {}", redirectUrl);
            if (StringUtils.endsWithIgnoreCase(redirectUrl, ".html")) { redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage()); }}return new SimpleResponseResult("User is not logged in. Please direct user to login page."); }}Copy the code

Set the unblocked login page URL to dynamic

@Configuration
public class SecurityBrowserConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder passwordEncoder(a) {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .formLogin()
                .loginPage("/auth/require").loginProcessingUrl("/auth/login")
                .and()
                .authorizeRequests()
                    .antMatchers("/auth/require").permitAll()
            		// Set the unblocked login page URL to dynamic.antMatchers(securityProperties.getBrowser().getLoginPage()).permitAll() .anyRequest().authenticated() .and() .csrf().disable(); }}Copy the code

For now, we treat the security-Demo module as a third-party application, using the reusable security-Browser

Above all, want to start classes of security – demo module SecurityDemoApplication moved to the top. The zhenganwen. Securitydemo package, Make sure you are able to scan to the security – the core of the top. Zhenganwen. Securitydemo. Core. SecurityCoreConfig and security – top under the browser. Zhenganwen. Securitydemo. browser.SecurityBrowserConfig

And then, On the security – the demo application. The properties to add configuration items demo. Security. The loginPage = / login HTML and new resources folder under the resources and the logi N.h HTML:


      
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>Security Login page of the Demo application</h1>
<form action="/auth/login" method="post">User name:<input type="text" name="username">Password:<input type="password" name="password">
    <button type="submit">submit</button>
</form>
</body>
</html>
Copy the code

Restart the service, access /user. HTML found the redirect to login. HTML; Comment out the demo. Security. The loginPage = / login HTML, then restart the service access/user. The HTML found jump to sign – in. HTML, refactoring success!

The custom login successfully processed – AuthenticationSuccessHandler

The default logic for Security to handle a successful login is to redirect to a previously intercepted request, but for a REST service where the front end might be an AJAX request for a login and the expected response is information about the user, it’s not appropriate to redirect it. To the custom login success after processing, we need to implement AuthenticationSuccessHandler interface

package top.zhenganwen.securitydemo.browser.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/ * * *@author zhenganwen
 * @date 2019/8/24
 * @desc CustomAuthenticationSuccessHandler
 */
@Component("customAuthenticationSuccessHandler")
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException
            , ServletException {
        logger.info("User {} login successful", authentication.getName());
        response.setContentType("application/json; charset=utf-8"); response.getWriter().write(objectMapper.writeValueAsString(authentication)); response.getWriter().flush(); }}Copy the code

After the login is successful, we will get an Authentication, which is also a core interface of security, and its function is to encapsulate the relevant information of the user. Here, we turn it into A JSON string response to show the front-end what it contains

We also need to configure it into HttpSecurity via successHandler() for it to take effect (instead of the default logon success handling logic) :

@Configuration
public class SecurityBrowserConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder passwordEncoder(a) {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private AuthenticationSuccessHandler customAuthenticationSuccessHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .formLogin()
                .loginPage("/auth/require").loginProcessingUrl("/auth/login")
                .successHandler(customAuthenticationSuccessHandler)
                .and()
                .authorizeRequests()
                    .antMatchers("/auth/require").permitAll() .antMatchers(securityProperties.getBrowser().getLoginPage()).permitAll() .anyRequest().authenticated() .and() .csrf().disable(); }}Copy the code

Restart the service, access /login.html and login:

{
    authorities: [
        {
            authority: "admin"
        },
        {
            authority: "user"
        }
    ],
    details: {
        remoteAddress: "0:0:0:0:0:0:0:1",
        sessionId: "3BA37577BAC493D0FE1E07192B5524B1"
    },
    authenticated: true,
    principal: {
        password: null,
        username: "admin",
        authorities: [
            {
                authority: "admin"
            },
            {
                authority: "user"
            }
        ],
        accountNonExpired: true,
        accountNonLocked: true,
        credentialsNonExpired: true,
        enabled: true
    },
    credentials: null,
    name: "admin"
}
Copy the code

You can see that Authentication contains the following information

  • authorities, permission, corresponding toUserDetialsIn thegetAuthorities()Return result of
  • details, the IP address of the client and the SESSIONID of the current call
  • authenticated, whether the authentication succeeds
  • principle, corresponding toUserDetailsServiceIn theloadUserByUsernameThe returnedUserDetails
  • credentialsThe password,securityBy default, the password is not returned to the front end
  • nameThe user name

Here, because we are logging in with a form, the above information is returned. Later, we will log in with a third party, such as wechat and QQ, so the information contained in Authentication may be different. That is, the onAuthenticationSuccess input parameter of the overridden onAuthenticationSuccess method will pass us a different Authentication implementation object depending on the login method

The custom login failed – AuthenticationFailureHandler processing

You can also customize the processing of login failure

package top.zhenganwen.securitydemo.browser.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/ * * *@author zhenganwen
 * @date 2019/8/24
 * @desc CustomAuthenticationFailureHandler
 */
@Component("customAuthenticationFailureHandler")
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        logger.info("Login failed =>{}", exception.getMessage());
        response.setContentType("application/json; charset=utf-8"); response.getWriter().write(objectMapper.writeValueAsString(exception)); response.getWriter().flush(); }}Copy the code
@Configuration
public class SecurityBrowserConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder passwordEncoder(a) {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private AuthenticationSuccessHandler customAuthenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler customAuthenticationFailureHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .formLogin()
                    .loginPage("/auth/require")
                    .loginProcessingUrl("/auth/login")
                    .successHandler(customAuthenticationSuccessHandler)
                    .failureHandler(customAuthenticationFailureHandler)
                .and()
                .authorizeRequests()
                    .antMatchers("/auth/require").permitAll() .antMatchers(securityProperties.getBrowser().getLoginPage()).permitAll() .anyRequest().authenticated() .and() .csrf().disable(); }}Copy the code

Access /login.html enter incorrect password to login:

{ cause: null, stackTrace: [...] , localizedMessage: "bad credentials ", message:" bad credentials ", suppressed: []}Copy the code

refactoring

In order to make the security – browser reusable modules, we should keep the login success/failure handling strategies pulled out, let the third party applications the freedom to choose, then we can add a configuration items demo. Security. The loginProcessType

Switch to the security – the core:

package top.zhenganwen.security.core.properties;

/ * * *@author zhenganwen
 * @date 2019/8/24
 * @desc LoginProcessTypeEnum
 */
public enum LoginProcessTypeEnum {
	// Redirect to the previous request page or login failed page
    REDIRECT("redirect"), 
    // User information is displayed if the login succeeds, and error information is displayed if the login fails
    JSON("json");

    private String type;

    LoginProcessTypeEnum(String type) {
        this.type = type; }}Copy the code
@Data public class BrowserProperties { private String loginPage = "/sign-in.html"; private LoginProcessTypeEnum loginProcessType = LoginProcessTypeEnum.JSON; // Default return JSON information}Copy the code

Refactoring the login success/failure handler, SavedRequestAwareAuthenticationSuccessHandler and SimpleUrlAuthenticationFailureHandler is security provides the default login success (before you jump to the login request page) and Handler that failed to log on (jump to abnormal page)

package top.zhenganwen.securitydemo.browser.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import top.zhenganwen.security.core.properties.LoginProcessTypeEnum;
import top.zhenganwen.security.core.properties.SecurityProperties;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/ * * *@author zhenganwen
 * @date 2019/8/24
 * @desc CustomAuthenticationSuccessHandler
 */
@Component("customAuthenticationSuccessHandler")
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException
            , ServletException {
        if (securityProperties.getBrowser().getLoginProcessType() == LoginProcessTypeEnum.REDIRECT) {
            // Redirect to the pre-login request URL cached in session
            super.onAuthenticationSuccess(request, response, authentication);
            return;
        }
        logger.info("User {} login successful", authentication.getName());
        response.setContentType("application/json; charset=utf-8"); response.getWriter().write(objectMapper.writeValueAsString(authentication)); response.getWriter().flush(); }}Copy the code
package top.zhenganwen.securitydemo.browser.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import top.zhenganwen.security.core.properties.LoginProcessTypeEnum;
import top.zhenganwen.security.core.properties.SecurityProperties;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/ * * *@author zhenganwen
 * @date 2019/8/24
 * @desc CustomAuthenticationFailureHandler
 */
@Component("customAuthenticationFailureHandler")
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        if (securityProperties.getBrowser().getLoginProcessType() == LoginProcessTypeEnum.REDIRECT) {
            super.onAuthenticationFailure(request, response, exception);
            return;
        }
        logger.info("Login failed =>{}", exception.getMessage());
        response.setContentType("application/json; charset=utf-8"); response.getWriter().write(objectMapper.writeValueAsString(exception)); response.getWriter().flush(); }}Copy the code

Login to /login. HTML, test login success and login failure respectively, and return JSON response

On the security – in the demo

  • Add the demo application. The properties. The security. The loginProcessType = redirect

  • New/resources/resources/index. HTML

    
            
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    <h1>Spring Demo application home page</h1>
    </body>
    </html>
    Copy the code
  • New/resources/resources / 401 HTML

    
            
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    <h1>login fail!</h1>
    </body>
    </html>
    Copy the code

Restart the service. If the login succeeds, go to index.html; if the login fails, go to 401.html

Certification process source code level detail

After the above two sections, we have been able to use some basic functions of Security, but they are fragmented and the overall process is still vague. Knowing how and why, we need to analyze what Security is doing for us when we log in

Authentication Process

The diagram above shows the general process of login processing, After intercepting the login request, XxxAutenticationFilter will meet the login information and encapsulate authenticated=false Authentication to AuthenticationManager for verification. The AuthenticationManager does not do the verification logic itself, and entrusts the AuthenticationProvider to do the verification. The AuthenticationProvider either throws a verification failure exception during the verification process or the verification returns a new Authentication with UserDetials, Upon receipt of XxxAuthenticationFilter, the request filter invokes the logon success handler to execute the logon success logic

As we step through the verification process with breakpoint debugging using the username and password form login, the rest of the logins are pretty much the same

How are authentication results shared among multiple requests

To share data between multiple requests, you need sessions. Let’s see what security puts into the session and when it reads from the session

Last said in AbstractAuthenticationProcessingFilter ` ` doFilter method, After verification, successfulAuthentication(request, response, chain, authResult) is called. What does this do

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
    throws IOException, ServletException {

    if (logger.isDebugEnabled()) {
        logger.debug("Authentication success. Updating SecurityContextHolder to contain: "+ authResult); } SecurityContextHolder.getContext().setAuthentication(authResult); . successHandler.onAuthenticationSuccess(request, response, authResult); }Copy the code

Can be found in the call log in successfully before the processing logic processor called the SecurityContextHolder. GetContext () setAuthentication (authResult), View the knowable SecurityContextHolder. GetContext () is to get the current thread binding SecurityContext (thread can be seen as a variable, the scope for the life cycle of a thread), SecurityContext is just a wrapper around Authentication

public class SecurityContextHolder {
	private static SecurityContextHolderStrategy strategy;
	public static SecurityContext getContext(a) {
		returnstrategy.getContext(); }}Copy the code
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
    private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<SecurityContext>();
	public SecurityContext getContext(a) {
		SecurityContext ctx = contextHolder.get();
		if (ctx == null) {
			ctx = createEmptyContext();
			contextHolder.set(ctx);
		}
		returnctx; }}Copy the code
public interface SecurityContext extends Serializable {
	Authentication getAuthentication(a);
	void setAuthentication(Authentication authentication);
}
Copy the code
public class SecurityContextImpl implements SecurityContext {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
    
	public Authentication getAuthentication(a) {
		return authentication;
	}

	public int hashCode(a) {
		if (this.authentication == null) {
			return -1;
		}
		else {
			return this.authentication.hashCode(); }}public void setAuthentication(Authentication authentication) {
		this.authentication = authentication; }... }Copy the code

So what is the purpose of saving Authentication in the SecurityContext of the current thread?

This goes to another special filter SecurityContextPersistenceFilter, it is at the forefront of the security of the whole filter chain:

private SecurityContextRepository repo;
// The first filter that the request arrives at
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
    throws IOException, ServletException {... HttpRequestResponseHolder holder =new HttpRequestResponseHolder(request,response);
    // Get the SecurityContext from Session, empty if not logged in
    SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

    try {
        // Save the SecurityContext to the ThreadLocalMap of the current thread
        SecurityContextHolder.setContext(contextBeforeChainExecution);
	   // Execute subsequent filter and Controller methods
        chain.doFilter(holder.getRequest(), holder.getResponse());

    }
    // The last filter to pass through when requesting a response
    finally {
        // Get the SecurityContext from the current thread
        SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
        SecurityContextHolder.clearContext();
        // Persist the SecurityContext to Sessionrepo.saveContext(contextAfterChainExecution, holder.getRequest(),holder.getResponse()); . }}Copy the code
public class HttpSessionSecurityContextRepository implements SecurityContextRepository {...public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
		HttpServletRequest request = requestResponseHolder.getRequest();
		HttpServletResponse response = requestResponseHolder.getResponse();
		HttpSession httpSession = request.getSession(false); SecurityContext context = readSecurityContextFromSession(httpSession); .returncontext; }... }Copy the code

Obtain information about an authentication user

In our code by static method SecurityContextHolder. GetContext () getAuthentication to obtain user information, or can be directly in the Controller into the statement Authentication, Security will do it for you automatically. If you just want to get the part of Authentication that corresponds to UserDetails, use @authenticationPrinciple UserDetails currentUser

@GetMapping("/info1")
public Object info1(a) {
    return SecurityContextHolder.getContext().getAuthentication();
}
@GetMapping("/info2")
public Object info2(Authentication authentication) {
    return authentication;
}
Copy the code

GET /user/info1

{
    authorities: [
        {
            authority: "admin"
        },
        {
            authority: "user"
        }
    ],
    details: {
        remoteAddress: "0:0:0:0:0:0:0:1",
        sessionId: "24AE70712BB99A969A5C56907C39C20E"
    },
    authenticated: true,
    principal: {
        password: null,
        username: "admin",
        authorities: [
            {
                authority: "admin"
            },
            {
                authority: "user"
            }
        ],
        accountNonExpired: true,
        accountNonLocked: true,
        credentialsNonExpired: true,
        enabled: true
    },
    credentials: null,
    name: "admin"
}
Copy the code
@GetMapping("/info3")
public Object info3(@AuthenticationPrincipal UserDetails currentUser) {
    return currentUser;
}
Copy the code

GET /user/info3

{
    password: null,
    username: "admin",
    authorities: [
        {
            authority: "admin"
        },
        {
            authority: "user"
        }
    ],
    accountNonExpired: true,
    accountNonLocked: true,
    credentialsNonExpired: true,
    enabled: true
}
Copy the code

The resources

  • Video tutorial

    Link: pan.baidu.com/s/1wQWD4wE0… Extraction code: Z6ZI