qq_27430221 2023-04-19 19:51 采纳率: 0%
浏览 88
已结题

reuseRefreshTokens(false)生效了时间到了仍然强制log out

** Background:**
Client reported a issue which the user always logout by the system during working. Checked the system logic, we believe it is because the access token only valid for 3 hours. That's why the user always logout at around 12pm local time.

Current Access Token Logic:
The token involve 2 part:
Access token
Default valid time = 30mins
When access token expire, system will use refresh token to get a new access token with same valid time.
Refresh token
Default valid time = 6 hours (Wunsche's setting = 3 hours)
When refresh token expire, system will logout the user.
Expectation:

When user is actively using the system, we should extend/ refresh the Refresh Token.
Generate a new refresh token with new valid time by every api request if the original refresh token haven't expire.

项目框架:angular + springboot
当前希望解决:只要ui上有操作,refresh token 永不过期,尝试使用 api: reuseRefreshTokens(false),

    @Override
    public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
            .authenticationManager(authenticationManager)
            .tokenStore(tokenStore())
            .tokenEnhancer(tokenEnhancerChain())    
            .reuseRefreshTokens(false)
            .tokenGranter(appendTokenGranter(endpoints))
            .userDetailsService(userDetailsService)
            .exceptionTranslator(new WebResponseExceptionTranslatorImpl());

    }

debug 前端可以看见access token和refresh token 都刷新,但是一旦超过初始设定的refresh token过期时间,仍然会强制log out,log报错:

2023-04-19 19:40:46,661 [4fca686a-a242-4ade-bc4d-4ba1d93fd6c3] INFO  o.s.s.o.p.endpoint.TokenEndpoint - Handling error: InvalidTokenException, Invalid refresh token (expired): eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRlVGltZUZvcm1hdENvZGUiOiJkZC9NTU0veXl5eSBISDptbTpzcyIsImlzVGJ5TWlncmF0aW9uIjpmYWxzZSwidXNlcl9uYW1lIjoiZ2F2aW5Ad3VuIiwiZGF0ZUZvcm1hdENvZGUiOiJkZC9NTU0veXl5eSIsImxvY2FsZSI6ImVuX1VTIiwid29ya2luZ19kb21haW4iOiJXVU5TQ0hFIiwiZXhwaXJlZE9uIjoxNjgxOTA0NDE1ODAyLCJjbGllbnRfaWQiOiJjYngiLCJkb21haW5faWQiOiJXVU5TQ0hFIiwiY3VycmVudFVzZXJDb21iaW5lZElkIjoiZ2F2aW5Ad3VuQFdVTlNDSEUiLCJvd25lckRvbWFpbklkIjoiV1VOU0NIRSIsInJlZnJlc2hUb2tlbklkIjoiNTgwYjAyMTktYjI1My00YWQzLWJlZTMtNzVhMDJmMzM2YWZmIiwic2NvcGUiOlsicmVhZCIsIndyaXRlIiwidHJ1c3QiXSwiYXRpIjoiOTM0ZWU5ZWItODlmYy00OTgzLWJlNDQtOTg1NjM0NGJhMTk1IiwibmV3dWlUaW1lWm9uZUNvZGUiOiJBZnJpY2EvQWxnaWVycyIsImV4cCI6MTY4MTkwNDQyNCwianRpIjoiNTgwYjAyMTktYjI1My00YWQzLWJlZTMtNzVhMDJmMzM2YWZmIiwiZW1haWwiOiJnYXZpbi50ZXN0QGNieHNvZnR3YXJlLmNvbSIsInJlcXVlc3RDbGllbnQiOm51bGwsImN1cnJlbnRVc2VyTmFtZSI6ImdhdmluIHd1biIsInRva2VuSWQiOiI5MzRlZTllYi04OWZjLTQ5ODMtYmU0NC05ODU2MzQ0YmExOTUiLCJ3b3JraW5nRm9yVXNlcklkIjpudWxsLCJ1c2VySWQiOiI5ZmE5ZTFhNTMxYTI0MDQwOWE1NTRiMGFiOWRlNjNkNCIsImF1dGhvcml0aWVzIjpbInVzZXIiXSwiaXNTaW5nbGVTb3VyY2luZ0RvbWFpbiI6dHJ1ZSwidXNlckxvZ2luSWQiOiJnYXZpbkB3dW4ifQ.6rMSFCuDLMkiUqx9MbgLtzuHVWnw_H6oh1wv80dYJ60
2023-04-19 19:40:46,662 [] INFO  c.c.c.c.CustomizeEmbeddedTomcatContainer - POST /oauth/token status=401, time_taken=27, bytes=1268, query_string=, request_uuid=4fca686a-a242-4ade-bc4d-4ba1d93fd6c3, remote_ip_address=127.0.0.1

