Практичные галантереи Spring Security: поэкспериментируйте с пользовательским входом в систему

Spring Boot

1. Введение

предыдущий оSpring SecurityСоответствующая статья — это просто разминка. Для лучшего реального боя в будущем, если вы пропустили это, пожалуйстаБоевая серия Spring SecurityНачинать. Первым шагом в безопасном доступе является аутентификация (Authentication), первым шагом аутентификации является вход в систему. Сегодня мы собираемсяSpring Securityнастройки, для разработки расширяемой, масштабируемойformФункция входа.

2. Процесс входа в форму

НижеformОсновной процесс входа в систему:

только еслиformВход в систему может быть в основном преобразован в описанный выше процесс. Далее посмотримSpring Securityкак это обрабатывается.

3. Войдите в Spring Security

вчерашний деньSpring Security для борьбы с галантерейными товарами: запись класса пользовательской конфигурации WebSecurityConfigurerAdapterМы уже упоминали, что наш обычный настраиваемый контроль доступа в основном осуществляется черезHttpSecurityстроить. По умолчанию он предоставляет три метода входа в систему:

  • formLogin()обычная форма входа
  • oauth2Login()на основеOAuth2.0Соглашение об аутентификации/авторизации
  • openidLogin()на основеOpenIDСпецификация аутентификации

Все вышеперечисленные три методаAbstractAuthenticationFilterConfigurerосуществленный,

4. Форма входа в форму в HttpSecurity

Есть два способа включить форму входа в систему: один черезHttpSecurityизapply(C configurer)метод построенияAbstractAuthenticationFilterConfigurerРеализация этого — относительно продвинутый геймплей. Другой - наше общее использованиеHttpSecurityизformLogin()способ настройкиFormLoginConfigurer. Начнем с более обычного второго.

4.1 FormLoginConfigurer

КлассformКласс конфигурации для входа в форму. Он предоставляет некоторые из наших часто используемых методов настройки:

  • loginPage(String loginPage): АвторизоватьсястраницаВместо интерфейса нам нужно преобразовать режим разделения спереди и сзади, по умолчанию/login.
  • loginProcessingUrl(String loginProcessingUrl)Фактическая форма отправляет информацию о пользователе в фоновом режиме.Action, а затем по фильтруUsernamePasswordAuthenticationFilterобработка перехвата,ActionНа самом деле не обрабатывает никакой логики.
  • usernameParameter(String usernameParameter)Используется для настройки имени параметра пользователя, значение по умолчаниюusername.
  • passwordParameter(String passwordParameter)Используется для настройки имени пароля пользователя, по умолчаниюpassword
  • failureUrl(String authenticationFailureUrl)Он будет перенаправлен на этот путь после неудачного входа в систему. Как правило, он не будет использоваться для прямого разделения.
  • failureForwardUrl(String forwardUrl)Ошибка входа в систему будет перенаправлена ​​​​на это, и это обычно используется для разделения передней и задней части. может определитьController(контроллер) для обработки возвращаемого значения, но будьте осторожныRequestMethod.
  • defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse)По умолчанию перейти к этому после успешного входа в систему, еслиalwaysUseзаtrueПока процесс аутентификации выполняется и успешен, он всегда будет переходить к этому. Обычно рекомендуемые значения по умолчаниюfalse
  • successForwardUrl(String forwardUrl)Эффект тот же, что и вышеdefaultSuccessUrlизalwaysUseзаtrueНо будь остороженRequestMethod.
  • successHandler(AuthenticationSuccessHandler successHandler)Пользовательский обработчик успеха аутентификации, который может заменить все вышеперечисленное.successСпособ
  • failureHandler(AuthenticationFailureHandler authenticationFailureHandler)Пользовательский обработчик успеха отказа, который может заменить все вышеперечисленноеfailureСпособ
  • permitAll(boolean permitAll) formОсвобождать ли форму входа

Зная это, мы можем придумать индивидуальный логин.

5. Фактический бой с агрегированным входом в систему Spring Security

Далее следует наша самая захватывающая операция входа в систему. Если сомневаетесь, прочтите внимательноВесна в действииСерия разогревающих статей.

5.1 Простые требования

Доступ к нашему интерфейсу должен быть аутентифицирован, и после ошибки входа будет возвращено сообщение об ошибке (json).Не забывайте снижать чувствительность в реальном бою.).

