java Spring Security Cookie + JWT 认证

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/38341114/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-03 03:19:45  来源:igfitidea点击:

Spring Security Cookie + JWT authentication

javaspringauthenticationspring-security

提问by Assaf Moldavsky

I must say I am very confused about the entire model and I need help gluing all the floating pieces together.

我必须说我对整个模型非常困惑,我需要帮助将所有浮动部件粘合在一起。

I am not doing Spring REST, just plain WebMVC controllers.

我不是在做 Spring REST,只是在做普通的 WebMVC 控制器。

My mission: I want a form login with a username + pass authentication. I want to authenticate against a 3rd party service. Upon success I want to return a cookie but NOT use the default cookie token mechanism. I want the cookie to have a JWT token instead. By leveraging the cookie mechanism every request will be sent with the JWT.

我的任务:我想要一个带有用户名 + 通行证的表单登录。我想针对第 3 方服务进行身份验证。成功后,我想返回一个 cookie,但不使用默认的 cookie 令牌机制。我希望 cookie 有一个 JWT 令牌。通过利用 cookie 机制,每个请求都将与 JWT 一起发送。

So to break it down I have the following modules to take care of:

所以为了分解它,我有以下模块需要处理:

  1. do authentication against a 3rd party service when doing a user + pas logi n
  2. replace cookie session token with my custom implementation upon successful auth

  3. upon every request parse the JWT from the cookie ( using a filter )

  4. extract user details / data from the JWT to be accessible to the controllers

  1. 在进行用户 + 密码登录时对 3rd 方服务进行身份验证
  2. 成功验证后用我的自定义实现替换 cookie 会话令牌

  3. 根据每个请求从 cookie 解析 JWT(使用过滤器)

  4. 从 JWT 中提取用户详细信息/数据以供控制器访问

What's confusing? ( please correct me where I am wrong )

有什么困惑?(请指正我错的地方)

3rd party authentication

第三方认证

to authenticate against a 3rd party I will need to have a custom provider by extending AuthenticationProvider

要针对第 3 方进行身份验证,我需要通过扩展 AuthenticationProvider 来拥有一个自定义提供程序

public class JWTTokenAuthenticationProvider implements AuthenticationProvider { 

      @Override
      public Authentication authenticate( Authentication authentication ) throws AuthenticationException {

          // auth against 3rd party

          // return Authentication
          return new UsernamePasswordAuthenticationToken( name, password, new ArrayList<>() );

      }

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

}

Questions:

问题:

  • is this provider executed upon successful authentication / login when user submits a form user + pass? if so that how is that related to AbstractAuthenticationProcessingFilter#successfulAuthentication?
  • do I have to return an instance of UsernamePasswordAuthenticationToken?
  • do I have to support UsernamePasswordAuthenticationToken to get user + pass here?
  • 当用户提交表单 user + pass 时,是否在成功验证/登录后执行此提供程序?如果是这样,它与 AbstractAuthenticationProcessingFilter#successfulAuthentication 有什么关系?
  • 我必须返回 UsernamePasswordAuthenticationToken 的实例吗?
  • 我是否必须支持 UsernamePasswordAuthenticationToken 才能在此处获取用户 + 通行证?

replace cookie token with a JWT

用 JWT 替换 cookie 令牌

No idea how to do this gracefully, I can think of a number of ways but they not Spring Security ways and I don't want to break out of the flow. Would be thankful for any suggestions here!

不知道如何优雅地做到这一点,我可以想到很多方法,但它们不是 Spring Security 的方法,我不想打破流程。非常感谢这里的任何建议!

parse the JWT with every request from a cookie

使用来自 cookie 的每个请求解析 JWT

From what I understand I need to extend AbstractAuthenticationProcessingFilter like so

据我了解,我需要像这样扩展 AbstractAuthenticationProcessingFilter

public class CookieAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    @Override
    public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response )
            throws AuthenticationException, IOException, ServletException {

        String token = "";

        // get token from a Cookie

        // create an instance to Authentication
        TokenAuthentication authentication = new TokenAuthentication(null, null);

        return getAuthenticationManager().authenticate(tokenAuthentication);

    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
                     FilterChain chain) throws IOException, ServletException {
        super.doFilter(req, res, chain);
    }

}

Questions:

问题:

  • when is AbstractAuthenticationProcessingFilter#successfulAuthentication called? is it called with the user logs in or when the JWT token been validated successfully?
  • is there any relation between this filter and the custom provider I posted previously? The manager will supposedly call the custom provider based on the token instance which is matched with what the provider supports via the support method?
  • AbstractAuthenticationProcessingFilter#successfulAuthentication 何时被调用?它是在用户登录时调用还是在成功验证 JWT 令牌时调用?
  • 此过滤器与我之前发布的自定义提供程序之间有任何关系吗?管理器应该会根据令牌实例调用自定义提供者,该实例与提供者通过 support 方法支持的内容相匹配?