以下是具体代码:

package com.cbxsoftware.rest.configuration;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

import com.cbxsoftware.rest.security.CbxAccessTokenConverter;
import com.cbxsoftware.rest.security.CbxTokenEnhancer;
import com.cbxsoftware.rest.security.WebResponseExceptionTranslatorImpl;
import com.cbxsoftware.rest.security.checker.AuthenticationChecker;
import com.cbxsoftware.rest.security.grant.CasTicketGranter;
import com.cbxsoftware.rest.service.DomainAttributeService;
import com.cbxsoftware.rest.service.SsoPgtService;

import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    private static final String GRANT_TYPE = "password";
    private static final String AUTHORIZATION_CODE = "authorization_code";
    private static final String REFRESH_TOKEN = "refresh_token";
    private static final String IMPLICIT = "implicit";
    private static final String SCOPE_READ = "read";
    private static final String SCOPE_WRITE = "write";
    private static final String TRUST = "trust";
    private static final String CAS = "cas";

    @Value("${cbx.client.id:cbx}")
    private String clientId;

    @Value("${cbx.client.secret:cbx@123}")
    private String clientSecret;

    @Value("${cbx.access_token.validitySeconds:300}")
    private Integer accessTokenValiditySeconds;

    @Value("${cbx.refresh_token.validitySeconds:21600}")
    private Integer refreshTokenValiditySeconds;

    @Value("${cbx.jwt.key:cbxRest@123}")
    private String jwtKey;

    private final AuthenticationManager authenticationManager;
    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;
    private final SsoPgtService ssoPgtService;
    private final DomainAttributeService domainAttributeService;
    private final List<AuthenticationChecker> authenticationCheckers;

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setAccessTokenConverter(new CbxAccessTokenConverter());
        converter.setSigningKey(jwtKey);
        return converter;
    }

    @Bean
    public TokenEnhancerChain tokenEnhancerChain() {
        //add additional information to JWT token
        final CbxTokenEnhancer cbxTokenEnhancer = new CbxTokenEnhancer(domainAttributeService);
        final List<TokenEnhancer> tokenEnhancers = Arrays.asList(cbxTokenEnhancer, accessTokenConverter());

        final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
        return tokenEnhancerChain;
    }

    @Override
    public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
            .withClient(clientId)
            .secret(passwordEncoder.encode(clientSecret))
            .authorizedGrantTypes(GRANT_TYPE, AUTHORIZATION_CODE, REFRESH_TOKEN, IMPLICIT, CAS)
            .scopes(SCOPE_READ, SCOPE_WRITE, TRUST)
            .accessTokenValiditySeconds(accessTokenValiditySeconds)
            .refreshTokenValiditySeconds(refreshTokenValiditySeconds);
    }

    @Override
    public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
            .authenticationManager(authenticationManager)
            .tokenStore(tokenStore())
            .tokenEnhancer(tokenEnhancerChain())    
            .reuseRefreshTokens(false)
            .tokenGranter(appendTokenGranter(endpoints))
            .userDetailsService(userDetailsService)
            .exceptionTranslator(new WebResponseExceptionTranslatorImpl());

    }

    private TokenGranter appendTokenGranter(final AuthorizationServerEndpointsConfigurer endpoints) {
        final List<TokenGranter> granters = Collections.singletonList(endpoints.getTokenGranter());
        final CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(granters);
        compositeTokenGranter.addTokenGranter(retrieveCasTicketGranter(endpoints));
        return compositeTokenGranter;
    }

    private TokenGranter retrieveCasTicketGranter(final AuthorizationServerEndpointsConfigurer endpoints) {
        return new CasTicketGranter(
            endpoints.getTokenServices(),
            endpoints.getClientDetailsService(),
            endpoints.getOAuth2RequestFactory(),
            userDetailsService,
            ssoPgtService,
            authenticationCheckers
        );
    }
}


angular 前端拦截器代码:


```bash
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';

import { Observable } from 'rxjs';
import { first, switchMap } from 'rxjs/operators';

import { AuthService } from '../auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private readonly authService: AuthService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (isRefreshTokenRequest(req)) {
      return next.handle(req);
    }

    return this.authService.userInfo$.pipe(
      first(),
      switchMap((userInfo) => {
        if (userInfo.access_token) {
          // Clone the request and set the new header in one step.
          const authReq = req.clone({
            setHeaders: {
              Authorization: `${userInfo.token_type} ${userInfo.access_token}`,
            },
          });

          // send cloned request with header to the next handler.
          return next.handle(authReq);
        } else {
          return next.handle(req);
        }
      }),
    );
  }
}

function isRefreshTokenRequest(req: HttpRequest<any>) {
  return typeof req.body === 'string' && req.body.includes('grant_type=refresh_token');
}



```

  • 写回答

