** 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');
}
```