Spring Boot + OAuth + JWT + MySQL 刷新令牌第二次不起作用
Posted
技术标签:
【中文标题】Spring Boot + OAuth + JWT + MySQL 刷新令牌第二次不起作用【英文标题】:Spring Boot + OAuth + JWT + MySQL refresh token not working for second time 【发布时间】:2017-02-08 18:51:33 【问题描述】:我正在使用 Spring boot、OAuth2、JWT 客户令牌和 mysql。
问题:我能够获取令牌并刷新令牌,使用刷新令牌我只能按时获取新令牌,如果我再次尝试使用新刷新令牌获取新令牌表示我收到以下错误。
错误信息
"error": "invalid_grant",
"error_description": "Invalid refresh token: eyJhbGciOiJSUzI1NiJ9.eyJsb2dpbklkIjoibmF2ZWVuIiwidXNlcl9uYW1lIjoibmF2ZWVuIiwic2NvcGUiOlsiY2l0eSIsInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiI4YmVlMzZhZi1lZWM1LTQzODItYjNkZi1jYTU3Mjc0NjQ5N2MiLCJleHAiOjE0NzUyMjc4NzUsImF1dGhvcml0aWVzIjpbIlRlc3RlciIsIlVzZXIiLCJBZG1pbiIsImNhbXBhaWduLWNhbmFsbCIsIm9yZy1jYW5hbGwiXSwianRpIjoiNjE1OWM4NTYtYTZmNi00Njg3LTg3OTMtMTA1NDdkODE4YmVhIiwiY2xpZW50X2lkIjoiY2l0eUNsaWVudElkIn0.FvA821Hv0ZzA6mdwNp-XlcHAy6tCncP8snkQDlmDWulFE-BIe-KxTT0ugjoK2l1ncAQugtyfXCnS_a0bgAPcu1HKmYgIvj4f3XBj1WLRagiDfJqjZAwZhDPvrwks7W1IsvWrzy5k-pmoO7373C5DU0jbFsanzkvMQ6LQAwb_bFfOB3GYH5BSIW4rcbe8AH1B3QKxn9J26Jj1yQWnkY8HnUqnxN5C-3jBwr8pvqPmX2AjOVeAnkoGfY6B3Dq1vz8EE17I8GG2uqGgUsaTiVqP3Lka__ue00MjajxcpVHeh7t1Qs0IbTa2oeuahAwcYOC_ik_Rplhn3w-LHpyhPBrTHA"
请找到身份验证服务器和资源服务器配置文件。 我无法找到我做错的地方。
授权服务器
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter
@Autowired
private DataSource dataSource;
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Autowired
@Qualifier("userDetailsService")
UserDetailsService userDetailsService;
@Override
public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
//.passwordEncoder(passwordEncoder());
@Bean
public PasswordEncoder passwordEncoder()
CustomPasswordEncoder encoder = new CustomPasswordEncoder();
return encoder;
@Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception
clients.jdbc(dataSource);
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception
final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
endpoints.tokenStore(tokenStore())
// .approvalStore(approvalStore())
.userDetailsService(userDetailsService).tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager);
@Bean
public JwtAccessTokenConverter accessTokenConverter()
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
final KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("mytest.jks"),
"mypass".toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mytest"));
return converter;
@Bean
public TokenEnhancer tokenEnhancer()
return new CustomTokenEnhancer();
// JDBC token store configuration
@Bean
public TokenStore tokenStore()
return new JdbcTokenStore(dataSource);
@Bean
@Primary
public ResourceServerTokenServices tokenServices()
final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setReuseRefreshToken(true);
return defaultTokenServices;
UserDetailsService
@Service("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService
@Autowired
private AppUserService appUserService;
@Autowired
private AppUserRepo appUserRepo;
@Transactional(readOnly = true)
@Override
public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException
// TODO: clean the server methods
final AppUser appUser = appUserRepo.findExceptDeletedByAppUserName(username);
final AppUserCreateDto appUserCreateDto = appUserService.getAppUserCreateByAppUserId(appUser.getAppUserId());
return buildUserForAuthentication(appUser, buildUserAuthority(appUserCreateDto));
// Converts com.mkyong.users.model.User user to
// org.springframework.security.core.userdetails.User
private User buildUserForAuthentication(AppUser appUser, List<GrantedAuthority> authorities)
return new User(appUser.getLoginId(), appUser.getPasswordHash(), appUser.isActive(), true, true, true,
authorities);
private List<GrantedAuthority> buildUserAuthority(AppUserCreateDto appUserCreateDto)
Set<GrantedAuthority> setAuths = new HashSet<GrantedAuthority>();
if (appUserCreateDto.isPrimary())
setAuths.add(new SimpleGrantedAuthority("SuperAdmin"));
else
for (AppUserRoleDto userRole : appUserCreateDto.getAppUserRoleDtos())
setAuths.add(new SimpleGrantedAuthority(userRole.getAppRoleDto().getName()));
for (AppUserClaimDto userClaim : appUserCreateDto.getAppUserClaimDtos())
setAuths.add(new SimpleGrantedAuthority(
userClaim.getAppClaimDto().getClaimType() + "-" + userClaim.getAppClaimDto().getClaimValue()));
List<GrantedAuthority> Result = new ArrayList<GrantedAuthority>(setAuths);
return Result;
自定义令牌
public class CustomTokenEnhancer implements TokenEnhancer
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication)
final Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put("loginId", authentication.getName());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
网络安全
@Configuration
@EnableWebSecurity
public class AuthorizationServerWebSecurityConfig extends WebSecurityConfigurerAdapter
@Autowired
@Qualifier("userDetailsService")
UserDetailsService userDetailsService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
@Bean
public PasswordEncoder passwordEncoder()
CustomPasswordEncoder encoder = new CustomPasswordEncoder();
return encoder;
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception
return super.authenticationManagerBean();
资源服务器配置
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter
@Override
public void configure(final HttpSecurity http) throws Exception
// @formatter:off
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED).and().anonymous().and()
.authorizeRequests().antMatchers("/api/p/**").permitAll().antMatchers("/api/ping").permitAll()
.antMatchers("/api/**").authenticated();
// @formatter:on
@Override
public void configure(final ResourceServerSecurityConfigurer config)
config.tokenServices(tokenServices());
@Bean
@Primary
public ResourceServerTokenServices tokenServices()
final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setReuseRefreshToken(true);
return defaultTokenServices;
@Bean
public TokenStore tokenStore()
return new JwtTokenStore(accessTokenConverter());
@Bean
public JwtAccessTokenConverter accessTokenConverter()
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
final Resource resource = new ClassPathResource("public.txt");
String publicKey = null;
try
publicKey = IOUtils.toString(resource.getInputStream());
catch (final IOException e)
throw new RuntimeException(e);
converter.setVerifierKey(publicKey);
return converter;
第一次获取令牌
"access_token":"eyJhbGciOiJSUzI1NiJ9.eyJsb2dpbklkIjoibmF2ZWVuIiwidXNlcl9uYW1lIjoibmF2ZWVuIiwic2NvcGUiOlsiY2l0eSIsInJlYWQiLCJ3cml0ZSJdLCJleHAiOjE0NzUyMjUyMzAsImF1dGhvcml0aWVzIjpbIlRlc3RlciIsIlVzZXIiLCJBZG1pbiIsImNhbXBhaWduLWNhbmFsbCIsIm9yZy1jYW5hbGwiXSwianRpIjoiYWNlMmQwYzgtZjRhOS00NzY5LThhN2EtNDk1ZjY3ZDFhMjk4IiwiY2xpZW50X2lkIjoiY2l0eUNsaWVudElkIn0.cKZgk39yQ_tl7NW4OhUmSnhPSgvWD8UPp6RRfpc0hsW28ICVjIaCURRaC-eEs9J_YuC2X7NDTFFy3KknFka7rDV2JMCFSILivW13EFT2i0TkHUVFCBWk4MMlEKOXQOUVPiMZ3t3zD6_Tkmo_NneNPjouRVyFjCZ4WGqWPEzGpofExZWlBzoV7bDuF28fTQKqxIQM-ubwx-hKY_btlaXQpyJCuKn4QoCwvp2Bh5zqSEIk2RiGh0nsUyi_MZl3TQX2kYDv-SwWOxf3K9bibY9xPhzgLVIER39Ipo7FrUE9KsYoEkXM1-CghbADjIXu03xd7GZ2530fs-MHhr24YzVY1Q",
"token_type": "bearer",
"refresh_token":"eyJhbGciOiJSUzI1NiJ9.eyJsb2dpbklkIjoibmF2ZWVuIiwidXNlcl9uYW1lIjoibmF2ZWVuIiwic2NvcGUiOlsiY2l0eSIsInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiJhY2UyZDBjOC1mNGE5LTQ3NjktOGE3YS00OTVmNjdkMWEyOTgiLCJleHAiOjE0NzUyMjc4NzUsImF1dGhvcml0aWVzIjpbIlRlc3RlciIsIlVzZXIiLCJBZG1pbiIsImNhbXBhaWduLWNhbmFsbCIsIm9yZy1jYW5hbGwiXSwianRpIjoiNjE1OWM4NTYtYTZmNi00Njg3LTg3OTMtMTA1NDdkODE4YmVhIiwiY2xpZW50X2lkIjoiY2l0eUNsaWVudElkIn0.S5pCyuaJVf6HnzLfct2HMQQdkcoZO0-FlgGIRJueAvMmVFRpiCqYCT7AniW8NUvltcMTkDXdZPJo21OmomdWUr3gO1BV3Ki9aJNuewXxsoymIy_L3xWbNb8k8hdrwhYZQufe1YnWLKgHpDUSc13cW-6SNQQwd9ugXkMIvp1qG7d9j6GCZrxOXj0HNLKR3CubfesweUb9GtG8D0XkEGc7O-xPSHZnJWX73sCT5Qi1fot1btTMoeCwp63r9Wa4TkESXrmXdSMzI0GUwc6x_7r3mv5gF34gzF8Z3fChSMglgxreRtF2PbTPGZKXQ3Dk-f0WcWOmbkpetg0n4Wo0dNujaw",
"expires_in": 359,
"scope": "city read write",
"loginId": "naveen",
"jti": "ace2d0c8-f4a9-4769-8a7a-495f67d1a298"
第一次获取刷新令牌
"access_token":"eyJhbGciOiJSUzI1NiJ9.eyJsb2dpbklkIjoibmF2ZWVuIiwidXNlcl9uYW1lIjoibmF2ZWVuIiwic2NvcGUiOlsiY2l0eSIsInJlYWQiLCJ3cml0ZSJdLCJleHAiOjE0NzUyMjUzNDcsImF1dGhvcml0aWVzIjpbIlRlc3RlciIsIlVzZXIiLCJBZG1pbiIsImNhbXBhaWduLWNhbmFsbCIsIm9yZy1jYW5hbGwiXSwianRpIjoiNzViYTA4OWYtZTczZC00MWIyLWEwM2ItMzBlZDRjNzIyMmM4IiwiY2xpZW50X2lkIjoiY2l0eUNsaWVudElkIn0.fhBqLGTyu4BaAv9zS9gGAgZYymhbBxgnIWBWedmmX1bHNGVWmGjkfxsOtMebVRzMx1WpQUreKSj4IO8bfSV6J9UXgJiq3bEP49gL_egvXIS0bmol35MaN1Kna1hod40RmhxEgfsScuP3Lf35eLX1cjqvpM_B6xtfjStf63AYZ0-e0_oigcXJkTU6QJmC2XFeeoaCHWEdWrWo6lIGMbriv2vlqIn81qENAZ_d9aNGpd-LtUqjJgD299xEOFhO6OCKfjx61gRLwB18daRI_lm_ns9mHUug3T87Ovq-axDYB4NaHB2LvMVi0pXYsdxjlRD-fQ--dNz4JcdTxbuhuxbr8Q",
"token_type": "bearer",
"refresh_token":"eyJhbGciOiJSUzI1NiJ9.eyJsb2dpbklkIjoibmF2ZWVuIiwidXNlcl9uYW1lIjoibmF2ZWVuIiwic2NvcGUiOlsiY2l0eSIsInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiI3NWJhMDg5Zi1lNzNkLTQxYjItYTAzYi0zMGVkNGM3MjIyYzgiLCJleHAiOjE0NzUyMjc4NzUsImF1dGhvcml0aWVzIjpbIlRlc3RlciIsIlVzZXIiLCJBZG1pbiIsImNhbXBhaWduLWNhbmFsbCIsIm9yZy1jYW5hbGwiXSwianRpIjoiNjE1OWM4NTYtYTZmNi00Njg3LTg3OTMtMTA1NDdkODE4YmVhIiwiY2xpZW50X2lkIjoiY2l0eUNsaWVudElkIn0.fTJw2F3z-YZQOZ8gTLy9oZheIcZVP9UnbqhTFBG0kVuNojTO7NkzvrzdbG6CwHifolK4A31o2smmw5RHlx6224PgBnE-mCzI6lFG94O77IGvBfCNARJL_X6HbWm2wvtTNnz8k0UN_xPgqHtTpBcIUAHHxMG3TyFZFAoKWYbsQ6WL1mVNFwUxr2R60JYUlCPMB8Tl-2P9IEQr2FIH9amX80fsV23n8023quouwLOVmgUGyVzT1bJ1s2KtgQ51D3T6bvxR4IBlEhSYJ2hmt7DB1IbYQBkxWkd53BiMQQEPyFNgR_9JWFLH7Uq2TUOOb8xL_NnsoyAIO71IFxRPOOsN9w",
"expires_in": 359,
"scope": "city read write",
"loginId": "naveen",
"jti": "75ba089f-e73d-41b2-a03b-30ed4c7222c8"
使用上述刷新令牌第二次获取刷新令牌
"error": "invalid_grant",
"error_description": "Invalid refresh token: eyJhbGciOiJSUzI1NiJ9.eyJsb2dpbklkIjoibmF2ZWVuIiwidXNlcl9uYW1lIjoibmF2ZWVuIiwic2NvcGUiOlsiY2l0eSIsInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiI3NWJhMDg5Zi1lNzNkLTQxYjItYTAzYi0zMGVkNGM3MjIyYzgiLCJleHAiOjE0NzUyMjc4NzUsImF1dGhvcml0aWVzIjpbIlRlc3RlciIsIlVzZXIiLCJBZG1pbiIsImNhbXBhaWduLWNhbmFsbCIsIm9yZy1jYW5hbGwiXSwianRpIjoiNjE1OWM4NTYtYTZmNi00Njg3LTg3OTMtMTA1NDdkODE4YmVhIiwiY2xpZW50X2lkIjoiY2l0eUNsaWVudElkIn0.fTJw2F3z-YZQOZ8gTLy9oZheIcZVP9UnbqhTFBG0kVuNojTO7NkzvrzdbG6CwHifolK4A31o2smmw5RHlx6224PgBnE-mCzI6lFG94O77IGvBfCNARJL_X6HbWm2wvtTNnz8k0UN_xPgqHtTpBcIUAHHxMG3TyFZFAoKWYbsQ6WL1mVNFwUxr2R60JYUlCPMB8Tl-2P9IEQr2FIH9amX80fsV23n8023quouwLOVmgUGyVzT1bJ1s2KtgQ51D3T6bvxR4IBlEhSYJ2hmt7DB1IbYQBkxWkd53BiMQQEPyFNgR_9JWFLH7Uq2TUOOb8xL_NnsoyAIO71IFxRPOOsN9w"
【问题讨论】:
【参考方案1】:我遇到了同样的问题。
这个问题是由 JwtAccessTokenConverter 引起的。
在第一个请求中,它在访问令牌中接收一个 UID 刷新令牌,但在第二个请求中,它接收一个 Jwt 刷新令牌并再次对其进行编码,生成一个“新刷新令牌”。
我通过创建自定义 JwtAccessTokenConverter、提取刷新令牌并创建新的 OAuth2AccessToken 解决了这个问题。
我不知道这是最好的还是正确的解决方案......
public class CustomJwtAccessToKenConverter extends JwtAccessTokenConverter
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication)
OAuth2RefreshToken oAuth2RefreshToken = accessToken.getRefreshToken();
String refreshToken = "";
JsonParser objectMapper = JsonParserFactory.create();
Map<String, Object> claims = objectMapper.parseMap(JwtHelper.decode(oAuth2RefreshToken.getValue().getClaims());
if(claims.containsKey(TOKEN_ID))
refreshToken = claims.get(TOKEN_ID).toString();
DefaultOAuth2RefreshToken defaultOAuth2RefreshToken = new DefaultOAuth2RefreshToken(refreshToken);
DefaultOAuth2AccessToken defaultOAuth2AccessToken = new DefaultOAuth2AccessToken(accessToken);
defaultOAuth2AccessToken.setRefreshToken(defaultOAuth2RefreshToken);
return super.enhance(defaultOAuth2AccessToken, authentication);
【讨论】:
【参考方案2】:JdbcTokenStore 与 JWT 不兼容。 不要设置任何 tokenStore。
“持久化 JWT 令牌是无关紧要的,因为 JWT 令牌是自包含的” 资源: https://github.com/spring-projects/spring-security-oauth/issues/687
【讨论】:
【参考方案3】:我遇到了完全相同的问题。最初获取令牌,然后在商店中找不到 refresh_token 时出错。
AuthorizationServerConfigurerAdapter 扩展类中的此更改解决了该问题。
@Bean
public TokenStore tokenStore()
return new JwtTokenStore(accessTokenConverter());
// Error was defining a JdbcTokenStore!
// return new JdbcTokenStore(dataSource);
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints)
endpoints.tokenStore(tokenStore())
// Other options here.
非常感谢 克里斯托夫·罗德里格斯 我花了好几个小时试图弄清楚为什么数据库中的令牌与发出的令牌不匹配。
【讨论】:
【参考方案4】:JdbcTokenStore 我也面临同样的问题。 我通过在 AuthorizationServerEndpointsConfigurer 中设置 reuseRefreshTokens(false) 和在 DefaultTokenServices 中设置 setSupportRefreshToken(true) 解决了这个问题
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception
endpoints.tokenStore(tokenStore)
.reuseRefreshTokens(false)
.accessTokenConverter(accessTokenConverter)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
;
@Bean
@Primary
//Making this primary to avoid any accidental duplication with another token service instance of the same name
public DefaultTokenServices tokenServices()
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
【讨论】:
以上是关于Spring Boot + OAuth + JWT + MySQL 刷新令牌第二次不起作用的主要内容,如果未能解决你的问题,请参考以下文章
带有加密 JWT 访问令牌的 Spring Boot OAuth2