Spring OAuth2为令牌端点的每个请求生成访问令牌

时间:2014-11-19 15:31:53

标签: java spring oauth spring-security

是否可以使用每个请求的client_credentials或密码授予类型生成多个有效的访问令牌?

使用上述授权类型生成令牌只会在当前请求到期时提供新令牌。

我可以使用密码授予类型生成刷新令牌,然后生成多个访问令牌,但这样做会使先前的任何访问令牌无效。

我知道如何更改以允许每次请求生成访问令牌到/ oauth / token端点并确保以前的任何令牌都没有失效?

以下是我的oauth服务器的XML配置。

<!-- oauth2 config start-->
  <sec:http pattern="/test/oauth/token" create-session="never"
              authentication-manager-ref="authenticationManager" > 
        <sec:intercept-url pattern="/test/oauth/token" access="IS_AUTHENTICATED_FULLY" />
        <sec:anonymous enabled="false" />
        <sec:http-basic entry-point-ref="clientAuthenticationEntryPoint"/>
        <sec:custom-filter ref="clientCredentialsTokenEndpointFilter" before="BASIC_AUTH_FILTER" /> 
        <sec:access-denied-handler ref="oauthAccessDeniedHandler" /> 
    </sec:http>


    <bean id="clientCredentialsTokenEndpointFilter"
          class="org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter">
        <property name="authenticationManager" ref="authenticationManager" />
    </bean>

    <sec:authentication-manager alias="authenticationManager">
        <sec:authentication-provider user-service-ref="clientDetailsUserService" />
    </sec:authentication-manager>

    <bean id="clientDetailsUserService"
          class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService">
        <constructor-arg ref="clientDetails" />
    </bean>

    <bean id="clientDetails" class="org.security.oauth2.ClientDetailsServiceImpl"></bean>

    <bean id="clientAuthenticationEntryPoint"
          class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
        <property name="realmName" value="springsec/client" />
        <property name="typeName" value="Basic" />
    </bean>

    <bean id="oauthAccessDeniedHandler"
          class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler"/>

    <oauth:authorization-server
        client-details-service-ref="clientDetails" token-services-ref="tokenServices">
        <oauth:authorization-code />
        <oauth:implicit/>
        <oauth:refresh-token/>
        <oauth:client-credentials />
        <oauth:password authentication-manager-ref="userAuthenticationManager"/>
    </oauth:authorization-server>

    <sec:authentication-manager id="userAuthenticationManager">
        <sec:authentication-provider  ref="customUserAuthenticationProvider">
        </sec:authentication-provider>
    </sec:authentication-manager>

    <bean id="customUserAuthenticationProvider"
          class="org.security.oauth2.CustomUserAuthenticationProvider">
    </bean>

    <bean id="tokenServices" 
          class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
        <property name="tokenStore" ref="tokenStore" />
        <property name="supportRefreshToken" value="true" />
        <property name="accessTokenValiditySeconds" value="300"></property>
        <property name="clientDetailsService" ref="clientDetails" />
    </bean>

    <bean id="tokenStore" class="org.springframework.security.oauth2.provider.token.store.JdbcTokenStore">
        <constructor-arg ref="jdbcTemplate" />
    </bean>

    <bean id="jdbcTemplate"
           class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/oauthdb"/>
        <property name="username" value="root"/>
        <property name="password" value="password"/>
    </bean>
    <bean id="oauthAuthenticationEntryPoint"
          class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
    </bean>

3 个答案:

答案 0 :(得分:17)

于2014年11月21日更新

当我仔细检查时,我发现InMemoryTokenStore使用OAuth2Authentication的哈希字符串作为Map的关键字。当我使用相同的用户名,client_id,范围..并且我得到相同的key。所以这可能会导致一些问题。所以我认为旧的方式已被弃用。以下是我为避免此问题所做的工作。

创建另一个可以计算唯一键的AuthenticationKeyGenerator,名为UniqueAuthenticationKeyGenerator

/*
 * Copyright 2006-2011 the original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */

