Oauth2授权模式password单一账号并发问题

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/chao_1990/article/details/83782147

使用场景:

        app程序为提高安全性,使用oauth2进行授权,授权方式采用password方式,IOS和Android在获取token时使用同一个用户名/密码(未加密)。

存在问题:

       app与本公司服务记性交互,通过https可以认为数据不会被劫持,单通过手机本地启动代理可以清晰明了的看见授权凭证,安全性降低,并且在同一个授权凭证大量并发时,会导致统一时间出现两个不同的access_token,也就表示会有一部分人在刚拿到access_token后就失效了。此种情况只会出现在创建access_token或是通过refresh_token获取access_token时才会出现,并且如果access_token有效时间长或是并发量小,此种问题不会频发出现。

解决方案:

       1、app端请求控制并发,也就是顺序请求,如果客户量大,还有会出现上述问题(公司以前的处理方式)。此方式并不推荐,能短时间解决问题,但是会引入诸多性能问题,如限制了客户端的开发模式,降低了客户端响应效率。

       2、引入锁,从根本上解决并发导致的问题,后面着重说明怎么实现。

 

实现方式:

1、先解决密码加密问题,一般采用RSA加密,客户端加密后的密文,到服务端是如何解析,何时解析才能保证后面的身份验证正常进行,代码如下

public class CustDaoAuthenticationProvider extends DaoAuthenticationProvider {
	@Override
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		if(authentication.getDetails() != null && authentication.getDetails() instanceof HashMap<?, ?>) {
			Map<String, String> map = (Map<String, String>) authentication.getDetails();
			//对密码进行RSA解密
			String pwd = authentication.getCredentials().toString();
			pwd = ValidateUtil.decodePasswd(pwd);
			authentication = new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), pwd);
					((AbstractAuthenticationToken) authentication).setDetails(map);
		}
		super.additionalAuthenticationChecks(userDetails, authentication);
	}
}

2、引入锁解决并发问题

自定义TokenStore及实现

public interface CustTokenStore extends TokenStore {

    /**
    *  @Method_Name             :callCustStoreAccessToken
    *  @param token
    *  @param authentication
    *  @return void
    *  @Creation Date           :2018/7/9
    *  @Author                  :zc.ding
    void callCustStoreAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication);
}

CustJdbcTokenStroe继承默认的JdbcTokenStore

/**
 * 解决获取access_token并发问题
 *
 * @author zc.ding
 * @create 2018/7/8
 */
public class CustJdbcTokenStore extends JdbcTokenStore implements CustTokenStore{

    private static final Log LOG = LogFactory.getLog(CustJdbcTokenStore.class);
    
    private static final String DEFAULT_ACCESS_TOKEN_STATEMENT = "insert into oauth_access_token (token_id, token, authentication_id, user_name, client_id, authentication, refresh_token) values (?, ?, ?, ?, ?, ?, ?)";

    private static final String DEFAULT_ACCESS_TOKEN_FROM_AUTHENTICATION_SELECT_STATEMENT = "select token_id, token from oauth_access_token where authentication_id = ?";
    
    private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();

    private final JdbcTemplate jdbcTemplateTmp;
    
    public CustJdbcTokenStore(DataSource dataSource) {
        super(dataSource);
        this.jdbcTemplateTmp = new JdbcTemplate(dataSource);
        super.setInsertAccessTokenSql(DEFAULT_ACCESS_TOKEN_STATEMENT);
    }
    
    @Override
    public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
        OAuth2AccessToken accessToken = null;

        String key = authenticationKeyGenerator.extractKey(authentication);
        try {
            accessToken = jdbcTemplateTmp.queryForObject(DEFAULT_ACCESS_TOKEN_FROM_AUTHENTICATION_SELECT_STATEMENT,
                    new RowMapper<OAuth2AccessToken>() {
                        @Override
                        public OAuth2AccessToken mapRow(ResultSet rs, int rowNum) throws SQLException {
                            return deserializeAccessToken(rs.getBytes(2));
                        }
                    }, key);
        }
        catch (EmptyResultDataAccessException e) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Failed to find access token for authentication " + authentication);
            }
        }
        catch (IllegalArgumentException e) {
            LOG.error("Could not extract access token for authentication " + authentication, e);
        }

        if (accessToken != null) {
            OAuth2Authentication authenticationTmp = readAuthentication(accessToken.getValue());
            if(authenticationTmp == null || !key.equals(authenticationKeyGenerator.extractKey(authenticationTmp))){
                removeAccessToken(accessToken.getValue());
                // Keep the store consistent (maybe the same user is represented by this authentication but the details have
                // changed)
                storeAccessToken(accessToken, authentication);
            }
        }
        return accessToken;
    }

    @Override
    public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
        //重写后不需要处理任何事情,全部交给callCustStoreAccessToken来处理
    }
    
    @Override
    public void callCustStoreAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication){
        super.storeAccessToken(token, authentication);
    }
}

