личный блог:www.zhenganwen.top, в конце статьи сюрприз!
Подготовка окружающей среды
Все примеры кода в этой статье размещены в облаке кода:git ee.com/beginning/…
В конце текста сюрприз!
Среда разработки
JDK1.8
Maven
Структура проекта
-
spring-security-demo
Родительский проект, используемый для зависимостей всего проекта
-
security-core
Основной модуль аутентификации безопасности,
security-browser
иsecurity-app
основаны на нем -
security-browser
Авторизация в браузере ПК, в основном через
Session
-
security-app
Авторизация мобильной связи
-
security-demo
применение
security-browser
иsecurity-app
полагаться
spring-security-demo
Добавить кspring
Зависимость автоматически совместимых зависимостей и скомпилированных плагинов
<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>
security-core
добавить настойчивости,OAuth
сертификация,social
Сертификация иcommons
Инструменты и другие зависимости, некоторые зависимости просто добавлены для последующего использования
<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>
security-browser
Добавить кsecurity-core
и зависимости управления кластером
<dependencies>
<dependency>
<groupId>top.zhenganwen</groupId>
<artifactId>security-core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
</dependency>
</dependencies>
security-app
Добавить кsecurity-core
<dependencies>
<dependency>
<groupId>top.zhenganwen</groupId>
<artifactId>security-core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
security-demo
Временно цитируетсяsecurity-browser
Подтвердить на ПК
<artifactId>security-demo</artifactId>
<dependencies>
<dependency>
<groupId>top.zhenganwen</groupId>
<artifactId>security-browser</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
настроить
существуетsecurity-demo
Добавьте класс запуска следующим образом
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() {
return "hello spring security";
}
}
Добавить в соответствии с сообщением об ошибкеmysql
информация о соединении
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=yes&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
Временно недоступенsession
совместное использование кластера иredis
, Отключить своп
spring.session.store-type=none
@SpringBootApplication(exclude = {RedisAutoConfiguration.class,RedisRepositoriesAutoConfiguration.class})
@RestController
public class SecurityDemoApplication {
Затем я обнаружил, что он смог начать успешно, но доступ/hello
чтобы узнать, что предлагает нам войти в систему, этоSpring Security
Политика аутентификации по умолчанию работает, давайте сначала отключим ее
security.basic.enabled = false
перезапустить доступ/hello
, страница отображаетсяhello spring security
, среда была успешно построена
Restful
Спокойная VS традиция
Restful
HTTP-интерфейс - это стиль письма, а не стандарт или регулирование. использоватьRestful
Основные различия между стилем и традиционным способом заключаются в следующем.
- URL
- Традиционный способ, как правило, через
URL
Добавьте строки и параметры запроса, указывающие на поведение интерфейса, например/user/get?username=xxx
-
Restful
style рекомендует, чтобы URL-адрес представлял собой системный ресурс,/user/1
должен указывать доступ к системеid
1 пользователь
- Традиционный способ, как правило, через
- метод запроса
- Традиционный способ, как правило, через
get
Подать, недостатокget
При отправке параметры запроса будут прикреплены к URL-адресу, а URL-адрес имеет ограничение по длине, и если не выполняется специальная обработка, параметры отображаются в URL-адресе в виде простого текста, что небезопасно. Запросы, требующие двух вышеуказанных пунктов, будут использоватьpost
Отправить -
Restful
Этот стиль поддерживает использование методов отправки для описания поведения запросов, таких какPOST
,DELETE
,PUT
,GET
Должен соответствовать запросам на добавление, удаление, изменение и проверку типов
- Традиционный способ, как правило, через
- средство связи
- Традиционно ответом на запрос является страница, поэтому необходимо разработать несколько систем для разных терминалов, а логика внешнего и внутреннего интерфейса связана.
-
Restful
стиль предполагает использованиеJSON
В качестве средства связи между передней и задней частями передняя и задняя части разделены; тип результата ответа идентифицируется кодом состояния ответа, например200
Указывает, что запрос успешно обрабатывается,404
Указывает, что соответствующий ресурс не найден,500
Указывает, что сервер обрабатывает исключение.
Restful
Подробная ссылка:У-у-у. Беги, о, quilt.com/my3C note/hot water…
Расширенные функции SpringMVC и службы REST
Запуск в пакете Jar
Созданная выше среда была запущена через IDE и получила доступ/hello
, но производственная среда обычно превращает проект в исполняемыйjar
пакет, через который можно пройтиjava -jar
запустить напрямую.
На этом этапе, если мы щелкнем правой кнопкой мыши родительский проект, чтобы запуститьmaven
Заказclean package
ты найдешьsecurity-demo/target
генерируется вjar
Только7KB
,Это потому чтоmaven
Метод упаковки по умолчанию от него не зависитjar
зайди и поставьspringboot
стартовый класс. В этот момент нам нужно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>
<!-- 生成的jar文件名 -->
<finalName>demo</finalName>
</build>
Сделай это сноваclean package
найдуtarget
произвелdemo.jar
иdemo.jar.original
,вdemo.jar
Является исполняемым иdemo.jar.original
Это сохраняетсяmaven
Упаковка по умолчанию
Используйте MockMVC для написания тестов интерфейса.
Придерживаясь принципа сначала тестирования (желательно сначала написать тестовые примеры, а затем написать интерфейсы, и проверить, что программа работает в соответствии с нашими идеями), нам нужно использоватьspring-boot-starter-test
Платформа тестирования и все, что с ней связаноMockMvc
API.mock
В смысле нагромождения это означает использование тестовых случаев для надежной сборки программы.
первый вsecurity-demo
добавить тестовые зависимости
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
затем вsrc/test/java
Новый тестовый класс выглядит следующим образом
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() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
@Test
public void hello() throws Exception {
mockMvc.perform(get("/hello").contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk())
.andExpect(jsonPath("$").value("hello spring security"));
}
}
Поскольку он тестирует интерфейс HTTP, его необходимо внедрить в веб-контейнер.WebApplicationContext
. вget()
,status()
,jsonPath()
Все они являются статическими импортированными методами, тестовый код означает черезGET
Отправить запрос на способ/hello
(get("/hello")
) и добавьте заголовок запроса какContent-Type: application/json
(поэтому параметр будет начинаться сjson
способ прикрепить к телу запроса, да правильно,GET
Запрос также может иметь прикрепленный тело запроса! )
andExpect(status().isOk())
Ожидаемый код состояния ответа200
(см. Коды состояния HTTP),andExpect((jsonPath("$").value("hello spring security"))
ожидаемый ответJSON
данные представляют собой строку, а содержимоеhello spring security
(Этот метод зависит отJSON
Разобрать фреймjsonpath
,$
выражатьJSON
Объект типа данных, соответствующий онтологии в Java. Дополнительные API см. в разделе:GitHub.com/search?please=just…
Наиболее важными API являютсяMockMvc
,MockMvcRequestBuilders
,MockMvcRequestBuilders
-
MockMvc
,перечислитьperform
Укажите адрес интерфейса -
MockMvcRequestBuilders
, Запросы на сборку (включая путь запроса, отправку, заголовок запроса, тело запроса и т. д.) -
MockMvcRequestBuilders
, подтвердите результат ответа, например код состояния ответа, тело ответа
Сведения об аннотации MVC
@RestController
используется для идентификацииController
заRestful Controller
, где возвращаемый результат метода будетSpringMVC
автоматически преобразуется вJSON
И заголовок ответа для установкиContent-Type=application/json
@RequestMapping
для сопоставления URL-адресов с методами иSpringMVC
Параметры запроса будут автоматически привязаны к входным параметрам метода в соответствии с соответствующей связью имен параметров.
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;
}
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());
return users;
}
}
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() throws Exception {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
@Test
public void query() throws Exception {
mockMvc.perform(get("/user").
contentType(MediaType.APPLICATION_JSON_UTF8)
.param("username", "tom"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(3));
}
}
пройти черезMockMvcRequestBuilders.param
К запросу можно прикрепить формальные параметры URL.
Укажите, как отправить
если не прошелmethod
указывает метод отправки, тогда будут приняты все методы отправки, но если установлено@RequestMapping(method = RequestMethod.GET)
, тогда толькоGET
Запрос будет принят, другие способы отправки приведут к405 unsupported request method
@RequestParam
Обязательные параметры
Приведенный выше пример кода, если запрос не приходит с параметрамиusername
,ТакController
Параметрам будет присвоено значение типа данных по умолчанию. Если вы хотите запросить параметр, вы можете использовать его, вы можете использовать его.@RequestParam
и указатьrequired=true
(Можно и не указывать, по умолчанию стоит)
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;
}
ControllerTest
@Test
public void testBadRequest() throws Exception {
mockMvc.perform(get("/user").
contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().is4xxClientError());
}
Поскольку запрос не имеет прикрепленных параметровusername
, поэтому он сообщит об ошибке400 bad request
, мы можем использоватьis4xxClientError()
Код состояния ответа400
просьба утверждать
отображение имени параметра
SpringMVC
По умолчанию значения параметров сопоставляются по тому же правилу, что и имя параметра.Если вы хотите преобразовать параметры в запросеusername
Значение привязано к параметру методаuserName
на, черезname
Собственность илиvalue
Атрибуты
@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;
}
@Test
public void testParamBind() throws Exception {
mockMvc.perform(get("/user").
contentType(MediaType.APPLICATION_JSON_UTF8)
.param("username", "tom"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(3));
}
Значение параметра по умолчанию
Если вы не хотите, чтобы запрос содержал параметр, но хотите, чтобы параметр метода имел значение по умолчанию, когда значение параметра не получено (например,“”
Сравниватьnull
Сообщать об ошибках сложнее), тогда можно пройтиdefaultValue
Атрибуты
@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;
}
@Test
public void testDefaultValue() throws Exception {
mockMvc.perform(get("/user").
contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(3));
}
привязка фасоли
Если запрос поставляется со многими параметрами, и параметры являются аффилированными свойствами объекта, затем записывают их один за другим в способе сравнения резервирования опорных столбцов, мы можем упаковать их в единые объекты передачи данных (Data Transportation Object DTO
), Такие как
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;
}
Затем заполните объект в параметре метода,SpringMVC
Это поможет нам реализовать привязку параметров запроса к свойствам объекта (правило привязки по умолчанию — имена параметров совпадают)
@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;
}
ReflectionToStringBuilder
Вспомогательный класс отражения может быть переопределен в объекте безtoString
Метод помогает нам просматривать свойства объекта посредством отражения.
@Test
public void testDtoBind() 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));
}
Привязка компонента не влияет на привязку @RequestParam.
и не думай о встрече@RequestParam
конфликт, вывод следующий
tom
top.zhenganwen.securitydemo.dto.UserQueryConditionDto@440ef8d[
username=tom
password=123456
phone=12345678911
]
Привязка компонента имеет приоритет над привязкой параметра примитивного типа.
Однако, если не даноuserName
Добавить к@RequestParam
аннотацию, то то, что он получит, будетnull
null
top.zhenganwen.securitydemo.dto.UserQueryConditionDto@440ef8d[
username=tom
password=123456
phone=12345678911
]
Привязка параметров пейджинга
spring-data
семья (например,spring-boot-data-redis
), чтобы помочь нам инкапсулировать DTO подкачкиPageable
, передаст параметр разбиения на страницы, который мы передаемsize
(строк на странице),page
(текущий номер страницы),sort
(поле сортировки и стратегия сортировки) автоматически привязываются к автоматически внедряемомуPageable
Примеры
@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;
}
@Test
public void testPageable() 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));
}
null
top.zhenganwen.securitydemo.dto.UserQueryConditionDto@24e5389c[
username=tom
password=123456
phone=12345678911
]
2
30
age: DESC
@PathVariable
переменный заполнитель
наиболее общийRestful URL
,рисунокGET /user/1
Получатьid
за1
информация о пользователе, то когда мы пишем интерфейс, нам нужно поставить1
заменить заполнителем, например{id}
, динамически связан с параметрами метода в соответствии с фактическим запросом URLid
начальство
@GetMapping("/user/{id}")
public User info(@PathVariable("id") Long id) {
System.out.println(id);
return new User("jack","123");
}
@Test
public void testPathVariable() throws Exception {
mockMvc.perform(get("/user/1").
contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("jack"));
}
1
Если имя параметра метода совпадает с именем переменной заполнителя URL, его можно опустить.@PathVariable
изvalue
Атрибуты
обычный матч
Иногда нам нужен детальный контроль над сопоставлением URL-адресов, например/user/1
Соответствует/user/{id}
,и/user/xxx
не будет соответствовать/user/{id}
@GetMapping("/user/{id:\\d+}")
public User getInfo(@PathVariable("id") Long id) {
System.out.println(id);
return new User("jack","123");
}
@Test
public void testRegExSuccess() throws Exception {
mockMvc.perform(get("/user/1").
contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk());
}
@Test
public void testRegExFail() throws Exception {
mockMvc.perform(get("/user/abc").
contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().is4xxClientError());
}
@JsonView
Сценарии применения
Иногда нам нужно отфильтровать некоторые поля объекта ответа, например, не показывать при запросе всех пользователей.password
поле, согласноid
Отображается при запросе пользователейpassword
поле, вы можете пройти@JsonView
Аннотация для достижения такой функции
инструкции
1. Объявите интерфейс представления, каждый интерфейс представляет стратегию видимости поля объекта при ответе на данные
Представление здесь относится к стратегии включения полей, которая добавляется позже.@JsonView
будет использоваться, когда
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
/**
* 普通视图,返回用户基本信息
*/
public interface UserOrdinaryView {
}
/**
* 详情视图,除了普通视图包含的字段,还返回密码等详细信息
*/
public interface UserDetailsView extends UserOrdinaryView{
}
private String username;
private String password;
}
Между представлениями и представлениями может существовать отношение наследования.После наследования представления будут унаследованы поля, содержащиеся в представлении.
2. Добавьте представление к полю объекта ответа, указав, что поле включено в представление.
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
/**
* 普通视图,返回用户基本信息
*/
public interface UserOrdinaryView {
}
/**
* 详情视图,除了普通视图包含的字段,还返回密码等详细信息
*/
public interface UserDetailsView extends UserOrdinaryView{
}
@JsonView(UserOrdinaryView.class)
private String username;
@JsonView(UserDetailsView.class)
private String password;
}
3. Добавьте представление в метод Controller, указав, что данные объекта, возвращаемые методом, отображают только поля, содержащиеся в представлении.
@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");
}
контрольная работа
@Test
public void testUserBasicViewSuccess() 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() 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"}
поэтапный рефакторинг
потребности в рефакторингебежать маленькими шагами, то есть каждый раз, когда вы заканчиваете писать часть функции, вам приходится оглядываться назад, чтобы увидеть, что нужно оптимизировать.
Оба метода в кодеRequestMapping
все использовано/user
, мы можем упомянуть его в классе для повторного использования
@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");
}
}
Хотя это очень детальный вопрос, у нас должна быть такая мысль и привычка.
Не забудьте повторно запустить все тестовые примеры после рефакторинга, чтобы убедиться, что рефакторинг не изменил поведение программы.
Обработать тело запроса
@RequestBody сопоставляет тело запроса с параметрами метода Java.
SpringMVC
По умолчанию параметры в корпусе запроса не анализируются и не связаны с параметрами метода
@PostMapping
public void createUser(User user) {
System.out.println(user);
}
@Test
public void testCreateUser() 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)
использовать@RequestBody
Тело запроса может бытьJSON
Данные анализируются в объекты Java и привязываются к параметрам метода.
@PostMapping
public void createUser(@RequestBody User user) {
System.out.println(user);
}
@Test
public void testCreateUser() 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)
Обработка параметров типа даты
Если вам нужно привязать данные типа времени кBean
изDate
В полевых условиях распространенным решением в Интернете является добавлениеjson
Преобразователь сообщений отформатирован таким образом, что логика отображения даты жестко закодирована в бэкенде.
Лучшей практикой должно быть то, что серверная часть сохраняет только метку времени, и только метка времени передается во внешний интерфейс, а ответственность за форматирование и отображение передается внешнему интерфейсу, а внешний интерфейс любит показать, как это отображать.
@PostMapping
public void createUser(@RequestBody User user) {
System.out.println(user);
}
@Test
public void testDateBind() 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)
Аннотация @Valid проверяет правильность параметров запроса.
Логика проверки извлечения
существуетController
В методе нам часто нужно проверить правильность параметров запроса перед выполнением логики обработки.Традиционный способ написания - использоватьif
судить
@PostMapping
public void createUser(@RequestBody User user) {
if (StringUtils.isBlank(user.getUsername())) {
throw new IllegalArgumentException("用户名不能为空");
}
if (StringUtils.isBlank(user.getPassword())) {
throw new IllegalArgumentException("密码不能为空");
}
System.out.println(user);
}
Однако, если требуется проверка в других местах, нужно писать повторяющийся код, раз логика проверки меняется, ее нужно менять во многих местах, а если что-то упустить, то это закроет в программе скрытые опасности. Немного осведомленности о рефакторинге может инкапсулировать метод для каждой логики проверки, но это все еще избыточно.
SpringMVC Restful
рекомендуется использовать@Valid
Чтобы добиться проверки параметров, и те, кто не прошел проверку, ответят400 bad request
Для внешнего интерфейса результат обработки (и неправильный формат запроса) представлен кодом состояния, вместо того, чтобы генерировать исключение напрямую, как в приведенном выше коде, код состояния, полученный внешним интерфейсом,500
Сначала мы должны использоватьhibernate-validator
Некоторые аннотации ограничений, предоставляемые структурой проверки для ограниченияBean
поле
@NotBlank
@JsonView(UserBasicView.class)
private String username;
@NotBlank
@JsonView(UserDetailsView.class)
private String password;
Просто добавьте эти аннотации,SpringMVC
не поможет нам проверить
@PostMapping
public void createUser(@RequestBody User user) {
System.out.println(user);
}
@Test
public void testConstraintValidateFail() throws Exception {
mockMvc.perform(post("/user").
contentType(MediaType.APPLICATION_JSON_UTF8)
.content("{\"username\":\"\"}"))
.andExpect(status().isOk());
}
User(id=null, username=, password=null, birthday=null)
Нам также необходимо проверитьBean
добавлено до@Valid
заметьте, такSpringMVC
будет основываться на нашемBean
Добавленные аннотации ограничений проверяются, и ответ не будет получен, если проверка не пройдена.400 bad request
@PostMapping
public void createUser(@Valid @RequestBody User user) {
System.out.println(user);
}
@Test
public void testConstraintValidateSuccess() throws Exception {
mockMvc.perform(post("/user").
contentType(MediaType.APPLICATION_JSON_UTF8)
.content("{\"username\":\"\"}"))
.andExpect(status().is4xxClientError());
}
Аннотация ограничения
hibernate-validator
Предоставленные аннотации ограничений следующие:
Например, при создании пользователя ограничьтеbirthday
Ценность прошедшего времени
первый вBean
Добавьте аннотации ограничений к полям
@Past
private Date birthday;
затем проверитьBean
добавлено до@Valid
аннотация
@PostMapping
public void createUser(@Valid @RequestBody User user) {
System.out.println(user);
}
@Test
public void testValidatePastTimeSuccess() 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\":\"123\",\"birthday\":\"" + date.getTime() + "\"}"))
.andExpect(status().isOk());
}
@Test
public void testValidatePastTimeFail() 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\":\"123\",\"birthday\":\"" + date.getTime() + "\"}"))
.andExpect(status().is4xxClientError());
}
Логика проверки мультиплексирования
Таким образом, если нам нужно добавить валидацию к методу модификации пользователя, просто добавьте@Valid
Просто
@PutMapping("/{id}")
public void update(@Valid @RequestBody User user, @PathVariable Long id) {
System.out.println(user);
System.out.println(id);
}
@Test
public void testUpdateSuccess() 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() throws Exception {
mockMvc.perform(put("/user/1").
contentType(MediaType.APPLICATION_JSON_UTF8)
.content("{\"username\":\"jack\",\"password\":\" \"}"))
.andExpect(status().is4xxClientError());
}
Логика ограничений должна бытьBean
Объявите его один раз через аннотацию ограничения в@Valid
Просто
BindingResult обрабатывает результаты проверки
Вышеупомянутый метод обработки все еще не идеален. Мы только сообщаем интерфейсу, что формат данных запроса неверен через код состояния ответа, но мы не указываем четко, что не так. Нам нужно предоставить интерфейсу более четкую информацию. .
В приведенном выше примере, если он не пройдет проверку, метод не будет выполнен и вернется напрямую.Нет возможности написать какую-либо подсказку, если мы хотим ее вставить. В это время вы можете использоватьBindingResult
, это может помочь нам получить информацию об ошибке проверки и вернуть ее во внешний интерфейс, а код состояния ответа станет 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);
}
@Test
public void testBindingResult() 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() 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
Примечательно,
BindingResult
должен и@Valid
используются вместе, и позиция в списке параметров должна следовать сразу за@Valid
Модифицировано после аргумента, в противном случае он появится как запутанные результаты
@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);
}
В приведенном выше коде в проверкеBean
иBindingResult
вставляется междуid
, ты найдешьBindingResult
больше не работает
@Test
public void testBindingResult2() 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
чек
пользовательское сообщение
Теперь мы можем пройти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);
}
@Test
public void testBindingResult3() 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)
Однако подсказка сообщения по умолчанию не очень удобна, и нам нужно соединить ее самостоятельно.На данный момент нам нужно настроить подсказку сообщения, и нам нужно только использовать аннотацию ограничения.message
Атрибут указывает подсказку о том, что проверка не удалась.
@NotBlank(message = "用户名不能为空")
@JsonView(UserBasicView.class)
private String username;
@NotBlank(message = "密码不能为空")
@JsonView(UserDetailsView.class)
private String password;
@Test
public void testBindingResult3() 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 密码不能为空
username 用户名不能为空
User(id=null, username= , password=null, birthday=Sun Aug 19 21:03:18 CST 2018)
Пользовательские аннотации проверки
Несмотря на то чтоhibernate-validator
Предоставляются некоторые часто используемые ограничения, но для сложных бизнес-сценариев все же необходимо настроить аннотацию ограничения.Ведь иногда недостаточно проверки, что не пусто или форматно.Может нам нужно перейти к запросу к базе данных, чтобы проверить
Далее мы обратимся к существующим аннотациям ограничений и настроим аннотацию ограничения «имя пользователя нельзя повторять» в соответствии с рекомендациями по рисованию тыквы.
1. Создайте новый класс аннотаций ограничений
Мы хотим, чтобы эта аннотация была отмечена вBean
в некоторых полях используйте@Target({FIELD})
; кроме того, чтобы аннотация работала во время выполнения, добавьте@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 {
}
Обратитесь к существующим аннотациям ограничений, таким какNotNull
,NotBlank
, все они имеют три метода
String message() default "{org.hibernate.validator.constraints.NotBlank.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
Итак, мы также объявляем эти три метода
@Target({FIELD})
@Retention(RUNTIME)
public @interface Unrepeatable {
String message() default "用户名已被注册";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
2. Напишите класс логики проверки
Согласно существующей аннотации, все они имеют аннотацию@Constraint
@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@ReportAsSingleViolation
@NotNull
public @interface NotBlank {
нажмите и удерживайтеCtrl
нажмитеvalidateBy
свойства, чтобы посмотреть и обнаружил, что он нуждается вConstraintValidator
класс реализации, теперь нам нужно написатьConstraintValidator
Настройте логику проверки и пройдитеvalidatedBy
свойство, чтобы связать его с нашимUnrepeatable
на заметку
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);
// 如果用户名存在,则拒绝请求并提示用户名已被注册,否则处理请求
return ifExists == true ? false : true;
}
}
в,ConstraintValidator<A,T>
ДженерикиA
указывается как аннотация для привязки,T
Укажите тип поля для проверки;isValid
Он используется для написания пользовательской логики проверки, такой как запрос базы данных на наличие записи имени пользователя и возвратtrue
Указывает, что проверка прошла,false
Ошибка проверки
@ComponentScan
в диапазоне сканированияConstraintValidator
Класс реализации будетSpring
Внедрить в контейнер, поэтому вам не нужно отмечать его в этом классеComponent
Вы можете ввести другиеBean
, например, в этом случае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;
}
}
3. Укажите класс проверки в аннотации ограничения.
пройти черезvalidatedBy
Атрибут указывает серию классов проверки, связанных этой аннотацией (эти классы проверки должны бытьConstraintValidator<A,T>
класс реализации
@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = { UsernameUnrepeatableValidator.class})
public @interface Unrepeatable {
String message() default "用户名已被注册";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
4. Тест
@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);
}
@Test
public void testCreateUserWithNewUsername() 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() 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 tom
用户名已被注册
User(id=null, username=tom, password=123, birthday=Mon Aug 20 08:25:11 CST 2018)
удалить пользователей
@Test
public void testDeleteUser() throws Exception {
mockMvc.perform(delete("/user/1").
contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk());
}
java.lang.AssertionError: Status
Expected :200
Actual :405
Сначала тестируйте, то есть сначала напишите тест-кейс, а затем напишите функциональный код.Даже если мы знаем, что функциональный тест не пройдет без написания функционального теста, тестовый код необходимо проверить, чтобы убедиться в правильности логики теста. .
Restful
Рекомендуется использовать код состояния ответа для обозначения результата обработки запроса, например, 200 означает, что удаление прошло успешно, если нет специального требования возвращать какую-то информацию, то нет необходимости добавлять тело ответа
@DeleteMapping("/{id:\\d+}")
public void delete(@PathVariable Long id) {
System.out.println(id);
// delete user
}
@Test
public void testDeleteUser() throws Exception {
mockMvc.perform(delete("/user/1").
contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk());
}
1
обработка ошибок
Механизм обработки ошибок SpringBoot по умолчанию
Дифференцируйте клиента, чтобы ответить
Если при обработке запроса возникает ошибка,SpringMVC
В зависимости от типа клиента будут разные результаты ответа, такие как доступ через браузерlocalhost:8080/xxx
вернет следующую страницу ошибки
при использованииPostman
Запрос получит следующий ответ
{
"timestamp": 1566268880358,
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/xxx"
}
Исходный код, соответствующий этому механизму, находится вBasicErrorController
в (происходит4xx
или500
Когда происходит исключение, запрос будет направлен на/error
,Зависит отBasicErrorController
Определить логику ответа на исключение)
@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);
}
Если это запрос, сделанный браузером, его заголовок запроса будет сопровождатьсяAccept: text/html...
,иPostman
Выданный запросAccept: */*
Поэтому первый будет выполнятьerrorHtml
реагировать на страницу ошибки, в то время какerror
Информация об исключении собирается дляmap
вернуться в виде
пользовательская страница ошибки
Для ответов об ошибках, когда клиентом является браузер, например 404/500, мы можем сделать это вsrc/main/resources/resources/error
Папка для записи пользовательской страницы ошибок,SpringMVC
Возвращает файл в соответствующее исключение происходит в папке404.html
или500.html
Создайтеsrc/main/resources/resources/error
папку и добавить404.html
и500.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>页面找不到了</title>
</head>
<body>
抱歉,页面找不到了!
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>服务异常</title>
</head>
<body>
服务端内部错误
</body>
</html>
Произошло исключение при фиктивной обработке запроса
@GetMapping("/{id:\\d+}")
@JsonView(User.UserDetailsView.class)
public User getInfo(@PathVariable("id") Long id) {
throw new RuntimeException("id不存在");
// System.out.println(id);
// return new User(1L, "jack", "123");
// return null;
}
доступlocalhost:8080/xxx
показывать404.html
Страница, посещениеlocalhost:8080/user/1
показывать500.html
страница
Стоит отметить, что настройка страницы исключений не приводит к тому, что небраузерные запросы также отвечают на страницу.
Пользовательская обработка исключений
за4XX
ошибка клиента,SpringMVC
вернет ответ об ошибке напрямую и не будет выполнятьсяController
Методы; для500
Если сервер выдает исключение, класс исключения будет собран.message
возвращаемое значение поля
Результат ответа исключения по умолчанию
Например ошибка клиента,GET /user/1
{
"timestamp": 1566270327128,
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.RuntimeException",
"message": "id不存在",
"path": "/user/1"
}
например ошибка сервера
@PostMapping
public void createUser(@Valid @RequestBody User user) {
System.out.println(user);
}
POST localhost:8080/user
Body {}
{
"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": "用户名不能为空",
"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": "密码不能为空",
"objectName": "user",
"field": "password",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotBlank"
}
],
"message": "Validation failed for object='user'. Error count: 2",
"path": "/user"
}
Пользовательские результаты ответа на исключение
Иногда нам нужно разобраться с часто бросать исключение, чтобы закрыть запрос при обработке запроса, например,
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不存在");
this.id = id;
}
}
@GetMapping("/{id:\\d+}")
@JsonView(User.UserDetailsView.class)
public User getInfo(@PathVariable("id") Long id) {
throw new IdNotExistException(id);
}
GET /user/1
{
"timestamp": 1566270990177,
"status": 500,
"error": "Internal Server Error",
"exception": "top.zhenganwen.securitydemo.exception.response.IdNotExistException",
"message": "id不存在",
"path": "/user/1"
}
SpringMVC
По умолчанию будут только исключенияmessage
вернуться, если нам нужноIdNotExistException
изid
Он также возвращается, чтобы дать внешнему интерфейсу более четкую подсказку, что требует от нас настройки обработки исключений.
- Необходимо добавить пользовательские классы обработки исключений.
@ControllerAdvice
- Используется в методе лечения ненормального
@ExceptionHandler
Объявите, какие исключения должны быть перехвачены, всеController
Если возникнет одно из этих исключений, вместо него будет выполнен метод. - Перехваченное исключение будет использоваться как параметр метода
- Результат, возвращаемый методом, такой же, как
Controller
Результат, возвращаемый методом, имеет тот же смысл, если вам нужно вернутьjson
вам нужно добавить метод@ResponseBody
Аннотация, если вы добавляете аннотацию к классу, это означает, что каждый метод имеет аннотацию
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());
return jsonResult;
}
}
использовать после перезагрузкиPostman GET /user/1
Ответ выглядит следующим образом
{
"id": 1,
"message": "id不存在"
}
перехватывать
Требование: записывайте время обработки всех запросов.
фильтр
фильтрJavaEE
Стандарт не зависитSpringMVC
Да, хочуSpringMVC
Есть два шага к использованию фильтров в
1. ОсознайтеFilter
интерфейс и внедрить в контейнер Spring
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 {
// 在web容器启动时执行
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("TimeFilter init");
}
// 在收到请求时执行,这时请求还未到达SpringMVC的入口DispatcherServlet
// 单次请求只会执行一次(不论期间发生了几次请求转发)
@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] 收到服务调用:" + service);
Date start = new Date();
System.out.println("[TimeFilter] 开始执行服务" + service + simpleDateFormat.format(start));
filterChain.doFilter(servletRequest, servletResponse);
Date end = new Date();
System.out.println("[TimeFilter] 服务" + service + "执行完毕 " + simpleDateFormat.format(end) +
",共耗时:" + (end.getTime() - start.getTime()) + "ms");
}
// 在容器销毁时执行
@Override
public void destroy() {
System.out.println("TimeFilter destroyed");
}
}
2. КонфигурацияFilterRegistrationBean
, этот шаг эквивалентен традиционному способу вweb.xml
добавить<Filter>
узел
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;
// 添加这个bean相当于在web.xml中添加一个Fitler节点
@Bean
public FilterRegistrationBean registerTimeFilter() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(timeFilter);
return filterRegistrationBean;
}
}
3. Тест
доступGET /user/1
, журнал консоли выглядит следующим образом
@GetMapping("/{id:\\d+}")
@JsonView(User.UserDetailsView.class)
public User getInfo(@PathVariable("id") Long id) {
// throw new IdNotExistException(id);
User user = new User();
return user;
}
[TimeFilter] 收到服务调用:【GET /user/1】
[TimeFilter] 开始执行服务【GET /user/1】2019-08-20 02:13:44
[TimeFilter] 服务【GET /user/1】执行完毕 2019-08-20 02:13:44,共耗时:4ms
так какFilter
даJavaEE
стандарт в , так что это зависит только отservlet-api
не зависит ни от какой сторонней библиотеки классов, поэтому естественно не знаетController
Существование , естественно, невозможно узнать, на какой метод будет сопоставлен этот запрос,SpringMVC
Этот недостаток устраняется введением перехватчиков.
пройти черезfilterRegistrationBean.addUrlPattern
Вы можете добавить правила блокировки для фильтров. Правилами блокировки по умолчанию являются все URL-адреса.
@Bean
public FilterRegistrationBean registerTimeFilter() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(timeFilter);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
Перехватчик Перехватчик
Перехватчик сFilter
имеют следующие отличия
-
Filter
основан на запросе,Interceptor
основан наController
Да, запрос может выполняться несколькоController
(через переадресацию), поэтому запрос будет выполнен только один разFilter
но может выполняться несколько разInterceptor
-
Interceptor
даSpringMVC
компонент в , поэтому он знаетController
Существование , может получить соответствующую информацию (например, метод отображения запроса, где метод находитсяbean
Ждать)
использоватьSpringMVC
Предоставленный перехватчик также требует двух шагов
1. ОсознайтеHandlerInterceptor
интерфейс
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 {
/**
* 在Controller方法执行前被执行
* @param httpServletRequest
* @param httpServletResponse
* @param handler 处理器(Controller方法的封装)
* @return true 会接着执行Controller方法
* false 不会执行Controller方法,直接响应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 + "被调用 " + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(start));
httpServletRequest.setAttribute("start", start.getTime());
return true;
}
/**
* 在Controller方法正常执行完毕后执行,如果Controller方法抛出异常则不会执行此方法
* @param httpServletRequest
* @param httpServletResponse
* @param handler
* @param modelAndView Controller方法返回的视图
* @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 + "调用结束 " + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(end)
+ " 共耗时:" + (end.getTime() - (Long) httpServletRequest.getAttribute("start")) + "ms");
}
/**
* 无论Controller方法是否抛出异常,都会被执行
* @param httpServletRequest
* @param httpServletResponse
* @param handler
* @param e 如果Controller方法抛出异常则为对应抛出的异常,否则为null
* @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 + "调用结束 " + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(end)
+ " 共耗时:" + (end.getTime() - (Long) httpServletRequest.getAttribute("start")) + "ms");
if (e != null) {
System.out.println("[TimeInterceptor#afterCompletion] 服务" + service + "调用异常:" + e.getMessage());
}
}
}
2. Класс конфигурации наследует WebMvcConfigureAdapter и переписывает метод addInterceptor для добавления пользовательского перехватчика.
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
TimeFilter timeFilter;
@Autowired
TimeInterceptor timeInterceptor;
@Bean
public FilterRegistrationBean registerTimeFilter() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(timeFilter);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(timeInterceptor);
}
}
несколько вызововaddInterceptor
Можно добавить несколько перехватчиков
3. Тест
GET /user/1
[TimeFilter] 收到服务调用:【GET /user/1】
[TimeFilter] 开始执行服务【GET /user/1】2019-08-20 02:59:00
[TimeInterceptor # preHandle] 服务【top.zhenganwen.securitydemo.web.controller.UserController@2b6a0ea9#getInfo】被调用 2019-08-20 02:59:00
[TimeFilter] 服务【GET /user/1】执行完毕 2019-08-20 02:59:00,共耗时:2ms
- будет
preHandle
Возвращаемое значение изменяется наtrue
[TimeFilter] 收到服务调用:【GET /user/1】
[TimeFilter] 开始执行服务【GET /user/1】2019-08-20 02:59:20
[TimeInterceptor # preHandle] 服务【top.zhenganwen.securitydemo.web.controller.UserController@2b6a0ea9#getInfo】被调用 2019-08-20 02:59:20
[TimeInterceptor # postHandle] 服务【top.zhenganwen.securitydemo.web.controller.UserController@2b6a0ea9#getInfo】调用结束 2019-08-20 02:59:20 共耗时:39ms
[TimeInterceptor # afterCompletion] 服务【top.zhenganwen.securitydemo.web.controller.UserController@2b6a0ea9#getInfo】调用结束 2019-08-20 02:59:20 共耗时:39ms
[TimeFilter] 服务【GET /user/1】执行完毕 2019-08-20 02:59:20,共耗时:42ms
- Выбрасывание исключения в методе контроллера
@GetMapping("/{id:\\d+}")
@JsonView(User.UserDetailsView.class)
public User getInfo(@PathVariable("id") Long id) {
throw new IdNotExistException(id);
// User user = new User();
// return user;
}
[TimeFilter] 收到服务调用:【GET /user/1】
[TimeFilter] 开始执行服务【GET /user/1】2019-08-20 03:05:56
[TimeInterceptor # preHandle] 服务【top.zhenganwen.securitydemo.web.controller.UserController@2b6a0ea9#getInfo】被调用 2019-08-20 03:05:56
[TimeInterceptor # afterCompletion] 服务【top.zhenganwen.securitydemo.web.controller.UserController@2b6a0ea9#getInfo】调用结束 2019-08-20 03:05:56 共耗时:11ms
[TimeFilter] 服务【GET /user/1】执行完毕 2019-08-20 03:05:56,共耗时:14ms
НаходитьafterCompletion
Исключественная печать логика не выполняется, потому чтоIdNotExistException
Обработка обрабатывалась нашим предыдущим необычным процессором и не выкидывала. Мы изменились, чтобы броситьRuntimeException
Попробуй снова
@GetMapping("/{id:\\d+}")
@JsonView(User.UserDetailsView.class)
public User getInfo(@PathVariable("id") Long id) {
throw new RuntimeException("id not exist");
}
[TimeFilter] 收到服务调用:【GET /user/1】
[TimeFilter] 开始执行服务【GET /user/1】2019-08-20 03:09:38
[TimeInterceptor # preHandle] 服务【top.zhenganwen.securitydemo.web.controller.UserController@2b6a0ea9#getInfo】被调用 2019-08-20 03:09:38
[TimeInterceptor # afterCompletion] 服务【top.zhenganwen.securitydemo.web.controller.UserController@2b6a0ea9#getInfo】调用结束 2019-08-20 03:09:38 共耗时:7ms
[TimeInterceptor#afterCompletion] 服务【top.zhenganwen.securitydemo.web.controller.UserController@2b6a0ea9#getInfo】调用异常:id not exist
java.lang.RuntimeException: id not exist
at top.zhenganwen.securitydemo.web.controller.UserController.getInfo(UserController.java:42)
...
[TimeInterceptor # preHandle] 服务【org.springframework.boot.autoconfigure.web.BasicErrorController@33f17289#error】被调用 2019-08-20 03:09:38
[TimeInterceptor # postHandle] 服务【org.springframework.boot.autoconfigure.web.BasicErrorController@33f17289#error】调用结束 2019-08-20 03:09:38 共耗时:7ms
[TimeInterceptor # afterCompletion] 服务【org.springframework.boot.autoconfigure.web.BasicErrorController@33f17289#error】调用结束 2019-08-20 03:09:38 共耗时:7ms
Схема последовательности вызовов методов примерно следующая
Срез Аспект
Сценарии применения
Interceptor
У него все еще есть свои ограничения, то есть нельзя получить информацию о входных параметрах для вызова метода Controller.Например, нам нужно логировать информацию о позиции заказа, запрошенную пользователем, чтобы предоставить данные для системы рекомендаций, то на этом времяInterceptor
ничего не поделать
отследить исходный кодDispatcherServlet -> doService -> doDispatch
обнаруживаемыйInterceptor
Причины не получения входных параметров:
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
mappedHandler.applyPreHandle
На самом деле, вызываяHandlerInterceptor
изpreHandle
метод, который вызывается послеha.handle(processedRequest, response, mappedHandler.getHandler())
параметры запросаprocessedRequest
вводить вhandler
Введите ссылку
инструкции
Аспектно-ориентированное программирование (Aspect-Oriented Program AOP
) — это шаблон проектирования улучшения объектов, основанный на динамическом прокси, который может добавлять подключаемые функции без изменения существующего кода.
существуетSpringMVC
Нам нужно три шага, чтобы использовать АОП в
- Напишите классы срезов/аспектов, которые сочетают в себе точечные разрезы и улучшения.
- Добавить к
@Component
, введенный в контейнер Spring - Добавить к
@Aspect
, активируйте переключатель программирования соотношения сторон
- Добавить к
- Напишите pointcut, который можно сделать с помощью аннотаций Pointcut состоит из двух частей: какие методы нужно улучшить и когда улучшить
- пора резать
-
@Before
, до выполнения метода -
@AfterReturning
, после окончания нормального выполнения метода -
@AfterThrowing
, после того как метод выдает исключение -
@After
, метод завершается нормальноreturn
раньше, эквивалентноreturn
вставлен передfinally
-
@Around
, вы можете использовать введенные входные параметрыProceedingJoinPoint
Гибкая реализация вышеуказанных четырех таймингов, его функция такая же, как и в методе перехватчикаhandler
Аналогично, но с более полезной информацией о времени выполнения
-
- pointcut, вы можете использовать
execution
выражение см.:docs.spring.IO/весна/документы…
- пора резать
- написать методы улучшения,
- Только один
@Around
Может иметь доход, может получитьProceedingJoinPoint
пример - позвонив
ProceedingJoinPoint
изpoint.proceed()
Может вызвать соответствующий метод контроллера и получить возвращаемое значение.
- Только один
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包下的所有Controller的所有方法
@Around("execution(* top.zhenganwen.securitydemo.web.controller.*.*(..))")
public Object handleControllerMethod(ProceedingJoinPoint point) throws Throwable {
// handler对应的方法签名(哪个类的哪个方法,参数列表是什么)
String service = "【"+point.getSignature().toLongString()+"】";
// 传入handler的参数值
Object[] args = point.getArgs();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
Date start = new Date();
System.out.println("[GlobalControllerAspect]开始调用服务" + service + " 请求参数: " + Arrays.toString(args) + ", " + simpleDateFormat.format(start));
Object result = null;
try {
// 调用实际的handler并取得结果
result = point.proceed();
} catch (Throwable throwable) {
System.out.println("[GlobalControllerAspect]调用服务" + service + "发生异常, message=" + throwable.getMessage());
throw throwable;
}
Date end = new Date();
System.out.println("[GlobalControllerAspect]服务" + service + "调用结束,响应结果为: " + result+", "+simpleDateFormat.format(end)+", 共耗时: "+(end.getTime()-start.getTime())+
"ms");
// 返回响应结果,不一定要和handler的处理结果一致
return result;
}
}
контрольная работа
@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();
}
GET /user/1
[TimeFilter] 收到服务调用:【GET /user/1】
[TimeFilter] 开始执行服务【GET /user/1】2019-08-20 05:21:48
[TimeInterceptor # preHandle] 服务【top.zhenganwen.securitydemo.web.controller.UserController@49433c98#getInfo】被调用 2019-08-20 05:21:48
[GlobalControllerAspect]开始调用服务【public top.zhenganwen.securitydemo.dto.User top.zhenganwen.securitydemo.web.controller.UserController.getInfo(java.lang.Long)】 请求参数: [1], 2019-08-20 05:21:48
[UserController # getInfo]query user by id
[GlobalControllerAspect]服务【public top.zhenganwen.securitydemo.dto.User top.zhenganwen.securitydemo.web.controller.UserController.getInfo(java.lang.Long)】调用结束,响应结果为: User(id=null, username=null, password=null, birthday=null), 2019-08-20 05:21:48, 共耗时: 0ms
[TimeInterceptor # postHandle] 服务【top.zhenganwen.securitydemo.web.controller.UserController@49433c98#getInfo】调用结束 2019-08-20 05:21:48 共耗时:4ms
[TimeInterceptor # afterCompletion] 服务【top.zhenganwen.securitydemo.web.controller.UserController@49433c98#getInfo】调用结束 2019-08-20 05:21:48 共耗时:4ms
[TimeFilter] 服务【GET /user/1】执行完毕 2019-08-20 05:21:48,共耗时:6ms
[TimeFilter] 收到服务调用:【GET /user/1】
[TimeFilter] 开始执行服务【GET /user/1】2019-08-20 05:24:40
[TimeInterceptor # preHandle] 服务【top.zhenganwen.securitydemo.web.controller.UserController@49433c98#getInfo】被调用 2019-08-20 05:24:40
[GlobalControllerAspect]开始调用服务【public top.zhenganwen.securitydemo.dto.User top.zhenganwen.securitydemo.web.controller.UserController.getInfo(java.lang.Long)】 请求参数: [1], 2019-08-20 05:24:40
[UserController # getInfo]query user by id
[GlobalControllerAspect]调用服务【public top.zhenganwen.securitydemo.dto.User top.zhenganwen.securitydemo.web.controller.UserController.getInfo(java.lang.Long)】发生异常, message=id not exist
[TimeInterceptor # afterCompletion] 服务【top.zhenganwen.securitydemo.web.controller.UserController@49433c98#getInfo】调用结束 2019-08-20 05:24:40 共耗时:2ms
[TimeInterceptor#afterCompletion] 服务【top.zhenganwen.securitydemo.web.controller.UserController@49433c98#getInfo】调用异常:id not exist
java.lang.RuntimeException: id not exist
at top.zhenganwen.securitydemo.web.controller.UserController.getInfo(UserController.java:42)
...
[TimeInterceptor # preHandle] 服务【org.springframework.boot.autoconfigure.web.BasicErrorController@445821a6#error】被调用 2019-08-20 05:24:40
[TimeInterceptor # postHandle] 服务【org.springframework.boot.autoconfigure.web.BasicErrorController@445821a6#error】调用结束 2019-08-20 05:24:40 共耗时:2ms
[TimeInterceptor # afterCompletion] 服务【org.springframework.boot.autoconfigure.web.BasicErrorController@445821a6#error】调用结束 2019-08-20 05:24:40 共耗时:3ms
Суммировать
процесс запроса
процесс ответа
Загрузка и загрузка файлов и пробный тест
Файл загружен
Старые правила, сначала проверьте, но используйтеMockMvc
Имитация запроса на загрузку файла все еще несколько отличается, запрос должен использовать статический метод.fileUpload
и установитьcontentType
заmultipart/form-data
@Test
public void upload() 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 请求参数,相当于<input>标签的的`name`属性
* originalName 上传的文件名称
* contentType 上传文件需指定为`multipart/form-data`
* content 字节数组,上传文件的内容
*/
.file(new MockMultipartFile("file", "hello.txt", "multipart/form-data", content)))
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString();
System.out.println(fileKey);
}
Контроллер управления файлами
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.getName());
System.out.println("[FileController]文件名称: " + file.getName());
System.out.println("[FileController]文件大小: "+file.getSize()+"字节");
String fileKey = new Date().getTime() + "_" + file.getOriginalFilename();
File storeFile = new File(FILE_STORE_FOLDER, fileKey);
// 可以通过file.getInputStream将文件上传到FastDFS、云OSS等存储系统中
// InputStream inputStream = file.getInputStream();
// byte[] content = new byte[inputStream.available()];
// inputStream.read(content);
file.transferTo(storeFile);
return fileKey;
}
}
Результаты теста
[FileController]文件请求参数: file
[FileController]文件名称: file
[FileController]文件大小: 12字节
1566349460611_hello.txt
Проверьте рабочий стол и найдите еще один1566349460611_hello.txt
и содержание егоhello upload
Загрузка файла
вводитьapache io
Инструментарий
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
Скачать интерфейс
@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()
) {
// 下载需要设置响应头为 application/x-download
response.setContentType("application/x-download");
// 设置下载询问框中的文件名
response.setHeader("Content-Disposition", "attachment;filename=" + fileKey);
IOUtils.copy(is, os);
os.flush();
}
}
Тест: доступ через браузерhttp://localhost:8080/file/1566349460611_hello.txt
Отображение записывается как/{fileKey:.+}
вместо/{fileKey}
Причина в том,SpringMVC
Сопоставление игнорируется.
символ после символа. Обычный.+
Указывает, что соответствует любому не-\n
символ, если регулярка не добавлена, метод вводится как параметрfileKey
Полученное значение будет1566349460611_hello
вместо1566349460611_hello.txt
Асинхронная обработка REST-сервисов
Раньше у нас было так, что каждый раз, когда клиент отправляет запрос,tomcat
Пул потоков отправляет поток для обработки до тех пор, пока обработка запроса не завершит результат ответа и поток не будет занят. Как только системный параллелизм увеличится, тогдаtomcat
Аватарный пул резьбы станет слабым, тогда мы можем принять форму асинхронной обработки.
Во избежание вмешательства фильтров, перехватчиков и журналов срезов, добавленных в предыдущем разделе, мы пока закомментируем его.
//@Component
public class TimeFilter implements Filter {
Внезапно обнаружил, что фильтр реализации похоже наследует
Filter
интерфейс и добавить@Component
будет работать, потому что только закомментироватьWebConfig
серединаregisterTimeFilter
метод, обнаружитьTimeFilter
все еще распечатайте журнал
//@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
//@Aspect
//@Component
public class GlobalControllerAspect {
ВЫЗЫВАЕМЫЙ процесс
существуетController
, еслиCallable
как возвращаемое значение метода, тоtomcat
Потоки в пуле потоков создадут новый поток для выполненияCallable
и вернуть результат клиенту
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());
// 创建订单
@PostMapping
public Callable<String> createOrder() {
// 生成12位单号
String orderNumber = RandomStringUtils.randomNumeric(12);
logger.info("[主线程]收到创建订单请求,订单号=>" + orderNumber);
Callable<String> result = () -> {
logger.info("[副线程]创建订单开始,订单号=>"+orderNumber);
// 模拟创建订单逻辑
TimeUnit.SECONDS.sleep(3);
logger.info("[副线程]创建订单完成,订单号=>" + orderNumber+",返回结果给客户端");
return orderNumber;
};
logger.info("[主线程]已将请求委托副线程处理(订单号=>" + orderNumber + "),继续处理其它请求");
return result;
}
}
использоватьPostman
Результаты теста следующие
Журнал консоли:
2019-08-21 21:10:39.059 INFO 17044 --- [nio-8080-exec-2] t.z.s.w.controller.AsyncOrderController : [主线程]收到创建订单请求,订单号=>719547514079
2019-08-21 21:10:39.059 INFO 17044 --- [nio-8080-exec-2] t.z.s.w.controller.AsyncOrderController : [主线程]已将请求委托副线程处理(订单号=>719547514079),继续处理其它请求
2019-08-21 21:10:39.063 INFO 17044 --- [ MvcAsync1] t.z.s.w.controller.AsyncOrderController : [副线程]创建订单开始,订单号=>719547514079
2019-08-21 21:10:42.064 INFO 17044 --- [ MvcAsync1] t.z.s.w.controller.AsyncOrderController : [副线程]创建订单完成,订单号=>719547514079,返回结果给客户端
Обратите внимание, что основной поток не выполняетсяCallable
Поставил задачу на заказ и сразу пошел дальше мониторить другие заявки.Задание на заказ делаетSpringMVC
начал новую темуMvcAsync1
воплощать в жизнь,Postman
Время отклика также вCallable
После завершения выполнения получается его возвращаемое значение. Для клиента асинхронная обработка бэкенда прозрачна и ничем не отличается от синхронной, но для бэкэндаtomcat
Поток, который прослушивает запросы, занят в течение очень короткого времени, что значительно улучшает его возможности параллелизма.
DeferredResult обрабатывается асинхронно
Callable
Недостатком асинхронной обработки является то, что асинхронная обработка может быть выполнена только путем локального создания нового вторичного потока, но сейчас, с преобладанием микросервисной архитектуры, нам часто требуется асинхронная обработка в разных системах. Например, в системе seckill количество одновременных запросов заказов велико, и если бэкенд обрабатывает каждый запрос заказа синхронно (то есть обрабатывает заказ в потоке запросов), а затем возвращает результат ответа, то обслуживание будет приостановлено ( отправить запрос на заказ). Ответа нет); в это время мы можем использовать промежуточное программное обеспечение сообщений, поток запросов отвечает только за мониторинг запроса на заказ, а затем отправляет сообщение в MQ, чтобы система заказов вытягивала сообщение (например, номер заказа) от MQ для обработки заказа и Результат обработки возвращается в систему seckill; система seckill самостоятельно настраивает поток для отслеживания сообщения о результате обработки заказа и возвращает результат обработки клиенту. как показано на рисунке
Для достижения эффекта, подобного вышеописанному, необходимо использоватьFuture
Режим (см. «Практика многопоточного программирования на Java (режим проектирования)»), то есть мы можем установить сертификат результата обработкиDeferredResult
, если мы прямо назовем егоgetResult
Результат обработки не может быть получен (будет заблокирован, это означает, что хотя поток запроса продолжает обрабатывать запрос, клиент по-прежнемуpending
, только когда поток вызывает свойsetResult(result)
, будет соответствоватьresult
ответ клиенту
В этом примере для уменьшения сложности используйте локальную памятьLinkedList
Вместо промежуточного программного обеспечения для распределенного обмена сообщениями, вместо использования локальной новой системы упорядочения потоков, отношения между различными типами выглядят следующим образом.
Убейте системный 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;
// 秒杀系统下单请求
@PostMapping
public DeferredResult<String> createOrder() {
logger.info("【请求线程】收到下单请求");
// 生成12位单号
String orderNumber = RandomStringUtils.randomNumeric(12);
// 创建处理结果凭证放入缓存,以便监听(订单系统向MQ发送的订单处理结果消息的)线程向凭证中设置结果,这会触发该结果响应给客户端
DeferredResult<String> deferredResult = new DeferredResult<>();
deferredResultHolder.placeOrder(orderNumber, deferredResult);
// 异步向MQ发送下单消息,假设需要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("向MQ发送下单消息, 单号: {}", orderNumber);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "本地临时线程-向MQ发送下单消息")
.start();
logger.info("【请求线程】继续处理其它请求");
// 并不会立即将deferredResult序列化成JSON并返回给客户端,而会等deferredResult的setResult被调用后,将传入的result转成JSON返回
return deferredResult;
}
}
два МК
package top.zhenganwen.securitydemo.web.async;
import org.springframework.stereotype.Component;
import java.util.LinkedList;
/**
* @author zhenganwen
* @date 2019/8/22
* @desc OrderProcessingQueue 下单消息MQ
*/
@Component
public class OrderProcessingQueue extends LinkedList<String> {
}
package top.zhenganwen.securitydemo.web.async;
import org.springframework.stereotype.Component;
import java.util.LinkedList;
/**
* @author zhenganwen
* @date 2019/8/22
* @desc OrderCompletionQueue 订单处理完成MQ
*/
@Component
public class OrderCompletionQueue extends LinkedList<OrderCompletionResult> {
}
package top.zhenganwen.securitydemo.web.async;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author zhenganwen
* @date 2019/8/22
* @desc OrderCompletionResult 订单处理完成结果信息,包括单号和是否成功
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderCompletionResult {
private String orderNumber;
private String result;
}
Кэш учетных данных
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
* @desc DeferredResultHolder 订单处理结果凭证缓存,通过凭证可以在未来的时间点获取处理结果
*/
@Component
public class DeferredResultHolder {
private Map<String, DeferredResult<String>> holder = new HashMap<>();
// 将订单处理结果凭证放入缓存
public void placeOrder(@NotBlank String orderNumber, @NotNull DeferredResult<String> result) {
holder.put(orderNumber, result);
}
// 向凭证中设置订单处理完成结果
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);
}
}
Два монитора, соответствующие двум очередям
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;
// spring容器启动或刷新时执行此方法
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 本系统(秒杀系统)启动时,启动一个监听MQ下单完成消息的线程
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("收到订单处理完成消息,单号为: {}", finishedOrderNumber);
deferredResultHolder.completeOrder(finishedOrderNumber, orderCompletionResult.getResult());
}
},"本地监听线程-监听订单处理完成")
.start();
// 假设是订单系统监听MQ下单消息的线程
new Thread(() -> {
while (true) {
String orderNumber;
synchronized (orderProcessingQueue) {
while (orderProcessingQueue.isEmpty()) {
try {
orderProcessingQueue.wait();
} catch (InterruptedException e) {
}
}
orderNumber = orderProcessingQueue.pollFirst();
orderProcessingQueue.notifyAll();
}
logger.info("收到下单请求,开始执行下单逻辑,单号为: {}", orderNumber);
boolean status;
// 模拟执行下单逻辑
try {
TimeUnit.SECONDS.sleep(2);
status = true;
} catch (Exception e) {
logger.info("下单失败=>{}", e.getMessage());
status = false;
}
// 向 订单处理完成MQ 发送消息
synchronized (orderCompletionQueue) {
orderCompletionQueue.addLast(new OrderCompletionResult(orderNumber, status == true ? "success" : "error"));
logger.info("发送订单完成消息, 单号: {}",orderNumber);
orderCompletionQueue.notifyAll();
}
}
},"订单系统线程-监听下单消息")
.start();
}
}
контрольная работа
2019-08-22 13:22:05.520 INFO 21208 --- [nio-8080-exec-2] t.z.s.web.async.AsyncOrderController : 【请求线程】收到下单请求
2019-08-22 13:22:05.521 INFO 21208 --- [nio-8080-exec-2] t.z.s.web.async.AsyncOrderController : 【请求线程】继续处理其它请求
2019-08-22 13:22:06.022 INFO 21208 --- [ 订单系统线程-监听下单消息] t.z.s.web.async.OrderProcessingListener : 收到下单请求,开始执行下单逻辑,单号为: 104691998710
2019-08-22 13:22:06.022 INFO 21208 --- [地临时线程-向MQ发送下单消息] t.z.s.web.async.AsyncOrderController : 向MQ发送下单消息, 单号: 104691998710
2019-08-22 13:22:08.023 INFO 21208 --- [ 订单系统线程-监听下单消息] t.z.s.web.async.OrderProcessingListener : 发送订单完成消息, 单号: 104691998710
2019-08-22 13:22:08.023 INFO 21208 --- [本地监听线程-监听订单处理完成] t.z.s.web.async.OrderProcessingListener : 收到订单处理完成消息,单号为: 104691998710
перехват асинхронной обработки configureSync, тайм-аут, конфигурация пула потоков
расширяться перед намиWebMvcConfigureAdapter
подклассWebConfig
можно переписать вconfigureAsyncSupport
способ сделать некоторую настройку для асинхронной обработки
registerCallableInterceptors & registerDeferredResultInterceptors
Ранее мы переписалиaddInterceptors
Пара зарегистрированных перехватчиков методомCallable
иDeferredResult
Два вида асинхронной обработки недействительны. Если вы хотите настроить перехватчики для обоих, вам необходимо переопределить эти два метода.
setDefaultTimeout
Установите время ожидания для асинхронной обработки. Если время превышает это время, он ответит напрямую, не дожидаясь завершения асинхронной задачи.
setTaskExecutor
SpringBoot
По умолчанию асинхронные задачи выполняются путем создания новых потоков.После выполнения потоки уничтожаются.Для выполнения асинхронных задач путем мультиплексирования потоков (пулов потоков) вы можете передать через этот метод пользовательский пул потоков.
Переднее и заднее разделение
Документация по интерфейсу Swagger
swagger
Проект может автоматически генерировать интерфейсные документы в соответствии с написанными нами интерфейсами, что нам удобно для разделения фронтенда и бекенда.
полагаться
<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>
в стартовом классеSecurityDemoApplication
добавить@@EnableSwagger2
Аннотация позволяет автоматически генерировать интерфейсные документы и обращаться к ним после запуска.localhost:8080/swagger-ui.html
Общие примечания
-
@ApiOperation
, аннотированный в методе Controller для описания поведения метода@GetMapping @JsonView(User.UserBasicView.class) @ApiOperation("用户查询服务") public List<User> query(String userName, UserQueryConditionDto userQueryConditionDto, Pageable pageable) {
-
@ApiModelProperty
, аннотированный вBean
на поле, используется для описания значения поля@Data public class UserQueryConditionDto { @ApiModelProperty("用户名") private String username; @ApiModelProperty("密码") private String password; @ApiModelProperty("电话号码") private String phone; }
-
@ApiParam
, аннотированные к параметрам метода контроллера, чтобы описать значение параметров@DeleteMapping("/{id:\\d+}") public void delete(@ApiParam("用户id") @PathVariable Long id) { System.out.println(id); }
Документация по интерфейсу будет восстановлена после перезапуска.
WireMock
Чтобы облегчить параллельную разработку передней и задней частей, мы можем использоватьWireMock
в качестве сервера виртуального интерфейса
Когда внутренний интерфейс не разработан, внешний интерфейс может подделывать некоторые статические данные (например, файлы JSON) в качестве результата запроса с помощью локальных файлов.Этот метод подходит, когда внешний интерфейс имеет только один Терминал. Однако, когда существует много видов внешних интерфейсов, таких как ПК, H5, APP, апплет и т. д., каждый из которых фальсифицирует данные в своей локальной области, тогда это кажется немного повторяющимся, и каждый фальсифицирует данные в соответствии со своими потребностями. собственные идеи могут привести к финалу и реальный интерфейс не может быть бесшовно подключен
В настоящее времяwiremock
Появление решения решает эту болевую точку,wiremock
используетсяJava
Разработан независимый сервер, который может предоставлять HTTP-сервисы внешнему миру, мы можем передатьwiremock
Клиент для редактирования/настройкиwiremock
Сервер делает это какweb
Один и тот же сервис предоставляет различные интерфейсы, и нет необходимости в повторном развертывании.
Загрузите и запустите сервис wiremock
вайрмок может бытьjar
способ работы,ссылка для скачивания, перейдите в его каталог после завершения загрузкиcmd
Выполните следующую команду, чтобы начатьwiremock
сервер,--port=
Укажите рабочий порт
java -jar wiremock-standalone-2.24.1.jar --port=8062
полагаться
вводитьwiremock
Клиентские зависимости и их зависимостиhttpclient
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
Поскольку автоматическая совместимость зависимостей уже используется в родительском проекте, нет необходимости указывать номер версии. Затем отредактируйте его через клиентский API.wiremock
сервер, добавляем к нему интерфейс
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(); // 移除所有旧的配置
// 添加配置,一个stub代表一个接口
stubFor(
get(urlEqualTo("/order/1")).
// 设置响应结果
willReturn(
aResponse()
.withBody("{\"id\":1,\"orderNumber\":\"545616156\"}")
.withStatus(200)
)
);
}
}
Вы можете сначала сохранить данные JSON
resources
, затем черезClassPathResource#getFile
иFileUtils#readLines
читать данные как строку
доступlocalhost:8062/order/1
:
{
id: 1,
orderNumber: "545616156"
}
пройти черезWireMock
API, вы можете настроить различные сервисы интерфейса для виртуального сервера
Разработка аутентификации на основе форм с помощью Spring Security
Summary
Основные функции Spring Security
- Аутентификация (кто вы)
- Авторизация (что можно сделать)
- Защита от атак (предотвращение подделки удостоверений, если хакеры могут подделывать удостоверения для входа в систему, две вышеуказанные функции не будут работать)
Содержание этой главы
- Основы безопасности Spring
- Реализовать аутентификацию по имени пользователя и паролю
- Используйте номер мобильного телефона + SMS-аутентификация
Первые впечатления от Spring Security
Security
Есть базовый механизм аутентификации по умолчанию, закомментируем элементы конфигурацииsecurity.basic.enabled=false
(по умолчаниюtrue
), перезапустите, чтобы просмотреть журнал, и вы найдете сообщение
Using default security password: f84e3dea-d231-47a2-b20a-48bac8ed5f1e
Затем мы посещаемGET /user
, появится окно входа, позволяющее нам войти,security
По умолчанию есть встроенный пользователь с именемuser
, пароль есть в логе вышеUsing default security password: f84e3dea-d231-47a2-b20a-48bac8ed5f1e
(пароль будет восстанавливаться каждый раз при перезапуске), мы используем эти две формы входа, и страница возвращается к службе, к которой мы хотим получить доступ.
formLogin
Из этого раздела мыsecurity-browser
Напишите нашу логику аутентификации браузера в модуле
Мы можем сделать это, добавив класс конфигурации (добавивConfiguration
, и расширитьWebSecurityConfigureAdapter
) для настройки метода проверки, логики проверки и т. д., например, для установки метода проверки для формирования проверки:
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
//设置认证方式为表单登录,若未登录而访问受保护的URL则跳转到表单登录页(security帮我们写了一个默认的登录页)
.formLogin()
// 添加其他配置
.and()
// 验证方式配置结束,开始配置验证规则
.authorizeRequests()
// 设置任何请求都需要通过认证
.anyRequest()
.authenticated();
}
}
доступ/user
, перейти на страницу входа по умолчанию/login
(страница входа и URL-адрес входа мы можем настроить), имя пользователяuser
, пароль по-прежнему находится в журнале, а вход в систему выполнен успешно и выполняется переход к/user
httpBasic
Если метод аутентификации установленformLogin
изменить наhttpBasic
этоsecurity
Конфигурация по умолчанию (эквивалентно импортуsecurity
Эффект, что ничего не совпадает после зависимости), то есть выскакивает окно логина
Основы безопасности Spring
три фильтра
как показано на рисунке,Spring Security
Ядро этого на самом деле представляет собой цепочку фильтров, поэтому он ненавязчивый и подключаемый. В цепочке фильтров есть 3 типа фильтров:
-
Фильтр аутентификации
XxxAuthenticationFilter
, отмеченные зеленым на рисунке выше, а имена их классов начинаются сAuthenticationFilter
В конце функция состоит в том, чтобы сохранить информацию для входа. Эти фильтры применяются динамически на основе нашей конфигурации, как мы называли ранее.formLogin()
На самом деле он включен.UsernamePasswordAuthenticationFilter
,перечислитьhttpBaisc()
включенBasicAuthenticationFilter
ближайший позади
Controller
два фильтраExceptionTranslationFilter
иFilterSecurityInterceptor
Содержит основную логику аутентификации, включенную по умолчанию, и мы не можем ее отключить. -
FilterSecurityInterceptor
, хотя и назван в честьInterceptor
конец, но это все ещеFilter
, который является ближайшимController
Фильтр, он будет перехватывать запрос на доступ к соответствующему URL-адресу в соответствии с настроенными нами правилами перехвата (для доступа к каким URL-адресам необходимо войти в систему, для доступа к каким URL-адресам требуются определенные разрешения и т. д.). исходный код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()); ... }
doFilter
это то, что действительно зовет насController
(потому что это конец цепочки фильтров), но перед этим он вызываетbeforeInvocation
Независимо от того, перехватил ли запрос проверку личности и связанных прав, соответствующая ошибка проверки вызовет исключение без сертификации (Unauthenticated
) и несанкционированное исключение (Unauthorized
), эти исключения будутExceptionTranslationFilter
пойманный -
ExceptionTranslationFilter
, как следует из названия, он анализирует исключения, и некоторые из его исходных кодов выглядят следующим образом.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 ... } }
это звонит
chain.doFilter
На самом деле пошелFilterSecurityInterceptor
, будетFilterSecurityInterceptor.doFilter
ВыброситьSpringSecurityException
Аномальная обработка захвата и анализа, например.FilterSecurityInterceptor
выброшенныйUnauthenticated
исключение, тоExceptionTranslationFilter
Он перенаправит на страницу входа или откроет окно входа (в зависимости от того, какой фильтр аутентификации мы настроили), когда мы успешно войдем в систему, фильтр аутентификации перенаправит на URL-адрес, который мы изначально хотели посетить.
отладка точки останова
Мы можем проверить вышеизложенное с помощью отладки точки останова и установить метод проверки наformLogin
, то после 3-х фильтров иController
В точках останова перезапустите доступ к службе/user
Пользовательская логика аутентификации пользователей
Логика получения информации о пользователях — UserDetailsService
До сих пор мы вошли черезuser
и пароль, сгенерированный журналом запуска, которыйsecurity
имеет встроенныйuser
Пользователь. В реальных проектах у нас обычно есть таблица, предназначенная для хранения пользователей, которые будут передаваться черезjdbc
Либо читать информацию о пользователях из других СХД, тогда нам нужно настроить логику чтения информации о пользователях, реализовавUserDetailsService
интерфейс может сказатьsecurity
Как получить информацию о пользователе из
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("登录用户名: " + username);
// 实际项目中你可以调用Dao或Repository来查询用户是否存在
if (Objects.equals(username, "admin") == false) {
throw new UsernameNotFoundException("用户名不存在");
}
// 在查询到用户后需要将相关信息包装成一个UserDetails实例返回给security,这里的User是security提供的一个实现
// 第三个参数需要传一个权限集合,这里使用了一个security提供的工具类将用分号分隔的权限字符串转成权限集合,本来应该从用户权限表查询的
return new org.springframework.security.core.userdetails.User(
"admin","123456", AuthorityUtils.commaSeparatedStringToAuthorityList("user,admin")
);
}
}
После перезапуска службы можно пройти толькоadmin,123456
войти
Обработка логики проверки пользователя — UserDetails
Давайте взглянемUserDetails
Исходный код интерфейса
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
// 用来和用户登录时填写的密码进行比对
String getPassword();
String getUsername();
// 账户是否是非过期的
boolean isAccountNonExpired();
// 账户是否是非冻结的
boolean isAccountNonLocked();
// 密码是否是非过期的,有些安全性较高的系统需要账户每隔一段时间更换密码
boolean isCredentialsNonExpired();
// 账户是否可用,可以对应逻辑删除字段
boolean isEnabled();
}
переписывание сis
При использовании первых четырех методов, если не требуется соответствующего решения, вернутьtrue
То есть, например, класс сущности, соответствующий пользовательской таблице, выглядит следующим образом
@Data
public class User{
private Long id;
private String username;
private String password;
private String phone;
private int deleted; //0-"正常的",1-"已删除的"
private int accountNonLocked; //0-"账号未被冻结",1-"账号已被冻结"
}
Для удобства мы можем напрямую использовать класс сущности для достиженияUserDetails
интерфейс
@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(){
return pwd;
}
public String getUsername(){
return uname;
}
public boolean isAccountNonExpired(){
return true;
}
public boolean isAccountNonLocked(){
return accountNonLocked == 0;
}
public boolean isCredentialsNonExpired(){
return true;
}
public boolean isEnabled(){
return deleted == 0;
}
}
Обработка шифрования и дешифрования пароля - PasswordenCoder
Поле пароля в пользовательской таблице обычно хранит не открытый текст пароля, а зашифрованный зашифрованный текст.PasswordEncoder
поддерживается:
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
}
Когда мы вставляем пользователя в базу данных, нам нужно вызватьencode
Зашифруйте текстовый пароль перед вставкой; когда пользователь входит в систему,security
позвонюmatches
Сравните поверхность зашифрованного текста, которую мы нашли в базе данных, с паролем открытого текста, отправленным пользователем.
security
Предоставляет нам асимметричное шифрование этого интерфейса (для одного и того же открытого пароля каждый вызовencode
Получаемые шифртексты все разные, только черезmatches
Чтобы сравнить, соответствуют ли открытый текст и зашифрованный текст) класс реализацииBCryptPasswordEncoder
, нам нужно только настроитьBean
,security
подумаешь, что мы вернулисьUserDetails
изgetPassword
Возвращаемый пароль черезBean
Зашифровано (поэтому будьте осторожны, вызывая это при вставке пользователейBean
изencode
Зашифруйте пароль и вставьте его в базу данных)
@Configuration
public class SecurityBrowserConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@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("登录用户名: " + username);
// 实际项目中你可以调用Dao或Repository来查询用户是否存在
if (Objects.equals(username, "admin") == false) {
throw new UsernameNotFoundException("用户名不存在");
}
// 假设查出来的密码如下
String pwd = passwordEncoder.encode("123456");
return new org.springframework.security.core.userdetails.User(
"admin", pwd, AuthorityUtils.commaSeparatedStringToAuthorityList("user,admin")
);
}
}
BCryptPasswordEncoder
Он не обязательно используется только для шифрования и проверки паролей, мы все можем использовать его для функций, связанных с шифрованием, в повседневной разработке.encode
также можно использовать методmatches
Метод сравнивает, является ли зашифрованный текст результатом шифрования открытого текста.
Персонализированный процесс аутентификации пользователя
Пользовательская страница входа
существуетformLogin()
после использованияloginPage()
Вы можете указать страницу входа и не забудьте отключить перехват URL-адреса;UsernamePasswordAuthenticationFilter
По умолчанию перехватывать отправку на/login
изPOST
Запросите и получите данные для входа, если вы хотите, чтобы форма была заполненаaction
не для/post
, то можно настроитьloginProcessingUrl
сделатьUsernamePasswordAuthenticationFilter
Соответствующий
@Configuration
public class SecurityBrowserConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//设置认证方式为表单登录,若未登录而访问受保护的URL则跳转到表单登录页(security帮我们写了一个默认的登录页)
.formLogin()
.loginPage("/sign-in.html").loginProcessingUrl("/auth/login")
.and()
// 验证方式配置结束,开始配置验证规则
.authorizeRequests()
// 登录页面不需要拦截
.antMatchers("/sign-in.html").permitAll()
// 设置任何请求都需要通过认证
.anyRequest().authenticated();
}
}
Пользовательская страница входа:security-browser/src/main/resource/resources/sign-in.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<body>
<form action="/auth/login" method="post">
用户名: <input type="text" name="username">
密码: <input type="password" name="password">
<button type="submit">提交</button>
</form>
</body>
</html>
доступ после перезагрузкиGET /user
, адаптированный к странице входа, которую мы написалиsign-in.html
,заполнятьadmin,123456
Войдите в систему и обнаружите, что ошибка выглядит следующим образом
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'.
Это потому чтоsecurity
Предотвращение межсайтовых запросов CSRF включен по умолчанию (например, с помощью HTTP-клиента).Postman
Так же можно сделать такой запрос на вход), сначала отключаем его
http
.formLogin()
.loginPage("/sign-in.html").loginProcessingUrl("/auth/login")
.and()
.authorizeRequests()
.antMatchers("/sign-in.html").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable()
перезапустить доступGET /user
, после перехода для входа в систему он автоматически вернется к/user
пользовательская страница входа в систему успешно
Логин отдыха логин
Поскольку мы основываемся наREST
service, поэтому, если это запрос не из браузера, мы должны вернуть код состояния 401, сообщающий клиенту, что требуется аутентификация, вместо перенаправления на страницу входа.
Тогда мы не можемloginPage
Записывается как путь к странице входа, но должен быть перенаправлен наController
,Зависит отController
Определите, переходит ли пользователь, когда браузер получает доступ к странице или когда не-браузер, такой как Android, обращается к службе REST, если первый перенаправляется на страницу входа, а второй — код состояния ответа 401 и сообщение JSON.
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会将跳转前的请求存储在session中
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@RequestMapping("/auth/require")
// 该注解可设置响应状态码
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public SimpleResponseResult requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 从session中取出跳转前用户访问的URL
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest != null) {
String redirectUrl = savedRequest.getRedirectUrl();
logger.info("引发跳转到/auth/login的请求是: {}", redirectUrl);
if (StringUtils.endsWithIgnoreCase(redirectUrl, ".html")) {
// 如果用户是访问html页面被FilterSecurityInterceptor拦截从而跳转到了/auth/login,那么就重定向到登录页面
redirectStrategy.sendRedirect(request, response, "/sign-in.html");
}
}
// 如果不是访问html而被拦截跳转到了/auth/login,则返回JSON提示
return new SimpleResponseResult("用户未登录,请引导用户至登录页");
}
}
@Configuration
public class SecurityBrowserConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
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();
}
}
Рефакторинг — конфигурация вместо хардкода
благодаря нашемуsecurity-browser
Модули разрабатываются как модули многократного использования и должны поддерживать пользовательские конфигурации, такие как внедрение нашегоsecurity-browser
После модуля они должны иметь возможность настроить свою собственную страницу входа, если они не используют ту, которую мы предоставляем по умолчанию.sign-in.html
, чтобы сделать это, нам нужно предоставить некоторые элементы конфигурации, например, кто-то еще представляет нашуsecurity-browser
После этого, добавивdemo.security.browser.loginPage=/login.html
сможет передать свой проектlogin.html
Заменить нашsign-in.html
из-за последующегоsecurity-app
Также может потребоваться поддержка подобных конфигураций, поэтому мыsecurity-core
Определите общий класс конфигурации для инкапсуляции различных элементов конфигурации каждого модуля.
security-core
класс в:
package top.zhenganwen.security.core.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author zhenganwen
* @date 2019/8/23
* @desc SecurityProperties 封装整个项目各模块的配置项
*/
@Data
@ConfigurationProperties(prefix = "demo.security")
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
}
package top.zhenganwen.security.core.properties;
import lombok.Data;
/**
* @author zhenganwen
* @date 2019/8/23
* @desc BrowserProperties 封装security-browser模块的配置项
*/
@Data
public class BrowserProperties {
private String loginPage = "/sign-in.html"; //提供一个默认的登录页
}
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
// 启用在启动时将application.properties中的demo.security前缀的配置项注入到SecurityProperties中
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
}
затем вsecurity-browser
генерал-лейтенантSecurityProperties
Введенная логика в файле конфигурации, которая будет перенаправлять на страницу входа, зависит отdemo.security.browser.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("引发跳转到/auth/login的请求是: {}", redirectUrl);
if (StringUtils.endsWithIgnoreCase(redirectUrl, ".html")) {
redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
}
}
return new SimpleResponseResult("用户未登录,请引导用户至登录页");
}
}
Установите динамический URL-адрес незаблокированной страницы входа.
@Configuration
public class SecurityBrowserConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
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()
// 将不拦截的登录页URL设置为动态的
.antMatchers(securityProperties.getBrowser().getLoginPage()).permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
}
Теперь мы будемsecurity-demo
Модули используются как сторонние приложения, с использованием многоразовыхsecurity-browser
Во-первых, чтобыsecurity-demo
Стартовый класс для модуляSecurityDemoApplication
перейти кtop.zhenganwen.securitydemo
под пакетом, чтобы убедиться, что он может быть отсканированsecurity-core
внизtop.zhenganwen.securitydemo.core.SecurityCoreConfig
иsecurity-browser
внизtop.zhenganwen.securitydemo.browser.SecurityBrowserConfig
Затем вsecurity-demo
изapplication.properties
Добавьте элементы конфигурации вdemo.security.browser.loginPage=/login.html
И вresources
Новое строительствоresources
папки иlogin.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Security Demo应用的登录页面</h1>
<form action="/auth/login" method="post">
用户名: <input type="text" name="username">
密码: <input type="password" name="password">
<button type="submit">提交</button>
</form>
</body>
</html>
Перезапустите службу, посетите/user.html
нашел прыгнул кlogin.html
; закомментируйтеdemo.security.browser.loginPage=/login.html
, а затем перезагрузите доступ к обслуживанию/user.html
нашел прыгнул кsign-in.html
, реконструкция прошла успешно!
Пользовательская обработка успешного входа в систему — AuthenticationSuccessHandler
security
Логика обработки успешного входа в систему заключается в перенаправлении на ранее перехваченный запрос по умолчанию, но для служб REST внешним интерфейсом может быть запрос AJAX для входа в систему, а желаемым ответом является соответствующая информация о пользователе. очевидно, что вам неуместно перенаправлять его. Чтобы настроить обработку после успешного входа в систему, нам нужно реализоватьAuthenticationSuccessHandler
интерфейс
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("用户{}登录成功", authentication.getName());
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
response.getWriter().flush();
}
}
После успешного входа мы получимAuthentication
, что такжеsecurity
Один из основных интерфейсов , который используется для инкапсуляции релевантной информации о пользователе. Здесь мы преобразуем ее в строку JSON-ответа внешнему интерфейсу, чтобы увидеть, что она содержит.
Нам тоже нужно пройтиsuccessHandler()
настроить его наHttpSecurity
чтобы он вступил в силу (заменяет логику обработки успешного входа по умолчанию):
@Configuration
public class SecurityBrowserConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
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();
}
}
Перезапустите службу, посетите/login.html
и войдите в систему:
{
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"
}
Его можно найтиAuthentication
Содержит следующую информацию
-
authorities
, разрешения, соответствующиеUserDetials
серединаgetAuthorities()
вернуть результат -
details
, сеанс, IP-адрес клиента и SESSIONID этого сеанса -
authenticated
, пройти ли сертификацию -
principle
,соответствоватьUserDetailsService
серединаloadUserByUsername
возвращениеUserDetails
-
credentials
,пароль,security
По умолчанию обрабатывается, и пароль не возвращается на внешний интерфейс -
name
,имя пользователя
Здесь, поскольку мы формируем логин, возвращается указанная выше информация, а затем мы выполняем сторонний логин, такой как WeChat и QQ, затемAuthentication
Содержащаяся информация может быть другой, то есть переписаннойonAuthenticationSuccess
Метод вводаAuthentication
будут отправлены нам в соответствии с различными методами входаAuthentication
реализовать объект класса
Пользовательская обработка ошибок при входе в систему — AuthenticationFailureHandler
В соответствии с обработкой успешного входа в систему также можно настроить обработку неудачных попыток входа.
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("登录失败=>{}", exception.getMessage());
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString(exception));
response.getWriter().flush();
}
}
@Configuration
public class SecurityBrowserConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
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();
}
}
доступ/login.html
Введите неверный пароль для входа:
{
cause: null,
stackTrace: [...],
localizedMessage: "坏的凭证",
message: "坏的凭证",
suppressed: [ ]
}
рефакторинг
так какsecurity-browser
Чтобы стать многоразовым модулем, мы должны извлечь стратегию обработки успешного/неудачного входа в систему и позволить сторонним приложениям свободно выбирать.В это время мы можем добавить еще один элемент конфигурации.demo.security.browser.loginProcessType
переключить наsecurity-core
:
package top.zhenganwen.security.core.properties;
/**
* @author zhenganwen
* @date 2019/8/24
* @desc LoginProcessTypeEnum
*/
public enum LoginProcessTypeEnum {
// 重定向到之前的请求页或登录失败页
REDIRECT("redirect"),
// 登录成功返回用户信息,登录失败返回错误信息
JSON("json");
private String type;
LoginProcessTypeEnum(String type) {
this.type = type;
}
}
@Data
public class BrowserProperties {
private String loginPage = "/sign-in.html";
private LoginProcessTypeEnum loginProcessType = LoginProcessTypeEnum.JSON; //默认返回JSON信息
}
Переработан обработчик успешного/неудачного входа, гдеSavedRequestAwareAuthenticationSuccessHandler
иSimpleUrlAuthenticationFailureHandler
этоsecurity
Предоставлен обработчик успешного входа в систему по умолчанию (переход на страницу, запрошенную перед входом в систему) и неудачный вход в систему (переход на страницу исключения).
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) {
// 重定向到缓存在session中的登录前请求的URL
super.onAuthenticationSuccess(request, response, authentication);
return;
}
logger.info("用户{}登录成功", authentication.getName());
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
response.getWriter().flush();
}
}
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("登录失败=>{}", exception.getMessage());
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString(exception));
response.getWriter().flush();
}
}
доступ/login.html
, соответственно выполнить тесты успешного входа и неудачного входа и вернуть ответ JSON.
существуетsecurity-demo
середина
-
application.properties
добавлено вdemo.security.browser.loginProcessType=redirect
-
новый
/resources/resources/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>Spring Demo应用首页</h1> </body> </html>
-
новый
/resources/resources/401.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>login fail!</h1> </body> </html>
Перезапустите службу, успешно войдите в систему и перейдите кindex.html
, если не удается войти, перейдите к401.html
Подробное объяснение процесса сертификации на уровне исходного кода
После двух предыдущих разделов мы использовалиsecurity
Некоторые из основных функций доступны, но все они фрагментарны, и общее представление о процессе все еще неясно. Зная это и зная почему, мы должны анализировать при входе в системуsecurity
что ты сделал для нас?
Процесс аутентификации
На приведенном выше рисунке показан общий поток обработки входа в систему, фильтр запроса на входXxxAutenticationFilter
После перехвата запроса на вход информация для входа инкапсулируется вauthenticated=false
изAuthentication
перейти кAuthenticationManager
Поможем проверить,AuthenticationManager
Он не будет сам выполнять логику проверки, он будет делегироватьAuthenticationProvider
помогите проверить,AuthenticationProvider
Во время процесса проверки будет выдано исключение ошибки проверки, или проход проверки вернет новыйUserDetials
изAuthentication
return, фильтр запроса полученXxxAuthenticationFilter
После успешного входа в систему будет вызван процессор для выполнения успешной логики входа в систему.
Мы используем метод входа в форму имени пользователя и пароля для отладки точки останова и пошагового анализа процесса проверки.Другие методы входа аналогичны.
Как результаты аутентификации распределяются между несколькими запросами
Чтобы обмениваться данными между несколькими запросами, вам нужно использоватьsession
, Давайте посмотрим наsecurity
положить что-нибудь вsession
, когда будетsession
Читать
В предыдущем разделе было сказано, чтоAbstractAuthenticationProcessingFilter
``делать фильтр方法中,校验成功之后会调用
successAuthentication(request, response, chain, authResult)`, давайте посмотрим, что делает этот метод
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);
}
Можно обнаружить, что перед вызовом логики обработки обработчика успешного входа следует вызвать методSecurityContextHolder.getContext().setAuthentication(authResult)
, видеть, чтоSecurityContextHolder.getContext()
это получить текущую привязку потокаSecurityContext
(Его можно рассматривать как переменную потока, областью действия является жизненный цикл потока), иSecurityContext
На самом деле это правильноAuthentication
один слой упаковки
public class SecurityContextHolder {
private static SecurityContextHolderStrategy strategy;
public static SecurityContext getContext() {
return strategy.getContext();
}
}
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<SecurityContext>();
public SecurityContext getContext() {
SecurityContext ctx = contextHolder.get();
if (ctx == null) {
ctx = createEmptyContext();
contextHolder.set(ctx);
}
return ctx;
}
}
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
public class SecurityContextImpl implements SecurityContext {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
public Authentication getAuthentication() {
return authentication;
}
public int hashCode() {
if (this.authentication == null) {
return -1;
}
else {
return this.authentication.hashCode();
}
}
public void setAuthentication(Authentication authentication) {
this.authentication = authentication;
}
...
}
тогда будетAuthentication
сохранено в текущей темеSecurityContext
Какова цель?
Это включает в себя еще один специальный фильтрSecurityContextPersistenceFilter
,Это находитсяsecurity
Передняя часть всей цепочки фильтров:
private SecurityContextRepository repo;
// 请求到达的第一个过滤器
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
...
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,response);
// 从Session中获取SecurityContext,未登录时获取的则是空
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
try {
// 将SecurityContext保存到当前线程的ThreadLocalMap中
SecurityContextHolder.setContext(contextBeforeChainExecution);
// 执行后续过滤器和Controller方法
chain.doFilter(holder.getRequest(), holder.getResponse());
}
// 在请求响应时经过的最后一个过滤器
finally {
// 从当前线程获取SecurityContext
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
SecurityContextHolder.clearContext();
// 将SecurityContext持久化到Session
repo.saveContext(contextAfterChainExecution, holder.getRequest(),holder.getResponse());
...
}
}
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);
...
return context;
}
...
}
Получить аутентифицированную информацию о пользователе
В нашем коде мы можем передавать статические методыSecurityContextHolder.getContext().getAuthentication
для получения информации о пользователе, или вы можете напрямуюController
Заявление о вступленииAuthentication
,security
Поможет нам автоматически залить, если вы просто хотите получитьAuthentication
серединаUserDetails
соответствующую часть, вы можете использовать@AuthenticationPrinciple UserDetails currentUser
@GetMapping("/info1")
public Object info1() {
return SecurityContextHolder.getContext().getAuthentication();
}
@GetMapping("/info2")
public Object info2(Authentication authentication) {
return authentication;
}
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"
}
@GetMapping("/info3")
public Object info3(@AuthenticationPrincipal UserDetails currentUser) {
return currentUser;
}
GET /user/info3
{
password: null,
username: "admin",
authorities: [
{
authority: "admin"
},
{
authority: "user"
}
],
accountNonExpired: true,
accountNonLocked: true,
credentialsNonExpired: true,
enabled: true
}
использованная литература
-
видеоурок
Ссылка на сайт:disk.baidu.com/is/1wqWD4we0…Код извлечения: z6zi