It seems as if I have all the pieces I need, except the cookie session replacement, but I cannot put them into a single coherent model and I need from somebody who understand the mechanics well enough so I can glue all of this into a single module.

似乎我拥有我需要的所有部分,除了 cookie 会话替换,但我无法将它们放入一个连贯的模型中,我需要一个足够了解机制的人,以便我可以将所有这些粘合到一个模块中.

UPDATE 1

更新 1

OK, I think I am getting where this is starting... https://github.com/spring-projects/spring-security/blob/master/web/src/main/java/org/springframework/security/web/authentication/UsernamePasswordAuthenticationFilter.java

好的,我想我已经开始了...... https://github.com/spring-projects/spring-security/blob/master/web/src/main/java/org/springframework/security/web/身份验证/用户名密码AuthenticationFilter.java

This Filter registers itself to POST -> "/login" and than creates an instance of UsernamePasswordAuthenticationToken and passes the control to the next filter.

此过滤器将自身注册到 POST -> "/login",然后创建 UsernamePasswordAuthenticationToken 的实例并将控制权传递给下一个过滤器。

Question is where the cookie session is set....

问题是cookie会话在哪里设置....

UPDATE 2

更新 2

This section of the dos gives the top level flow that I was missing, for whoever is going through this take a look here... http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#tech-intro-authentication

dos 的这一部分给出了我缺少的顶级流程,对于正在经历这个的人,请看这里... http://docs.spring.io/spring-security/site/docs/current/reference/ htmlsingle/#tech-intro-authentication

This section regarding the AuthenticationProvider... http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#core-services-authentication-manager

本节关于 AuthenticationProvider... http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#core-services-authentication-manager

UPDATE 3 - working case, is this the best way??

更新 3 - 工作案例,这是最好的方法吗??

So after digging through the Spring Security docs and their sources I got the initial model to work. Now, doing this, I realized there is more than one way to do it. Any advice of why picking this way VS what Denys proposed below?

因此,在深入研究 Spring Security 文档及其来源后,我使初始模型可以工作。现在,这样做,我意识到有不止一种方法可以做到。关于为什么选择这种方式 VS Denys 在下面提出的建议有什么建议吗?

Working example below...

下面的工作示例...

回答by Assaf Moldavsky

To get this to work the way described in the original post, this is what needs to happen...

为了让它按照原帖中描述的方式工作,这是需要发生的......

Custom Filter

自定义过滤器

public class CookieAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

        public CookieAuthenticationFilter( RequestMatcher requestMatcher ) {

            super( requestMatcher );
            setAuthenticationManager( super.getAuthenticationManager() );

        }

        @Override
        public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response )
            throws AuthenticationException, IOException, ServletException {

            String token = "";

            // get token from a Cookie
            Cookie[] cookies = request.getCookies();

            if( cookies == null || cookies.length < 1 ) {
                throw new AuthenticationServiceException( "Invalid Token" );
            }

            Cookie sessionCookie = null;
            for( Cookie cookie : cookies ) {
                if( ( "someSessionId" ).equals( cookie.getName() ) ) {
                sessionCookie = cookie;
                break;
                }
            }

            // TODO: move the cookie validation into a private method
            if( sessionCookie == null || StringUtils.isEmpty( sessionCookie.getValue() ) ) {
                throw new AuthenticationServiceException( "Invalid Token" );
            }

            JWTAuthenticationToken jwtAuthentication = new JWTAuthenticationToken( sessionCookie.getValue(), null, null );

            return jwtAuthentication;

        }


        @Override
        public void doFilter(ServletRequest req, ServletResponse res,
                 FilterChain chain) throws IOException, ServletException {
            super.doFilter(req, res, chain);
        }

}

Authentication Provider

身份验证提供程序

attach the provider to the UsernamePasswordAuthenticationToken which is generated by UsernamePasswordAuthenticationFilter, which attaches itself to "/login" POST. For formlogin with POST to "/login" will generate UsernamePasswordAuthenticationToken and your provider will be triggered

将提供者附加到由 UsernamePasswordAuthenticationFilter 生成的 UsernamePasswordAuthenticationToken ,它附加到“/login”POST。对于带有 POST 到“/login”的 formlogin 将生成 UsernamePasswordAuthenticationToken 并且您的提供者将被触发

@Component
public class ApiAuthenticationProvider implements AuthenticationProvider {

