Java Фильтр токена JWT: токен авторизован, но контроллер не достигнут - PullRequest
0 голосов
/ 23 марта 2020

Я работаю над Java API, используя Spring, для которого у меня есть два способа аутентификации.

  1. Первый позволяет использовать бэк-офисную часть API через бин формы входа в систему и менеджера аутентификации, называемый AdminAuthenticationProvider. Backoffice и конфигурация были (сначала) сгенерированы SpringRoo (а затем обновлены вручную). Это хорошо работает.
  2. Второй способ аутентификации - это процесс без сохранения состояния, использующий токен JWT и используемый внешним проектом, который использует вызовы REST. Некоторые маршруты бесплатны для всех (например, логин), другие требуют аутентификации с помощью токена в заголовке запроса. Свободные маршруты тоже работают хорошо.

Вот как это определяется в src / main / resources / META-INF / spring / applicationContex.security. xml:

    <?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security" 
    xmlns:beans="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
        http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd">
    <!-- HTTP security configurations -->
    <http auto-config="true" use-expressions="true">
        <form-login login-processing-url="/resources/j_spring_security_check" login-page="/login" authentication-failure-url="/login?login_error=t"
        />
        <logout logout-url="/resources/j_spring_security_logout" />

        <!-- Back office routes -->
        <intercept-url pattern="/users/**" access="hasRole('ADM')" />
        <intercept-url pattern="/login/**" access="permitAll" />
        <!-- ... -->

        <!-- front office routes -->
        <intercept-url pattern="/resources/**" access="permitAll" />
        <intercept-url pattern="/static/**" access="permitAll" />
        <intercept-url pattern="/p/login" access="permitAll" />
        <intercept-url pattern="/p/new" access="permitAll" />
        <!-- ... -->
        <!-- set the expected method to prevent from OPTIONS queries to be rejected because of no Authentication header -->
        <intercept-url pattern="/p/logout" access="isAuthenticated()" method="POST"/>        
        <intercept-url pattern="/**" access="permitAll" method="OPTIONS"/>
        <intercept-url pattern="/**" access="isAuthenticated()" />

        <!-- Concurrent Session Control -->
        <session-management session-authentication-error-url="/sessionExpired" >
            <concurrency-control max-sessions="1"/>
        </session-management>
    </http>

    <!-- Configure bakc office Authentication mechanism -->
    <beans:bean name="adminAuthenticationProvider" class="com.xxx.security.AdminAuthenticationProvider">
    </beans:bean>

    <authentication-manager alias="authenticationManager">
        <authentication-provider ref="adminAuthenticationProvider" />
    </authentication-manager>
</beans:beans>

а вот src / main / webapp / WEB-INF / web. xml content:

<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.5" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

    <display-name>myAPI</display-name>

    <description>my description</description>

    <!-- Enable escaping of form submission contents -->
    <context-param>
        <param-name>defaultHtmlEscape</param-name>
        <param-value>true</param-value>
    </context-param>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:META-INF/spring/applicationContext*.xml</param-value>
    </context-param>

    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>Spring OpenEntityManagerInViewFilter</filter-name>
        <filter-class>org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>Spring OpenEntityManagerInViewFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping> 

    <filter>
        <filter-name>AuthenticationFilter</filter-name>
        <filter-class>com.xxx.security.JwtTokenFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>AuthenticationFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- Concurrent Session Control -->
    <listener>
        <listener-class>
            org.springframework.security.web.session.HttpSessionEventPublisher
        </listener-class>
    </listener>     

    <!-- Creates the Spring Container shared by all Servlets and Filters -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!-- Handles Spring requests -->
    <servlet>
        <servlet-name>myAPI</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>WEB-INF/spring/webmvc-config.xml</param-value>
        </init-param>
        <init-param>
            <param-name>readonly</param-name>
            <param-value>false</param-value>
        </init-param>        
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>myAPI</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <session-config>
        <session-timeout>60</session-timeout>
    </session-config>

    <error-page>
        <exception-type>java.lang.Exception</exception-type>
        <location>/uncaughtException</location>
    </error-page>

    <error-page>
        <error-code>404</error-code>
        <location>/resourceNotFound</location>
    </error-page>
</web-app>

Проблема в маршрутах, для которых в заголовке требуется токен. Я создал фильтр для управления всеми вызовами API:

  • , если маршрут свободен, тогда API может управлять им, независимо от того, есть токен в заголовке или нет
  • если маршрут защищен, то он должен проверить, есть ли токен в заголовке, и является ли он все еще действительным

