Пользовательская логика аутентификации Spring Security

задняя часть Spring
Пользовательская логика аутентификации Spring Security

Это второй день моего участия в первом испытании обновлений 2022. Подробную информацию о мероприятии см.:Вызов первого обновления 2022 г.

Содержание этой статьи основано наПроцесс аутентификации Spring SecurityЕсли вы не понимаете, вы можете прочитать эту статью:Процесс аутентификации Spring Security.

анализировать проблему

Ниже приведена блок-схема встроенной аутентификации имени пользователя и пароля Spring Security, с которой мы можем начать:

image.png

В соответствии с приведенным выше рисунком мы можем настроить процесс аутентификации, как кошка и тигр, например, аутентификацию с помощью SMS-кода мобильного телефона. На рисунке я выделил разными цветами основные звенья, участвующие в процессе: синий блок — это часть, соответствующая аутентификации по имени пользователя/паролю, а зеленый блок — это часть, которая не имеет никакого отношения к конкретному методу аутентификации. логика.

Таким образом, мы можем разработать нашу пользовательскую логику в соответствии с классами в синей части, которые в основном включают следующее:

  • обычайAuthenticationкласс реализации, сUsernamePasswordAuthenticationTokenАналогично, используется для сохранения информации аутентификации.
  • настраиваемый фильтр сUsernamePasswordAuthenticationFilterТочно так же для конкретного запроса информация аутентификации инкапсулируется и вызывается логика аутентификации.
  • Один AuthenticationProviderКласс реализации , который обеспечивает логику аутентификации иDaoAuthenticationProviderАналогичный.

Затем возьмите в качестве примера аутентификацию с кодом подтверждения мобильного телефона, чтобы выполнить их одну за другой.

Пользовательская аутентификация

Сначала дайте код, а потом объясните:

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private final Object principal;

    private Object credentials;

    public SmsCodeAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }
    public SmsCodeAuthenticationToken(Object principal, Object credentials,
                                      Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return this.credentials;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        Assert.isTrue(!isAuthenticated,
                "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}

а такжеUsernamePasswordAuthenticationTokenто же, наследствоAbstractAuthenticationTokenАбстрактный класс, который необходимо реализоватьgetPrincipalа такжеgetCredentialsдва метода. При аутентификации по имени пользователя/паролю принципал представляет имя пользователя, а учетные данные представляют пароль.Здесь мы можем позволить им ссылаться на номер мобильного телефона и код подтверждения.Поэтому мы добавляем эти два свойства, а затем реализуем метод.

Кроме того, нам нужно написать два конструктора для создания неаутентифицированной и успешно аутентифицированной аутентификационной информации.

пользовательский фильтр

В этой части вы можете обратиться кUsernamePasswordAuthenticationFilterнаписать. Или онлайн-код:

public class SmsCodeAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {

    public static final String FORM_MOBILE_KEY = "mobile";
    public static final String FORM_SMS_CODE_KEY = "smsCode";

    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login",
            "POST");

    private boolean postOnly = true;

    protected SmsCodeAuthenticationProcessingFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        String mobile = obtainMobile(request);
        mobile = (mobile != null) ? mobile : "";
        mobile = mobile.trim();
        String smsCode = obtainSmsCode(request);
        smsCode = (smsCode != null) ? smsCode : "";
        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile, smsCode);
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    private String obtainMobile(HttpServletRequest request) {
        return request.getParameter(FORM_MOBILE_KEY);
    }

    private String obtainSmsCode(HttpServletRequest request) {
        return request.getParameter(FORM_SMS_CODE_KEY);
    }

    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }
}

Эта часть относительно проста, ключевые моменты заключаются в следующем:

  • Прежде всего, конструктор по умолчанию указывает фильтр для соответствия тем запросам, которые соответствуют здесь/sms/loginPOST-запрос.
  • существует attemptAuthenticationметод, сначала изrequestПолучите номер мобильного телефона и код подтверждения, введенные в форму, чтобы создать неаутентифицированную информацию о токене.
  • Передайте информацию о токенеthis.getAuthenticationManager().authenticate(authRequest)метод.

пользовательский поставщик

Вот основная логика для завершения аутентификации, код здесь имеет только самую базовую логику проверки, и не написано более строгой проверки, такой как проверка того, отключен ли пользователь и т. д., потому что эта часть громоздка, но проста.

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    public static final String SESSION_MOBILE_KEY = "mobile";
    public static final String SESSION_SMS_CODE_KEY = "smsCode";
    public static final String FORM_MOBILE_KEY = "mobile";
    public static final String FORM_SMS_CODE_KEY = "smsCode";

    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        authenticationChecks(authentication);
        String mobile = authentication.getName();
        UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
        SmsCodeAuthenticationToken authResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
        return authResult;
    }

    /**
     * 认证信息校验
     * @param authentication
     */
    private void authenticationChecks(Authentication authentication) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // 表单提交的手机号和验证码
        String formMobile = request.getParameter(FORM_MOBILE_KEY);
        String formSmsCode = request.getParameter(FORM_SMS_CODE_KEY);
        // 会话中保存的手机号和验证码
        String sessionMobile = (String) request.getSession().getAttribute(SESSION_MOBILE_KEY);
        String sessionSmsCode = (String) request.getSession().getAttribute(SESSION_SMS_CODE_KEY);

        if (StringUtils.isEmpty(sessionMobile) || StringUtils.isEmpty(sessionSmsCode)) {
            throw new BadCredentialsException("为发送手机验证码");
        }

        if (!formMobile.equals(sessionMobile)) {
            throw new BadCredentialsException("手机号码不一致");
        }

        if (!formSmsCode.equals(sessionSmsCode)) {
            throw new BadCredentialsException("验证码不一致");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return (SmsCodeAuthenticationToken.class.isAssignableFrom(authentication));
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}