4条回答 默认 最新

  • 极客智能体-在线 2023-04-19 23:31
    关注

    以下答案由GPT-3.5大模型与博主波罗歌共同编写:
    从代码中可以看到,在AuthorizationServerEndpointsConfigurer中的configure方法中,我们看到了这个属性reuseRefreshTokens(false)的设置。这个属性决定了当我们使用refresh_token获取新的access_token时,会不会刷新refresh_token的有效期。

    根据OAuth2的规范,refresh_token是有过期时间的,这个时间是可以配置的。如果refresh_token过期了,那么就需要用户重新登录获取新的access_token和refresh_token了,这也是为什么你的用户总是在工作时被系统登出的原因。

    但现在你希望用户重新登录的需求没有了,因为只要用户在系统中进行操作,就应该刷新refresh_token的有效期。如果非常长时间没有用户操作,可以理解为用户已经离开了系统,这时可以让refresh_token过期。

    默认情况下,reuseRefreshTokens属性是true,也就是说当refresh_token刷新access_token时,同时也会刷新refresh_token的有效期,这个也是OAuth2规范中的默认行为。如果设置为false,那么refresh_token的有效期只会延长一段时间,而不是无限期延长。

    在你的代码中,你已经将reuseRefreshTokens属性设置为false了,因此你期望的行为是每次用户操作时刷新refresh_token的有效期。但是根据你的描述,它并没有生效。

    那么问题出在哪里呢?我们暂且不管前端的代码,它应该没有问题。问题出在后端Spring Security Oauth2框架的配置中。

    根据你提供的代码,我们看到你只对access token进行了增强,也就是重写了类CbxAccessTokenConverter和类CbxTokenEnhancer。但是它们只影响了access token,对refresh token并没有产生影响,也就是说情况还和原来一样。

    所以你需要在类OAuth2AuthorizationServerConfig的configure方法中再进一步增强token。正确的代码如下:

        @Override
        public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
            endpoints
                .authenticationManager(authenticationManager)
                .tokenStore(tokenStore())
                .tokenEnhancer(tokenEnhancerChain())
                .accessTokenConverter(accessTokenConverter())
                .reuseRefreshTokens(false)
                .tokenGranter(appendTokenGranter(endpoints))
                .userDetailsService(userDetailsService)
                .exceptionTranslator(new WebResponseExceptionTranslatorImpl())
                .addInterceptor(new TokenStoreInterceptor(tokenStore()));
        }
    
        private static class TokenStoreInterceptor implements HandlerInterceptor {
            private final TokenStore tokenStore;
    
            TokenStoreInterceptor(TokenStore tokenStore) {
                this.tokenStore = tokenStore;
            }
    
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                String refreshTokenValue = request.getParameter("refresh_token");
                if (refreshTokenValue != null) {
                    OAuth2AccessToken refreshToken = tokenStore.readRefreshToken(refreshTokenValue);
                    if (refreshToken != null) {
                        tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);
                        tokenStore.removeRefreshToken(refreshToken);
                    }
                }
    
                return true;
            }
        }
    

    可以注意到,我们添加了一个拦截器TokenStoreInterceptor,在每次请求时检查是否带有refresh_token,如果这时候refresh_token的有效期已经过了,那么就将之前已经使用过的access_token和refresh_token删除。

    这样就可以实现你期望的功能了。
    如果我的回答解决了您的问题,请采纳!

    评论

报告相同问题?

问题事件

  • 系统已结题 4月27日
  • 修改了问题 4月19日
  • 修改了问题 4月19日
  • 创建了问题 4月19日

悬赏问题

  • ¥15 35114 SVAC视频验签的问题
  • ¥15 impedancepy
  • ¥15 在虚拟机环境下完成以下,要求截图!
  • ¥15 求往届大挑得奖作品(ppt…)
  • ¥15 如何在vue.config.js中读取到public文件夹下window.APP_CONFIG.API_BASE_URL的值
  • ¥50 浦育平台scratch图形化编程
  • ¥20 求这个的原理图 只要原理图
  • ¥15 vue2项目中,如何配置环境,可以在打完包之后修改请求的服务器地址
  • ¥20 微信的店铺小程序如何修改背景图
  • ¥15 UE5.1局部变量对蓝图不可见