Вот фильтр:

 public class JwtTokenFilter extends GenericFilterBean {

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String authKey = null;
        List<GrantedAuthority> authList = null;
        User principal = null;
        Authentication auth = null;

        /* OPTIONS request are not authenticated, so do not manage them */
        if(!HttpMethod.OPTIONS.matches(httpServletRequest.getMethod())) {
            authKey = httpServletRequest.getHeader("authorization");
            if(null != authKey) {
                if(authKey.toLowerCase().startsWith("bearer ")) {
                    authKey = authKey.substring("bearer ".length());
                    request.setAttribute("authorization", authKey);
                }
                if(WebServiceAnswer.STATUS_OK == TokenManager.checkToken(authKey).getStatus()) {
                    authList = new ArrayList<GrantedAuthority>();
                    authList.add(new SimpleGrantedAuthority(Roles.getRoleUser()));
                    principal = new User(authKey, "", authList);
                    auth = new UsernamePasswordAuthenticationToken(principal, "", authList);
                    SecurityContextHolder.getContext().setAuthentication(auth);
                }
            }
        }
        // continue to process default behavior
        chain.doFilter(request, response);
        SecurityContextHolder.getContext().setAuthentication(null);
    }

}

После Фильтр выполняет свою работу, он должен передать руку контроллеру:

@RequestMapping("/p")
@Controller
@RooWebScaffold(path = "p", formBackingObject = P.class)
public class PController {
    ...

    @CrossOrigin(origins = { "http://localhost:4200", "http://www.xxx.xx" })
    @RequestMapping(value = "/logout", method = RequestMethod.POST)
    public ResponseEntity<String> logout(@RequestHeader("Authorization") String authKey) {
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Type", "application/json; charset=utf-8");
        P p = null;
        WebServiceAnswer answer = new WebServiceAnswer();
        JSONObject answerData;
        String token = null;

        // get expected p
        p = P.findByToken(authKey);
        // activation key not found
        if (null == p) {
            ...
            return new ResponseEntity<String>(answer.toJsonString(), headers, HttpStatus.BAD_REQUEST);
        }

        // authentication key found 
        ...
        return new ResponseEntity<String>(answer.toJsonString(), headers, HttpStatus.ACCEPTED);
    }    
}

Я отладил фильтр, и все нормально работает в вызове doFitler (), или, по крайней мере, каждая строка запускается без проблем.

Проблема в том, что если я выполняю маршрут POST to / p / logout (например) с ожидаемым токеном в заголовках, то метод контроллера PController.logout () не запускается.

Если я обновлю applicationContext. xml и установлю это:

<intercept-url pattern="/p/logout" access="permitAll" method="POST"/>

Затем вызывается метод контроллера.

Что я пропустил в проверке подлинности без сохранения состояния, так что контроллер вызывается, когда ожидается, пожалуйста?


РЕДАКТИРОВАТЬ Я попробовал другой способ управления своими запросами без сохранения состояния с внешнего сайта:

  1. Я удалил фильтр AuthenticationFilter из Интернета. xml
  2. Я установил несколько http-элементов в applicationContext-security.xlm, чтобы настроить выделенный диспетчер принятия решений о доступе к ожидаемым маршрутам (он же фильтр, который я удалил из Интернета. xml):
    <?xml version="1.0" encoding="UTF-8"?>
    <beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd">

        <!-- Configure front office authorization mechanism -->
        <beans:bean name="jwtTokenFilter" class="com.xxx.security.JwtTokenFilter"></beans:bean>
        <http security="none" pattern="/p/login"></http>
        <http security="none" pattern="/p/new"></http>
        <http security="none" pattern="/p/emailConfirm"></http>
        <http security="none" pattern="/p/sendNewActivationKey"></http>
        <http security="none" pattern="/p/sendResetPasswordEmail"></http>
        <http security="none" access-decision-manager-ref="jwtTokenFilter" pattern="/p/resetPassword"></http>
        <http auto-config="true" use-expressions="true" pattern="/p/logout" >
            <custom-filter ref="jwtTokenFilter" position="FIRST"/>
            <intercept-url pattern="/p/logout" access="permitAll" method="OPTIONS"/>
            <intercept-url pattern="/p/logout" access="isAuthenticated()" method="POST"/>
        </http>

        <!-- Configure back office authentication mechanism -->
        <http auto-config="true" use-expressions="true">
            <form-login 
                login-processing-url="/resources/j_spring_security_check" 
                login-page="/login" 
                authentication-failure-url="/login?login_error=t"
            />
            <logout logout-url="/resources/j_spring_security_logout" />

            <!-- Configure these elements to secure application URIs -->
            <intercept-url pattern="/resources/**" access="permitAll" />
            <intercept-url pattern="/static/**" access="permitAll" />

            <intercept-url pattern="/login/**" access="permitAll" />

            <intercept-url pattern="/users/**" access="hasRole('ROLE_ADMIN')" />