/**
 * Basic key generator taking into account the client id, scope, resource ids and username (principal name) if they
 * exist.
 * 
 * @author Dave Syer
 * @author thanh
 */
public class UniqueAuthenticationKeyGenerator implements AuthenticationKeyGenerator {

    private static final String CLIENT_ID = "client_id";

    private static final String SCOPE = "scope";

    private static final String USERNAME = "username";

    private static final String UUID_KEY = "uuid";

    public String extractKey(OAuth2Authentication authentication) {
        Map<String, String> values = new LinkedHashMap<String, String>();
        OAuth2Request authorizationRequest = authentication.getOAuth2Request();
        if (!authentication.isClientOnly()) {
            values.put(USERNAME, authentication.getName());
        }
        values.put(CLIENT_ID, authorizationRequest.getClientId());
        if (authorizationRequest.getScope() != null) {
            values.put(SCOPE, OAuth2Utils.formatParameterList(authorizationRequest.getScope()));
        }
        Map<String, Serializable> extentions = authorizationRequest.getExtensions();
        String uuid = null;
        if (extentions == null) {
            extentions = new HashMap<String, Serializable>(1);
            uuid = UUID.randomUUID().toString();
            extentions.put(UUID_KEY, uuid);
        } else {
            uuid = (String) extentions.get(UUID_KEY);
            if (uuid == null) {
                uuid = UUID.randomUUID().toString();
                extentions.put(UUID_KEY, uuid);
            }
        }
        values.put(UUID_KEY, uuid);

        MessageDigest digest;
        try {
            digest = MessageDigest.getInstance("MD5");
        }
        catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("MD5 algorithm not available.  Fatal (should be in the JDK).");
        }

        try {
            byte[] bytes = digest.digest(values.toString().getBytes("UTF-8"));
            return String.format("%032x", new BigInteger(1, bytes));
        }
        catch (UnsupportedEncodingException e) {
            throw new IllegalStateException("UTF-8 encoding not available.  Fatal (should be in the JDK).");
        }
    }
}

最后,将它们连接起来

<bean id="tokenStore" class="org.springframework.security.oauth2.provider.token.store.JdbcTokenStore">
    <constructor-arg ref="jdbcTemplate" />
    <property name="authenticationKeyGenerator">
        <bean class="your.package.UniqueAuthenticationKeyGenerator" />
    </property>
</bean>

以下方式可能会导致一些问题,请参阅更新的答案!!!

<德尔> 您正在使用DefaultTokenServices。试试这段代码,确保重新定义你的`tokenServices`     包com.thanh.backend.oauth2.core;     import java.util.Date;     import java.util.UUID;     import org.springframework.security.core.AuthenticationException;     import org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken;     import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;     import org.springframework.security.oauth2.common.ExpiringOAuth2RefreshToken;     import org.springframework.security.oauth2.common.OAuth2AccessToken;     import org.springframework.security.oauth2.common.OAuth2RefreshToken;     import org.springframework.security.oauth2.provider.OAuth2Authentication;     import org.springframework.security.oauth2.provider.token.DefaultTokenServices;     import org.springframework.security.oauth2.provider.token.TokenEnhancer;     import org.springframework.security.oauth2.provider.token.TokenStore;     / **      * @author thanh      * /     公共类SimpleTokenService扩展DefaultTokenServices {         private TokenStore tokenStore;         private TokenEnhancer accessTokenEnhancer;         @覆盖         public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication)抛出AuthenticationException {             OAuth2RefreshToken refreshToken = createRefreshToken(authentication);;             OAuth2AccessToken accessToken = createAccessToken(authentication,refreshToken);             tokenStore.storeAccessToken(accessToken,authentication);             tokenStore.storeRefreshToken(refreshToken,authentication);             return accessToken;         }         private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication,OAuth2RefreshToken refreshToken){             DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID()。toString());             int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());             if(validitySeconds&gt; 0){                 token.setExpiration(new Date(System.currentTimeMillis()+(validitySeconds * 1000L)));             }             token.setRefreshToken(refreshToken);             token.setScope(authentication.getOAuth2Request()getScope());             return accessTokenEnhancer!= null? accessTokenEnhancer.enhance(token,authentication):token;         }         private ExpiringOAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication){             if(!isSupportRefreshToken(authentication.getOAuth2Request())){                 return null;             }             int validitySeconds = getRefreshTokenValiditySeconds(authentication.getOAuth2Request());             ExpiringOAuth2RefreshToken refreshToken = new DefaultExpiringOAuth2RefreshToken(UUID.randomUUID()。toString(),                 new Date(System.currentTimeMillis()+(validitySeconds * 1000L)));             return refreshToken;         }         @覆盖         public void setTokenEnhancer(TokenEnhancer accessTokenEnhancer){             super.setTokenEnhancer(accessTokenEnhancer);             this.accessTokenEnhancer = accessTokenEnhancer;         }         @覆盖         public void setTokenStore(TokenStore tokenStore){             super.setTokenStore(tokenStore);             this.tokenStore = tokenStore;         }     }

