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 through
URL
To add string and query parameters that indicate interface behavior, such as/user/get? username=xxx
Restful
The style recommends that a URL represent a system resource,/user/1
Should indicate access to the systemid
The user is 1
- The traditional way is usually through
- Request way
- The traditional way is generally through
get
Submission, the downsideget
The 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 usedpost
submit Restful
Style favors the use of submission to describe request behavior, such asPOST
,DELETE
,PUT
,GET
Respond to requests to add, delete, change, or check the type
- The traditional way is generally through
- 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
Restful
Style advocate useJSON
As 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 as200
Indicates that the request was successfully processed,404
Indicates that the corresponding resource is not found.500
Indicates 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 callperform
Specifying the interface addressMockMvcRequestBuilders
, construct the request (including request path, submission method, request header, request body, etc.)MockMvcRequestBuilders
Assert 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
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
- Custom exception handling classes need to be added
@ControllerAdvice
- Used in an exception handling method
@ExceptionHandler
Declare what exceptions the method intercepts, all of themController
If one of these exceptions is thrown, the method is converted to execute - The caught exception is used as an input parameter to the method
- Method returns the result of and
Controller
Method returns the same meaning if requiredjson
You need to add it to the method@ResponseBody
Annotation, 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,Filter
Interface 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,FilterRegistrationBean
This step is equivalent to the traditional way inweb.xml
Add 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
Filter
It’s request-based,Interceptor
Is based onController
Multiple requests may be executed at a timeController
(by forwarding), so a request is executed only onceFilter
But it may be executed multiple timesInterceptor
Interceptor
isSpringMVC
Component in, so it knowsController
Can get relevant information (such as the method to which the request is mapped, the method of thebean
Etc.)
Using the interceptor provided by SpringMVC also requires two steps
1, implementation,HandlerInterceptor
interface
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
- will
preHandle
Return 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
- add
- Writing pointcuts, which can be done using annotations, consists of two parts: which methods need to be enhanced and when
- Cut to the timing
@Before
Before the method is executed@AfterReturning
After the method is executed normally@AfterThrowing
Method throws an exception@After
The method is successfully executedreturn
Before, equivalent to inreturn
A paragraph was inserted beforefinally
@Around
, can use the injected input parameterProceedingJoinPoint
Flexible implementation of the above four timing, its role with the interceptor methodhandler
Similar, but with more useful runtime information
- Pointcut, you can use
execution
For details, see:Docs. Spring. IO/spring/docs…
- Cut to the timing
- Write enhancements,
- Only one
@Around
You can get in, you can getProceedingJoinPoint
The instance - By calling the
ProceedingJoinPoint
thepoint.proceed()
You can call the corresponding Controller method and get the return value
- Only one
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-browser
Module 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 toUserDetials
In thegetAuthorities()
Return result ofdetails
, the IP address of the client and the SESSIONID of the current callauthenticated
, whether the authentication succeedsprinciple
, corresponding toUserDetailsService
In theloadUserByUsername
The returnedUserDetails
credentials
The password,security
By default, the password is not returned to the front endname
The 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