...    

            <!-- Concurrent Session Control -->
            <session-management session-authentication-error-url="/sessionExpired" >
                <concurrency-control max-sessions="1"/>
            </session-management>

        </http>

        <beans:bean name="adminAuthenticationProvider" class="com.xxx.security.AdminAuthenticationProvider"></beans:bean>
        <authentication-manager alias="authenticationManager">
            <authentication-provider ref="adminAuthenticationProvider" />
        </authentication-manager>

    </beans:beans>
Я немного изменил рефакторинг JwtTokenFilter, чтобы убедиться, что я установил статус ответа и его содержание:
public class JwtTokenFilter extends GenericFilterBean {

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        if(this.isRequestAuthorized(request, response, chain)) {
            // continue to process default behavior
            chain.doFilter(request, response);
            SecurityContextHolder.getContext().setAuthentication(null);
        }
    }

    private boolean isRequestAuthorized(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String verb = httpServletRequest.getMethod();
        String route = httpServletRequest.getRequestURI();
        String authKey = null;
        WebServiceAnswer answer = null;
        String message = null;
        List<GrantedAuthority> authList = null;
        User principal = null;
        Authentication auth = null;

        // OPTIONS request are not authenticated, so do not manage them
        if(HttpMethod.OPTIONS.matches(verb)) {
            return true;
        }

        // if we are here, we should have an authorization token
        authKey = httpServletRequest.getHeader("authorization");
        if(null == authKey) {
            message = String.format("%s requests to route %s require authorization ; Not token found in headers", verb, route);
            ((HttpServletResponse)response).setStatus(HttpStatus.UNAUTHORIZED.value());
            answer = new WebServiceAnswer();
            answer.setStatus(WebServiceAnswer.STATUS_KO);
            answer.setHttpStatus(HttpStatus.UNAUTHORIZED);
            answer.setCode("AUTHORIZATION_ERROR-NO_AUTH_KEY");
            answer.setHint(message);
            ((HttpServletResponse)response).getWriter().write(new ObjectMapper().writeValueAsString((answer)));
            return false;
        }

        // check authKey 
        if(authKey.toLowerCase().startsWith("bearer ")) {
            authKey = authKey.substring("bearer ".length());
            request.setAttribute("authorization", authKey);
        }
        answer = TokenManager.checkToken(authKey);
        if(WebServiceAnswer.STATUS_OK == answer.getStatus()) {
            // authKey is valid: authorize the request
            authList = new ArrayList<GrantedAuthority>();
            authList.add(new SimpleGrantedAuthority(Roles.getRoleUser()));
            principal = new User(authKey, "", authList);
            auth = new UsernamePasswordAuthenticationToken(principal, "", authList);
            SecurityContextHolder.getContext().setAuthentication(auth);
            ((HttpServletResponse)response).setStatus(HttpStatus.OK.value());
            ((HttpServletResponse)response).getWriter().write(new ObjectMapper().writeValueAsString((answer)));
            return true;
        }

        // authKey is not valid: reject the request
        ((HttpServletResponse)response).setStatus(HttpStatus.UNAUTHORIZED.value());
        ((HttpServletResponse)response).getWriter().write(new ObjectMapper().writeValueAsString((answer)));
        return false;
    }

}

Здесь у меня такое же поведение, как и раньше :( Если токен не задан или недействителен, тогда у меня есть ожидаемый http-код состояния для ответа, НО у меня нет содержимого ответа: (

И если токен задан и активен, то контроллер не вызывается .. .

Примечание: в applicationContext-security, если я устанавливаю маршрут выхода из системы следующим образом:

    <http security="none" access-decision-manager-ref="jwtTokenFilter" pattern="/p/logout"></http>

, тогда фильтр не вызывается, но Метод контроллера IS.

Я совершенно уверен, что проблема заключается либо в понимании аутентификации в цепочке фильтров (цепочка фильтров не может понять, что запрос авторизован, поэтому контроллер не вызывается), либо фильтр дает слишком поздно авторизацию (после этого система решает НЕ вызывать контроллер, потому что запрос не авторизован).

Любой может ЛП пожалуйста?

...