Java JWT令牌过滤器:令牌已认证但未达到控制器

时间:2020-03-23 16:54:52

标签: java spring jwt stateless

我正在使用Spring开发Java API,对此我有两种验证方式。

  1. 第一个允许一个通过称为AdminAuthenticationProvider的表单登录和身份验证管理器bean使用API​​的后台办公室部分。 后台和配置(首先)由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内容:

<?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()调用中一切正常,或者至少每行都没有问题。

问题是,如果我对/ p / logout路由执行POST(例如),并在标头中包含预期的令牌,则不会运行控制器方法PController.logout()。

如果我更新applicationContext.xml,并设置它:

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

然后调用controller方法。

我在无状态身份验证中错过了什么,因此在需要时请调用控制器?


编辑 我尝试了另一种方法来管理来自前台站点的无状态请求:

  1. 我从web.xml中删除了AuthenticationFilter过滤器
  2. 我在applicationContext-security.xlm中设置了几个http元素,以为预期的路由(也就是我从web.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>
  1. 我重构了一些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。

我非常确定问题在于过滤链中的身份验证理解(过滤链无法理解请求已被授权,因此未调用控制器)或过滤器给予的授权太晚了(之后,系统决定不调用控制器,原因是该请求未被授权。

任何人都可以帮忙吗?

0 个答案:

没有答案