Мы определяем контроллер, который обрабатывает успех и неудачу:

 @RestController
 @RequestMapping("/login")
 public class LoginController {
     @Resource
     private SysUserService sysUserService;
 
     /**
      * 登录失败返回 401 以及提示信息.
      *
      * @return the rest
      */
     @PostMapping("/failure")
     public Rest loginFailure() {
 
         return RestBody.failure(HttpStatus.UNAUTHORIZED.value(), "登录失败了,老哥");
     }
 
     /**
      * 登录成功后拿到个人信息.
      *
      * @return the rest
      */
     @PostMapping("/success")
     public Rest loginSuccess() {
           // 登录成功后用户的认证信息 UserDetails会存在 安全上下文寄存器 SecurityContextHolder 中
         User principal = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
         String username = principal.getUsername();
         SysUser sysUser = sysUserService.queryByUsername(username);
         // 脱敏
         sysUser.setEncodePassword("[PROTECT]");
         return RestBody.okData(sysUser,"登录成功");
     }
 }

Затем мы настраиваем конфигурацию для переопределенияvoid configure(HttpSecurity http)Метод настроен следующим образом (здесь нужно отключить crsf):

 @Configuration
 @ConditionalOnClass(WebSecurityConfigurerAdapter.class)
 @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
 public class CustomSpringBootWebSecurityConfiguration {
 
     @Configuration
     @Order(SecurityProperties.BASIC_AUTH_ORDER)
     static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
         @Override
         protected void configure(AuthenticationManagerBuilder auth) throws Exception {
             super.configure(auth);
         }
 
         @Override
         public void configure(WebSecurity web) throws Exception {
             super.configure(web);
         }
 
         @Override
         protected void configure(HttpSecurity http) throws Exception {
             http.csrf().disable()
                     .cors()
                     .and()
                     .authorizeRequests().anyRequest().authenticated()
                     .and()
                     .formLogin()
                     .loginProcessingUrl("/process")
                     .successForwardUrl("/login/success").
                     failureForwardUrl("/login/failure");
 
         }
     }
 }

Используйте Postman или другие инструменты для отправки форм в стиле Posthttp://localhost:8080/process?username=Felordcn&password=12345Информация о пользователе будет возвращена:

 {
     "httpStatus": 200,
     "data": {
         "userId": 1,
         "username": "Felordcn",
         "encodePassword": "[PROTECT]",
         "age": 18
     },
     "msg": "登录成功",
     "identifier": ""
 }

После изменения пароля на другое значение и повторного запроса аутентификации:

  {
      "httpStatus": 401,
      "data": null,
      "msg": "登录失败了,老哥",
      "identifier": "-9999"
  }

6. Простая реализация нескольких методов входа в систему

Это конец? Сейчас есть много способов войти. Есть обычные текстовые сообщения, электронные письма и сканирование кода.Третья сторона не входит в рамки сегодняшнего дня. Как вести себя с продакт-менеджерами, у которых много идей? Давайте создадим метод входа в систему, который можно расширить до различных поз. мы выше2. Процесс входа в формусерединаПользовательиопределениеДобавьте переходник между ними для адаптации. Мы знаем это так называемоеопределениеэтоUsernamePasswordAuthenticationFilter.

нам просто нужно гарантироватьuriнастроен на вышеперечисленное/processи может пройтиgetParameter(String name)Получить имя пользователя и пароль.

Я вдруг чувствую, что могу подражатьDelegatingPasswordEncoderСпособ сделать это — вести реестр для реализации различных стратегий обработки. Конечно, мы должны реализоватьGenericFilterBeanсуществуетUsernamePasswordAuthenticationFilterвыполнял раньше. Также разработайте стратегию входа.

6.1 Определение метода входа в систему

Определяет перечисление методов входа в систему ``.


  public enum LoginTypeEnum {
  
      /**
       * 原始登录方式.
       */
      FORM,
      /**
       * Json 提交.
       */
      JSON,
      /**
       * 验证码.
       */
      CAPTCHA
  
  }

6.2 Определение интерфейса препроцессора

Интерфейс препроцессора определен для обработки полученных параметров входа в систему с различными характеристиками и для обработки определенной логики. Это оправдание на самом деле немного произвольно, главное, чтобы вы научились думать. Я реализовал значение по умолчаниюform' 表单登录 和 通过RequestBody放入Есть два способа json`, которые не будут показаны здесь из-за нехватки места. конкретныйDEMOСм. внизу.

   public interface LoginPostProcessor {
   
   
   
       /**
        * 获取 登录类型
        *
        * @return the type
        */
       LoginTypeEnum getLoginTypeEnum();
   
       /**
        * 获取用户名
        *
        * @param request the request
        * @return the string
        */
       String obtainUsername(ServletRequest request);
   
       /**
        * 获取密码
        *
        * @param request the request
        * @return the string
        */
       String obtainPassword(ServletRequest request);
   
   }