Основные положения этого кодекса следующие:

  • supportsМетод используется для определения поддерживаемого этим провайдером типа AuthenticationToken, который соответствует созданному нами ранееSmsCodeAuthenticationToken.
  • существует authenticateВ методе мы сравниваем номер мобильного телефона и код подтверждения в Токене с номером мобильного телефона и кодом подтверждения, сохраненным в сеансе. (Часть сохранения номера мобильного телефона и кода подтверждения в сеансе реализована ниже). После того, как сравнение будет правильным, получите соответствующего пользователя из службы UserDetailsService и соответствующим образом создайте аутентифицированный токен, верните его и, наконец, доберитесь до фильтра.

Обработчик после успеха/неудачной пользовательской аутентификации

Раньше мы знали, анализируя исходный код, что фильтр в FilterdoFilterметод, который фактически находится в его родительском классе

AbstractAuthenticationProcessingFilterсередина,attemptAuthenticationМетод также вызывается в doFilter.

Когда мы закончим предыдущую пользовательскую логику, независимо от того, прошла ли аутентификация успешно или нет,attemptAuthenticationМетод вернет успешный результат проверки подлинности или выдаст исключение ошибки проверки подлинности.doFilterМетод будет вызывать различную логику обработки в зависимости от результата аутентификации (успех/неудача) Мы также можем настроить эти две логики обработки.

Я вставляю код прямо ниже:

public class SmsCodeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("text/plain;charset=UTF-8");
        response.getWriter().write(authentication.getName());
    }
}
public class SmsCodeAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("text/plain;charset=UTF-8");
        response.getWriter().write("认证失败");
    }
}

Выше приведена логика обработки после успеха и неудачи.Вам необходимо реализовать соответствующие интерфейсы и реализовать методы соответственно. Обратите внимание, что это только для тестирования, а простейшая логика написана так, чтобы при тестировании можно было различить два случая. В реальном проекте соответствующая логика должна выполняться в соответствии с конкретным бизнесом, например, сохранять информацию о текущем вошедшем в систему пользователе.

Настроить пользовательскую логику аутентификации

Чтобы наша пользовательская аутентификация вступила в силу, нам нужно добавить Filter и Provider в конфигурацию Spring Security, Мы можем поместить эту часть конфигурации в класс конфигурации отдельно:

@Component
@RequiredArgsConstructor
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    private final UserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity http) {

        SmsCodeAuthenticationProcessingFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationProcessingFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(new SmsCodeAuthenticationSuccessHandler());
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(new SmsCodeAuthenticationFailureHandler());

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);

        http.authenticationProvider(smsCodeAuthenticationProvider)
                .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

Среди них следует отметить следующие моменты:

  • Обязательно предоставьте AuthenticationManager фильтру и просмотрите логику проверки подлинности, упомянутую ранее.Без этого шага после того, как информация проверки подлинности инкапсулирована в фильтре, невозможно найти соответствующего поставщика.
  • Предоставьте два класса логики обработки после успеха/неудачи для фильтра, иначе он не войдет в эти две логики, а войдет в логику обработки по умолчанию.
  • UserDetailsService используется в Provider, поэтому не забудьте указать его.
  • Наконец, добавьте оба в объект HttpSecurity.

Затем вам нужно добавить следующее в основную конфигурацию Spring Security.

  • Во-первых, впрыснутьSmsCodeAuthenticationSecurityConfigконфигурация.
  • Затем вconfigure(HttpSecurity http)метод, конфигурация импорта:http.apply`` ( ``smsCodeAuthenticationSecurityConfig`` ) ``;.
  • Наконец, поскольку код подтверждения необходимо запрашивать и проверять перед аутентификацией,/sms/**Путь освобождается.

контрольная работа

Готово, давайте проверим. Во-первых, нам нужно предоставить интерфейс для отправки кода подтверждения. Поскольку это тест, мы напрямую возвращаем код подтверждения. Код интерфейса выглядит следующим образом:

@GetMapping("/getCode")
public String getCode(@RequestParam("mobile") String mobile,
                      HttpSession session) {
    String code = "123456";
    session.setAttribute("mobile", mobile);
    session.setAttribute("smsCode", code);
    return code;
}

Чтобы получить соответствующих пользователей, если вы не внедрили собственный UserDetailsService, сначала напишите простую логику и выполните тест.loadUserByUsernameМетод заключается в следующем:

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // TODO: 临时逻辑,之后对接用户管理相关的服务
    return new User(username, "123456",
            AuthorityUtils.createAuthorityList("admin"));
}

Хорошо, вот результаты теста: