OAuth2-SpringBoot - 刷新令牌

Posted

技术标签:

【中文标题】OAuth2-SpringBoot - 刷新令牌【英文标题】:OAuth2-SpringBoot - Refresh token 【发布时间】:2017-12-13 12:29:27 【问题描述】:

我已将我的 Spring Boot 应用程序配置为提供 oauth2 授权。

@Configuration
public class OAuth2Configuration 

    @Configuration
    @EnableResourceServer
    protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter 

       @Autowired
       private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;

       @Autowired
       private CustomLogoutSuccessHandler customLogoutSuccessHandler;

       @Override
       public void configure(HttpSecurity http) throws Exception 
           http.exceptionHandling()
               .authenticationEntryPoint(customAuthenticationEntryPoint)
                        .and()
                        .logout()
                        .logoutUrl("/oauth/logout")
                        .logoutSuccessHandler(customLogoutSuccessHandler)
                        .and()
                        .csrf()
                        .disable()
                        .headers()
                        .frameOptions().disable()
                        .exceptionHandling().and()
                        .sessionManagement()
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                        .and()
                        .authorizeRequests()
                        .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                        .antMatchers("/api/v1/login/**").permitAll()
                        .antMatchers("/api/v1/admin/**").permitAll()
                        .antMatchers("/api/v1/test/**").permitAll()
                        .antMatchers("/oauth/token").permitAll()
                        .antMatchers("/api/**").authenticated();
            
        

    @Configuration
    @EnableAuthorizationServer
    protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter implements EnvironmentAware 

        private static final String ENV_OAUTH = "authentication.oauth.";
        private static final String PROP_CLIENTID = "clientid";
        private static final String PROP_SECRET = "secret";
        private static final String PROP_TOKEN_VALIDITY_SECONDS = "tokenValidityInSeconds";
        private RelaxedPropertyResolver propertyResolver;

        @Autowired
        private DataSource dataSource;

        @Bean
        public TokenStore tokenStore() 
            return new JdbcTokenStore(dataSource);
        

        @Autowired
        @Qualifier("authenticationManagerBean")
        private AuthenticationManager authenticationManager;

        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception 
            endpoints.tokenStore(tokenStore())
                     .authenticationManager(authenticationManager);
        

        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception 
            clients.inMemory()
                   .withClient(propertyResolver.getProperty(PROP_CLIENTID))
                   .scopes("read", "write")
                   .authorities(Authorities.ROLE_USER.name())
                   .authorizedGrantTypes("password", "refresh_token", "authorization_code", "implicit")
                   .secret(propertyResolver.getProperty(PROP_SECRET))
                   .accessTokenValiditySeconds(
                    propertyResolver.getProperty(PROP_TOKEN_VALIDITY_SECONDS, Integer.class, 1800))
                   .refreshTokenValiditySeconds(100000);
            

        @Override
        public void setEnvironment(Environment environment) 
            this.propertyResolver = new RelaxedPropertyResolver(environment, ENV_OAUTH);
        
    


@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter 
    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public CustomPasswordEncoder passwordEncoder() 
        return new CustomPasswordEncoder();
    

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception 
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
    

    @Override
    public void configure(WebSecurity web) throws Exception 
        web.ignoring()
           .antMatchers(HttpMethod.OPTIONS, "/**").antMatchers("/api/login/**");
    

    @Override
    public void configure(HttpSecurity http) throws Exception       
        http.httpBasic().realmName("WebServices").and().sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
            .requestMatchers().antMatchers("/oauth/authorize").and()
            .authorizeRequests().antMatchers("/oauth/authorize")
            .authenticated();
    

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception 
        return super.authenticationManagerBean();
    

    @EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
    private static class GlobalSecurityConfiguration extends GlobalMethodSecurityConfiguration 
        @Override
        protected MethodSecurityExpressionHandler createExpressionHandler() 
            return new OAuth2MethodSecurityExpressionHandler();
        
    


public class UserDetailsServiceImpl implements UserDetailsService 
    @Inject
    private AccountDao accountDao;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(final String login) 
        Account userFromDatabase = null;
        String lowercaseLogin = login.toLowerCase();
        if (lowercaseLogin.contains("@")) 
            userFromDatabase = accountDao.getByEmailId(lowercaseLogin);
         else 
            userFromDatabase = accountDao.getByPhoneNumber(lowercaseLogin);
        

        if (userFromDatabase != null) 
            if (!userFromDatabase.getActivated()) 
                throw new UserNotActivatedException("User " + lowercaseLogin + " was not activated");
            
            List<GrantedAuthority> grantedAuthorities = userFromDatabase.getRoles().stream()
                    .map(authority -> new SimpleGrantedAuthority(authority.getRoleName())).collect(Collectors.toList());
            return new org.springframework.security.core.userdetails.User(userFromDatabase.getAccountName(),
                    userFromDatabase.getAccountPassword(), grantedAuthorities);
         else 
            throw new UsernameNotFoundException("User " + lowercaseLogin + " was not found in the " + "database");
        
    