6.3 Реализация фильтра предварительной обработки входа

Фильтр поддерживаетLoginPostProcessorтаблица сопоставления. Через внешний интерфейс, чтобы определить метод входа в систему для стратегической предварительной обработки, он в конечном итоге будет переданUsernamePasswordAuthenticationFilter. пройти черезHttpSecurityизaddFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class)метод добавлен.

 package cn.felord.spring.security.filter;
 
 import cn.felord.spring.security.enumation.LoginTypeEnum;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.Assert;
 import org.springframework.util.CollectionUtils;
 import org.springframework.web.filter.GenericFilterBean;
 
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 
 import static org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY;
 import static org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY;
 
 /**
  * 预登录控制器
  *
  * @author Felordcn
  * @since 16 :21 2019/10/17
  */
 public class PreLoginFilter extends GenericFilterBean {
 
 
     private static final String LOGIN_TYPE_KEY = "login_type";
 
 
     private RequestMatcher requiresAuthenticationRequestMatcher;
     private Map<LoginTypeEnum, LoginPostProcessor> processors = new HashMap<>();
 
 
     public PreLoginFilter(String loginProcessingUrl, Collection<LoginPostProcessor> loginPostProcessors) {
         Assert.notNull(loginProcessingUrl, "loginProcessingUrl must not be null");
         requiresAuthenticationRequestMatcher = new AntPathRequestMatcher(loginProcessingUrl, "POST");
         LoginPostProcessor loginPostProcessor = defaultLoginPostProcessor();
         processors.put(loginPostProcessor.getLoginTypeEnum(), loginPostProcessor);
 
         if (!CollectionUtils.isEmpty(loginPostProcessors)) {
             loginPostProcessors.forEach(element -> processors.put(element.getLoginTypeEnum(), element));
         }
 
     }
 
 
     private LoginTypeEnum getTypeFromReq(ServletRequest request) {
         String parameter = request.getParameter(LOGIN_TYPE_KEY);
 
         int i = Integer.parseInt(parameter);
         LoginTypeEnum[] values = LoginTypeEnum.values();
         return values[i];
     }
 
 
     /**
      * 默认还是Form .
      *
      * @return the login post processor
      */
     private LoginPostProcessor defaultLoginPostProcessor() {
         return new LoginPostProcessor() {
 
 
             @Override
             public LoginTypeEnum getLoginTypeEnum() {
 
                 return LoginTypeEnum.FORM;
             }
 
             @Override
             public String obtainUsername(ServletRequest request) {
                 return request.getParameter(SPRING_SECURITY_FORM_USERNAME_KEY);
             }
 
             @Override
             public String obtainPassword(ServletRequest request) {
                 return request.getParameter(SPRING_SECURITY_FORM_PASSWORD_KEY);
             }
         };
     }
 
 
     @Override
     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
         ParameterRequestWrapper parameterRequestWrapper = new ParameterRequestWrapper((HttpServletRequest) request);
         if (requiresAuthenticationRequestMatcher.matches((HttpServletRequest) request)) {
 
             LoginTypeEnum typeFromReq = getTypeFromReq(request);
 
             LoginPostProcessor loginPostProcessor = processors.get(typeFromReq);
 
 
             String username = loginPostProcessor.obtainUsername(request);
 
             String password = loginPostProcessor.obtainPassword(request);
 
 
             parameterRequestWrapper.setAttribute(SPRING_SECURITY_FORM_USERNAME_KEY, username);
             parameterRequestWrapper.setAttribute(SPRING_SECURITY_FORM_PASSWORD_KEY, password);
 
         }
 
         chain.doFilter(parameterRequestWrapper, response);
 
 
     }
 }

6.4 Проверка

пройти черезPOSTспособ отправки формыhttp://localhost:8080/process?username=Felordcn&password=12345&login_type=0Запрос может быть успешным. Или вы также можете успешно отправить следующие способы:

Больше способов просто нужно реализовать интерфейсLoginPostProcessorинъекцияPreLoginFilter

7. Резюме

Сегодня, благодаря использованию различных технологий, мы реализовали практическое применение сосуществования множества способов от простого входа в систему до динамического расширения. Я думаю, что для вас будет много квитанций.На этот раз **code DEMO можно оплатить, подписавшись на официальный аккаунт:FelordcnОтветитьss03Бери, дальше будет интереснее.

关注公众号:Felordcn获取更多资讯

Личный блог: https://felord.cn