        @Autowired
        TokenAuthenticationService tokenAuthService;

        @Override
        public Authentication authenticate( Authentication authentication ) throws AuthenticationException {

            String login = authentication.getName();
            String password = authentication.getCredentials().toString();

            // perform API call to auth against a 3rd party

            // get User data
            User user = new User();

            // create a JWT token
            String jwtToken = "some-token-123"

            return new JWTAuthenticationToken( jwtToken, user, new ArrayList<>() );

        }

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

Custom Authentication object

自定义身份验证对象

For the JWT we want to have our own Authentication token object to carry the data that we want along the stack.

对于 JWT,我们希望拥有自己的身份验证令牌对象,以沿堆栈携带我们想要的数据。

public class JWTAuthenticationToken extends AbstractAuthenticationToken {

        User principal;
        String token;

        public JWTAuthenticationToken( String token, User principal, Collection<? extends GrantedAuthority> authorities ) {
            super( authorities );
            this.token = token;
            this.principal = principal;
        }

        @Override
        public Object getCredentials() {
            return null;
        }

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

        public void setToken( String token ) {
            this.token = token;
        }

        public String getToken() {
            return token;
        }
}

Authentication Success Handler

身份验证成功处理程序

This is getting called when our custom provider did it's job by authenticating the user against a 3rd party and generating a JWT token, This is the place where the Cookie gets into the Response.

当我们的自定义提供程序通过针对第 3 方对用户进行身份验证并生成 JWT 令牌来完成它的工作时,它会被调用,这是 Cookie 进入响应的地方。

@Component
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(
                HttpServletRequest request, 
                HttpServletResponse response, 
                Authentication authentication) throws IOException, ServletException {

        if( !(authentication instanceof JWTAuthenticationToken) ) {
            return;
        }

        JWTAuthenticationToken jwtAuthenticaton =    (JWTAuthenticationToken) authentication;

        // Add a session cookie
        Cookie sessionCookie = new Cookie( "someSessionId", jwtAuthenticaton.getToken() );
        response.addCookie( sessionCookie );

        //clearAuthenticationAttributes(request);

        // call the original impl
        super.onAuthenticationSuccess( request, response, authentication );
}

}

}

Hooking This All Together

把这一切联系在一起

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired @Required
    ApiAuthenticationProvider apiAuthProvider;

    @Autowired @Required
    AuthenticationSuccessHandler authSuccessHandler;

    @Autowired @Required
    SimpleUrlAuthenticationFailureHandler authFailureHandler;

    @Override
    protected void configure( AuthenticationManagerBuilder auth ) throws Exception {
    auth.authenticationProvider( apiAuthProvider );
    }

    @Override
    protected void configure( HttpSecurity httpSecurity ) throws Exception {

            httpSecurity

            // don't create session
            .sessionManagement()
                .sessionCreationPolicy( SessionCreationPolicy.STATELESS )
                .and()

            .authorizeRequests()
                .antMatchers( "/", "/login", "/register" ).permitAll()
                .antMatchers( "/js/**", "/css/**", "/img/**" ).permitAll()
                .anyRequest().authenticated()
                .and()

            // login
            .formLogin()
                .failureHandler( authFailureHandler )
                //.failureUrl( "/login" )
                .loginPage("/login")
                .successHandler( authSuccessHandler )
                        .and()

            // JWT cookie filter
            .addFilterAfter( getCookieAuthenticationFilter(
                    new AndRequestMatcher( new AntPathRequestMatcher( "/account" ) )
            ) , UsernamePasswordAuthenticationFilter.class );
    }


    @Bean
    SimpleUrlAuthenticationFailureHandler getAuthFailureHandler() {

            SimpleUrlAuthenticationFailureHandler handler = new SimpleUrlAuthenticationFailureHandler( "/login" );
            handler.setDefaultFailureUrl( "/login" );
            //handler.setUseForward( true );

            return handler;

    }

    CookieAuthenticationFilter getCookieAuthenticationFilter( RequestMatcher requestMatcher ) {

            CookieAuthenticationFilter filter = new CookieAuthenticationFilter( requestMatcher );
            filter.setAuthenticationFailureHandler( authFailureHandler );
            return filter;
    }
}

回答by Assaf Moldavsky

The easiest way would be to add Spring Session into your project and extend HttpSessionStrategythat provides convenient hooks for session created/destroyed events and has method to extract session from HttpServletRequest.

最简单的方法是将 Spring Session 添加到您的项目中并扩展HttpSessionStrategy,它为会话创建/销毁事件提供方便的挂钩,并具有从 HttpServletRequest 中提取会话的方法。