现在每当我在访问令牌过期后尝试获取刷新令牌时,我总是会得到

2017-07-10 00:57:40.797 信息 68115 --- [nio-9090-exec-4] os.s.o.provider.endpoint.TokenEndpoint :处理错误: NoSuchClientException,没有请求 id 的客户端:12345678

虽然数据库中有一行,列电话号码为 12345678,帐户名称为 12345678。

https://myTestWebServices/oauth/token?grant_type=refresh_token&refresh_token=f4cc8213-3f2b-4a30-965b-6feca898479e

我将标题设置为Authorization: Basic xxx xxx 与我用来获取 access_token 的相同,所以我假设它工作正常。

但是输出总是这个

"error": "unauthorized", "error_description": "用户 12345678 是 在数据库中找不到”

【问题讨论】:

发布完整的堆栈跟踪。并解释您的 OAuth2 流程是如何获取刷新令牌的。 @AbhijitSarkar 请立即查看 你试过我的建议了吗? 你有解决办法吗? 我有同样的问题,使用授权类型密码我能够获取令牌,但使用授权类型“刷新令牌”获取异常用户未找到,因为用户详细信息服务(loaduserByUsername 方法)获取用户名为空,但授予类型密码正确 【参考方案1】:

您应该在获取时传递 clientIdclient secret(它们与 userIdpassword 不同) access token 使用 refresh token。不确定 authorisation 标头中传递了什么。

您似乎遇到了两个不同的问题。什么时候出现以下错误:

"error": "unauthorized", "error_description": "用户 12345678 不是 在数据库中找到”

您能否验证用户是否成功认证以及服务是否返回访问令牌和刷新令牌?您可以将调试指针放在UserDetailsService 并检查流程。

尝试按照以下步骤验证配置:

获取刷新令牌,假设您正在使用

curl -vu clientId:clientSecret 'http://your_domain_url/api/oauth/token?username=userName&password=password&grant_type=password'

这里的用户名密码不同于客户端ID客户端密码 这应该会在响应中返回您的刷新令牌和访问令牌

"access_token":"d5deb98a-75fc-4f3a-bbfd-e5c87ca2ca6f","token_type":"bearer","refresh_token":"b2be4291-57e9-4b28-b114-feb3406e030d","expires_in":2,"scope":"read write"

上面的响应获得了访问令牌和刷新令牌。每当访问令牌过期时,您都可以使用刷新令牌来获取访问令牌,如下所示:

curl -vu clientId:clientSecret 'http://your_domain_url/api/oauth/token?grant_type=refresh_token&refresh_token=refresh_token_value'

回复:

"access_token":"13fd30f9-f0c5-414e-9fbd-a5e2f9f3e4a7","token_type":"bearer","refresh_token":"b2be4291-57e9-4b28-b114-feb3406e030d","expires_in":2,"scope":"read write"

现在您可以使用访问令牌进行服务调用

curl -i -H "Authorization: Bearer 13fd30f9-f0c5-414e-9fbd-a5e2f9f3e4a7" http://your_domain_url/api/mySecureApi

【讨论】:

【参考方案2】:

我认为密码grant_type 需要clientId 和clientSecret。您传递 Base64 编码的 clientId 和 clientSecret 而不是 Authorization 标头中的访问令牌。像这样:

curl -H "Authorization: Bearer [base64encode(clientId:clientSecret)]" "https://yourdomain.com/oauth/token?grant_type=refresh_token&refresh_token=[yourRefreshToken]"

我假设你首先得到了这样的令牌(即使我问了你也没有说):

curl --data "grant_type=password&username=user&password=pass&client_id=my_client" http://localhost:8080/oauth/token"

另外,在loadUserByUsername 中放置一个断点,并检查它是否因刷新尝试失败而被调用。

【讨论】:

我也有同样的问题【参考方案3】:

只需添加 UserDetailsS​​ervice 即可使用

@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception 
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
                .tokenStore(tokenStore())
                .accessTokenConverter(accessTokenConverter());
    

请求

http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=myclient&client_secret=secret&refresh_token=<token>

【讨论】:

拯救了我的一天。谢谢

以上是关于OAuth2-SpringBoot - 刷新令牌的主要内容,如果未能解决你的问题,请参考以下文章

滑动刷新令牌生存期过期后注销用户

试图找到一个带有刷新令牌的“万无一失”oauth 架构示例

OkHttp 和 Retrofit,用并发请求刷新令牌

react-query 遇到 401 时尝试刷新令牌并使查询无效

Node.js 护照 OAuth 2.0 身份验证:存储访问和刷新令牌的位置

Next.js s-s-r 存储访问令牌而不使用 redux