答案 1 :(得分:1)

以下@Thanh Nguyen Van的方法:

在使用Spring Boot和OAuth2开发后端时,我偶然发现了相同的问题。我遇到的问题是,如果多个设备共享相同的令牌,则一台设备刷新令牌后,另一台设备将变得毫无头绪,长话短说,两台设备都进入了令牌刷新狂潮。我的解决方案是用自定义实现替换默认的AuthenticationKeyGenerator,该实现重写DefaultAuthenticationKeyGenerator并在密钥生成器混合中添加新的参数client_instance_id。然后,我的移动客户端将发送此参数,该参数在应用程序安装(iOS或Android)中必须是唯一的。这不是特别的要求,因为大多数移动应用程序已经以某种形式跟踪了应用程序实例。

public class EnhancedAuthenticationKeyGenerator extends DefaultAuthenticationKeyGenerator {

    public static final String PARAM_CLIENT_INSTANCE_ID = "client_instance_id";

    private static final String KEY_SUPER_KEY = "super_key";
    private static final String KEY_CLIENT_INSTANCE_ID = PARAM_CLIENT_INSTANCE_ID;

    @Override
    public String extractKey(final OAuth2Authentication authentication) {
        final String superKey = super.extractKey(authentication);

        final OAuth2Request authorizationRequest = authentication.getOAuth2Request();
        final Map<String, String> requestParameters = authorizationRequest.getRequestParameters();

        final String clientInstanceId = requestParameters != null ? requestParameters.get(PARAM_CLIENT_INSTANCE_ID) : null;
        if (clientInstanceId == null || clientInstanceId.length() == 0) {
            return superKey;
        }

        final Map<String, String> values = new LinkedHashMap<>(2);
        values.put(KEY_SUPER_KEY, superKey);
        values.put(KEY_CLIENT_INSTANCE_ID, clientInstanceId);

        return generateKey(values);
    }

}

随后将以类似方式注入

final JdbcTokenStore tokenStore = new JdbcTokenStore(mDataSource);
tokenStore.setAuthenticationKeyGenerator(new EnhancedAuthenticationKeyGenerator());

然后,HTTP请求将如下所示

POST /oauth/token HTTP/1.1
Host: {{host}}
Authorization: Basic {{auth_client_basic}}
Content-Type: application/x-www-form-urlencoded

grant_type=password&username={{username}}&password={{password}}&client_instance_id={{instance_id}}

使用此方法的好处是,如果客户端不发送client_instance_id,则将生成默认密钥,并且如果提供了实例,则每次为相同对象返回相同的密钥实例。同样,关键是平台无关。不利之处是MD5摘要(内部使用)被调用了两次。

答案 2 :(得分:0)

不要在后端设置任何作用域值,并且在生成访问令牌时将sessionId或deviceId或任何唯一ID设置为作用域,那么对于相同的客户端和用户组合,您将始终获得新的令牌。