自定义CustTokenServies继承DefaultTokenServices只更新生成access_token的部分,添加锁。

/**
 * 在createAccessToken加上分布式锁解决获取access_token并发问题
 *
 * @author zc.ding
 * @create 2018/7/9
 */
public class CustTokenServices extends DefaultTokenServices {

    private CustTokenStore tokenStore;

    private TokenEnhancer accessTokenEnhancer;
    
    @Transactional
    @Override
    public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
        //先从数据库检索是否已经存在access_token
        OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
        OAuth2RefreshToken refreshToken = null;
        //判断是否拿到了有效access_token
        if (existingAccessToken != null) {
            if (existingAccessToken.isExpired()) {
                //如果access_token已经过期,那么删除当前access_token对应的数据
                if (existingAccessToken.getRefreshToken() != null) {
                    refreshToken = existingAccessToken.getRefreshToken();
                    // The token store could remove the refresh token when the
                    // access token is removed, but we want to
                    // be sure...
                    tokenStore.removeRefreshToken(refreshToken);
                }
                tokenStore.removeAccessToken(existingAccessToken);
            }
            else {
                // Re-store the access token in case the authentication has changed
                //关闭accessToken刷新操作,防止数据库并发时死锁
//                tokenStore.storeAccessToken(existingAccessToken, authentication);
                return existingAccessToken;
            }
        }

        // Only create a new refresh token if there wasn't an existing one
        // associated with an expired access token.
        // Clients might be holding existing refresh tokens, so we re-use it in
        // the case that the old access token
        // expired.
        if (refreshToken == null) {
            refreshToken = createRefreshToken(authentication);
        }
        // But the refresh token itself might need to be re-issued if it has
        // expired.
        else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
            ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
            if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
                refreshToken = createRefreshToken(authentication);
            }
        }

        //执行到此处,要么是没有拿到有效access_token,要么是access_token已经过期,在从缓存中获取access_token,若果没有,通过分布式锁创建access_token对象
        //redis中存储的是access_token默认过期时间与数据库中存储的过期时间一致,这里做了双检索,增加了程序的健壮性
        OAuth2AccessToken accessToken = null;
        AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();
        String key = authenticationKeyGenerator.extractKey(authentication);
        String accessTokenKey = "ACCESS_TOKEN:" + key;
        //判断redis中是否存在有效的access_token
        accessToken = this.getExistAccessToken(accessTokenKey);
        if(accessToken != null){
            return accessToken;
        }
        //创建access_token创建时并发锁
        String lockKey = "LOCK_ACCESS_TOKEN_" + key;
        JedisClusterLock lock = new JedisClusterLock();
        try {
            if (lock.lock(lockKey)) {
                accessToken = getExistAccessToken(accessTokenKey);
                if (accessToken != null) {
                    return accessToken;
                }else{
                    accessToken = createAccessToken(authentication, refreshToken);
                    //调用重写存储access_token的方法
//                    tokenStore.storeAccessToken(accessToken, authentication);
                    tokenStore.callCustStoreAccessToken(accessToken, authentication);
                    // In case it was modified
                    refreshToken = accessToken.getRefreshToken();
                    if (refreshToken != null) {
                        tokenStore.storeRefreshToken(refreshToken, authentication);
                    }
                    JedisClusterUtils.setAsJson(accessTokenKey, accessToken);
                    JedisClusterUtils.setExpireTime(accessTokenKey, accessToken.getExpiresIn());
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.freeLock(lockKey);
        }
        return accessToken;
    }

    private OAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication) {
        if (!isSupportRefreshToken(authentication.getOAuth2Request())) {
            return null;
        }
        int validitySeconds = getRefreshTokenValiditySeconds(authentication.getOAuth2Request());
        String value = UUID.randomUUID().toString();
        if (validitySeconds > 0) {
            return new DefaultExpiringOAuth2RefreshToken(value, new Date(System.currentTimeMillis()
                    + (validitySeconds * 1000L)));
        }
        return new DefaultOAuth2RefreshToken(value);
    }

    private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
        DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
        int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
        if (validitySeconds > 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;
    }

    /**
     *  从redis获取有效的access_token
     *  @Method_Name             :getExistAccessToken
     *  @param accessTokenKey
     *  @return org.springframework.security.oauth2.common.OAuth2AccessToken
     *  @Creation Date           :2018/7/9
     *  @Author                  :zc.ding
     */
    private OAuth2AccessToken getExistAccessToken(String accessTokenKey){
        OAuth2AccessToken accessTokenTmp = JedisClusterUtils.getObjectForJson(accessTokenKey, DefaultOAuth2AccessToken.class);
        long time = JedisClusterUtils.getRemainTime(accessTokenKey);
        if(accessTokenTmp != null && JedisClusterUtils.getRemainTime(accessTokenKey) > 0){
            ((DefaultOAuth2AccessToken) accessTokenTmp).setExpiration(new Date(System.currentTimeMillis() + (time * 1000L)));
            return accessTokenTmp;
        }
        return null;
    }

//    @Override
    public void setTokenStore(CustTokenStore tokenStore) {
        this.tokenStore = tokenStore;
        super.setTokenStore(tokenStore);
    }

    public void setAccessTokenEnhancer(TokenEnhancer accessTokenEnhancer) {
        this.accessTokenEnhancer = accessTokenEnhancer;
    }

    @Override
    public void setClientDetailsService(ClientDetailsService clientDetailsService) {
        super.setClientDetailsService(clientDetailsService);
    }
}

实现原理,就是保证在同一位置来执行access_token存储的操作,上述只是一种实现方式,第二种实现方式是只重写JdbcTokenStore的storeAccessToken方法,在存储的过程添加锁。

3、在oauth2的启动配置中,加载自定义配置

/**
	 * @Description : 授权服务器
	 * @Project : hk-api-services
	 * @Program Name : com.hongkun.finance.api.oauth.OAuth2ServerConfig.java
	 * @Author :  zc.ding
	 */
	@Configuration
	@EnableAuthorizationServer
	protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
		@Autowired
		private DataSource dataSource;

		@Autowired
		private CustTokenStore tokenStore;
		
        @Autowired
		private CustTokenServices custTokenServices;

		@Autowired
		private JdbcClientDetailsService jdbcClientDetailsService;

		/**
		 * authenticationManagerBean在SecurityConfig完成初始化
		 */
		@Autowired
		@Qualifier("authenticationManagerBean")
		private AuthenticationManager authenticationManager;

		@Override
		public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
			clients.withClientDetails(this.jdbcClientDetailsService);
		}
		

		/**
		 * @Description : token存储介质,可自定义
		 * @Method_Name : tokenStore
		 * @return : TokenStore
		 * @Creation Date : 2018年3月20日 下午4:20:42
		 * @Author :  zc.ding
		 */
		@Bean
		public CustTokenStore tokenStore() {
			return new CustJdbcTokenStore(dataSource);
		}

		/**
		 * @Description : clinetDetail维护实现类,可自定义
		 * @Method_Name : jdbcClientDetailsService
		 * @return : JdbcClientDetailsService
		 * @Creation Date : 2018年3月20日 下午4:18:11
		 * @Author :  zc.ding
		 */
		@Bean
		public JdbcClientDetailsService jdbcClientDetailsService() {
			return new JdbcClientDetailsService(dataSource);
		}

		/**
		*  用于解决获取access_token并发问题
		*  @Method_Name             :custTokenServices
		* 
		*  @return com.hongkun.finance.api.oauth.extend.CustTokenServices
		*  @Creation Date           :2018/7/9
		*  @Author                  :zc.ding
		*/
		@Bean
        public CustTokenServices custTokenServices(){
            CustTokenServices custTokenServices = new CustTokenServices();
            custTokenServices.setTokenStore(tokenStore);
            custTokenServices.setClientDetailsService(jdbcClientDetailsService);
		    return custTokenServices;    
        }
        
		@Override
		public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		    //由于加载顺序问题,此处需要设置下面属性
            custTokenServices.setClientDetailsService(jdbcClientDetailsService);
            custTokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
            custTokenServices.setTokenStore(tokenStore);
            endpoints.tokenStore(tokenStore).authenticationManager(authenticationManager)
					// 支持POST和GET请求,生产环境支持POST请求, HttpMethod.GET
					.allowedTokenEndpointRequestMethods(HttpMethod.POST).tokenServices(custTokenServices);
		}

		@Override
		public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
			// 允许security和aouth使用相同身份验证
			oauthServer.allowFormAuthenticationForClients();
		}
	}

至此oauth2密码(password)授权模式,单用户并发问题已经彻底解决。

tips:如果是自己的APP,可以将access_token的有限期设置长一些,不要占用更多的资源来处理token。

 

展开阅读全文

没有更多推荐了,返回首页