springboot2.0.X使用spring security oauth2

最近收到一个任务使用spring security做一个oauth2的授权服务器和资源服务器,授权方式为Client_Credentials写一个demo,官方文档对这方面的描述比较少,官方项目中只有一个demo,是个spring项目,所有东西都存在了内存中,虽然他提供了Enable注解,因为我们要求的是client-id,client-secret等存入数据库把token存入redis,当然他官方提供了实现,但是在springboot2.X中redis的默认连接客户端不再是jedis而是lettuce,官方使用的set方法会直接报错,该方法已经被废弃,所以需要把官方的操作调用的api全部更换掉。这里不说其他的授权方式,以及其他的存储方式,有需要的可以自行研究一下,包括jwt的使用。

这里我贴一些oauth2相关的文档,在使用oauth2的时候一定要知道它是一个什么东西,有经验的人应该都对接过微信或者支付宝等第三方平台提供的api,基本现在第三方平台都用的是oauth2的方式,之前用的oauth,很多都进行的升级,使用了oauth2,因为俩个不兼容。

Oauth2的中文文档地址:https://github.com/jeansfish/RFC6749.zh-cn/blob/master/index.md

Oauth2的英文文档地址:https://tools.ietf.org/html/rfc6749

OpenID Connect 规范协议地址:https://openid.net/specs/openid-connect-core-1_0.html

Spring security Github官方地址:https://github.com/spring-projects/spring-security

Spring security oauth2 Github开发人员指南地址:https://github.com/spring-projects/spring-security-oauth/blob/master/docs/oauth2.md

Spring security oauth2的项目地址也在上方

我自己的demo地址:https://github.com/451846939/spring-security-oauth2

详细代码和配置请参考我上面的demo地址

开发之前请把上方的文档全部读一遍,一定要读Oauth2的4种授权方式,包括他们各自需要传入的参数,因为security oauth2等等的框架都是基于协议的实现,不是凭空而来

这里不进行官方自动装配的源码分析了,因为官方的有些设置需要我们重新写,毕竟不附和我们自己的要求,当然我也不是很理解官方这里的自动装配为何不提供一个策略模式供我们自己配置选择

官方提供的

springboot2.0.X使用spring security oauth2

jdbcClientDetailsService可以直接使用不过官方这边oauth2实现的代码多次进行了数据库的查询,一次请求查询了多次的数据库,查询的都是同一条数据,我建议实际使用的时候可能需要改一下,当然官方提供的TokenStore中的reidsTokenStore是不可以用的

我这里重新改了他原来调用redis的Api得以正常使用

springboot2.0.X使用spring security oauth2

我自己的类是Boot2RedisTokenStore

这里贴出我的配置类

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.security.oauth2.authserver.AuthorizationServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.builders.JdbcClientDetailsServiceBuilder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.token.AccessTokenConverter;
import org.springframework.security.oauth2.provider.token.TokenStore;

import javax.annotation.Resource;
import javax.sql.DataSource;
@Configuration
@ConditionalOnClass(EnableJDBCAuthorizationServer.class)
@EnableConfigurationProperties(AuthorizationServerProperties.class)
public class JDBCAuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {

    private  AuthenticationManager authenticationManager;
    @Resource
    private  TokenStore tokenStore;
    private  AccessTokenConverter tokenConverter;
    @Resource
    private  AuthorizationServerProperties properties;

    public JDBCAuthorizationServerConfiguration(ObjectProvider<AccessTokenConverter> tokenConverter, AuthenticationConfiguration authenticationConfiguration) throws Exception {
        this.authenticationManager = authenticationConfiguration.getAuthenticationManager();
        this.tokenConverter=tokenConverter.getIfAvailable();
    }

    @Resource
    private DataSource dataSource;
    @Bean
    public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory){
       return new Boot2RedisTokenStore(redisConnectionFactory);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        JdbcClientDetailsServiceBuilder jdbc = clients.jdbc(dataSource);
        ClientDetailsService build = jdbc.build();
        clients.withClientDetails(build);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        if (this.tokenConverter != null) {
            endpoints.accessTokenConverter(this.tokenConverter);
        }
        if (this.tokenStore != null) {
            endpoints.tokenStore(this.tokenStore);
        }
        if (this.authenticationManager!=null) {
            endpoints.authenticationManager(this.authenticationManager);
        }
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.passwordEncoder(NoOpPasswordEncoder.getInstance());
        if (this.properties.getCheckTokenAccess() != null) {
            security.checkTokenAccess(this.properties.getCheckTokenAccess());
        }
        if (this.properties.getTokenKeyAccess() != null) {
            security.tokenKeyAccess(this.properties.getTokenKeyAccess());
        }
        if (this.properties.getRealm() != null) {
            security.realm(this.properties.getRealm());
        }
    }

}
import org.springframework.context.annotation.Import;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerSecurityConfiguration;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AuthorizationServerEndpointsConfiguration.class, AuthorizationServerSecurityConfiguration.class})
public @interface EnableJDBCAuthorizationServer {
}

Boot2ReidsTokenStore

import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
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.AuthenticationKeyGenerator;
import org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.JdkSerializationStrategy;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStoreSerializationStrategy;

import java.util.*;

public class Boot2RedisTokenStore implements TokenStore {

        private static final String ACCESS = "access:";
        private static final String AUTH_TO_ACCESS = "auth_to_access:";
        private static final String AUTH = "auth:";
        private static final String REFRESH_AUTH = "refresh_auth:";
        private static final String ACCESS_TO_REFRESH = "access_to_refresh:";
        private static final String REFRESH = "refresh:";
        private static final String REFRESH_TO_ACCESS = "refresh_to_access:";
        private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:";
        private static final String UNAME_TO_ACCESS = "uname_to_access:";

        private final RedisConnectionFactory connectionFactory;
        private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();
        private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy();

        private String prefix = "";

        public Boot2RedisTokenStore(RedisConnectionFactory connectionFactory) {
            this.connectionFactory = connectionFactory;
        }

        public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) {
            this.authenticationKeyGenerator = authenticationKeyGenerator;
        }

        public void setSerializationStrategy(RedisTokenStoreSerializationStrategy serializationStrategy) {
            this.serializationStrategy = serializationStrategy;
        }

        public void setPrefix(String prefix) {
            this.prefix = prefix;
        }

        private RedisConnection getConnection() {
            return connectionFactory.getConnection();
        }

        private byte[] serialize(Object object) {
            return serializationStrategy.serialize(object);
        }

        private byte[] serializeKey(String object) {
            return serialize(prefix + object);
        }

        private OAuth2AccessToken deserializeAccessToken(byte[] bytes) {
            return serializationStrategy.deserialize(bytes, OAuth2AccessToken.class);
        }

        private OAuth2Authentication deserializeAuthentication(byte[] bytes) {
            return serializationStrategy.deserialize(bytes, OAuth2Authentication.class);
        }

        private OAuth2RefreshToken deserializeRefreshToken(byte[] bytes) {
            return serializationStrategy.deserialize(bytes, OAuth2RefreshToken.class);
        }

        private byte[] serialize(String string) {
            return serializationStrategy.serialize(string);
        }

        private String deserializeString(byte[] bytes) {
            return serializationStrategy.deserializeString(bytes);
        }

        @Override
        public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
            String key = authenticationKeyGenerator.extractKey(authentication);
            byte[] serializedKey = serializeKey(AUTH_TO_ACCESS + key);
            byte[] bytes = null;
            RedisConnection conn = getConnection();
            try {
                bytes = conn.stringCommands().get(serializedKey);
            } finally {
                conn.close();
            }
            OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
            if (accessToken != null) {
                OAuth2Authentication storedAuthentication = readAuthentication(accessToken.getValue());
                if ((storedAuthentication == null || !key.equals(authenticationKeyGenerator.extractKey(storedAuthentication)))) {
                    // Keep the stores consistent (maybe the same user is
                    // represented by this authentication but the details have
                    // changed)
                    storeAccessToken(accessToken, authentication);
                }

            }
            return accessToken;
        }

        @Override
        public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
            return readAuthentication(token.getValue());
        }

        @Override
        public OAuth2Authentication readAuthentication(String token) {
            byte[] bytes = null;
            RedisConnection conn = getConnection();
            try {
                bytes = conn.stringCommands().get(serializeKey(AUTH + token));
            } finally {
                conn.close();
            }
            OAuth2Authentication auth = deserializeAuthentication(bytes);
            return auth;
        }

        @Override
        public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {
            return readAuthenticationForRefreshToken(token.getValue());
        }

        public OAuth2Authentication readAuthenticationForRefreshToken(String token) {
            RedisConnection conn = getConnection();
            try {
                byte[] bytes = conn.stringCommands().get(serializeKey(REFRESH_AUTH + token));
                OAuth2Authentication auth = deserializeAuthentication(bytes);
                return auth;
            } finally {
                conn.close();
            }
        }

        @Override
        public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
            byte[] serializedAccessToken = serialize(token);
            byte[] serializedAuth = serialize(authentication);
            byte[] accessKey = serializeKey(ACCESS + token.getValue());
            byte[] authKey = serializeKey(AUTH + token.getValue());
            byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication));
            byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication));
            byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId());

            RedisConnection conn = getConnection();
            try {
                conn.openPipeline();
                conn.stringCommands().set(accessKey, serializedAccessToken);
                conn.stringCommands().set(authKey, serializedAuth);
                conn.stringCommands().set(authToAccessKey, serializedAccessToken);
                if (!authentication.isClientOnly()) {
                    conn.listCommands().rPush(approvalKey, serializedAccessToken);
                }
                conn.listCommands().rPush(clientId, serializedAccessToken);
                if (token.getExpiration() != null) {
                    int seconds = token.getExpiresIn();
                    conn.keyCommands().expire(accessKey, seconds);
                    conn.keyCommands().expire(authKey, seconds);
                    conn.keyCommands().expire(authToAccessKey, seconds);
                    conn.keyCommands().expire(clientId, seconds);
                    conn.keyCommands().expire(approvalKey, seconds);
                }
                OAuth2RefreshToken refreshToken = token.getRefreshToken();
                if (refreshToken != null && refreshToken.getValue() != null) {
                    byte[] refresh = serialize(token.getRefreshToken().getValue());
                    byte[] auth = serialize(token.getValue());
                    byte[] refreshToAccessKey = serializeKey(REFRESH_TO_ACCESS + token.getRefreshToken().getValue());
                    conn.stringCommands().set(refreshToAccessKey, auth);
                    byte[] accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + token.getValue());
                    conn.stringCommands().set(accessToRefreshKey, refresh);
                    if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
                        ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken;
                        Date expiration = expiringRefreshToken.getExpiration();
                        if (expiration != null) {
                            int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L)
                                    .intValue();
                            conn.keyCommands().expire(refreshToAccessKey, seconds);
                            conn.keyCommands().expire(accessToRefreshKey, seconds);
                        }
                    }
                }
                conn.closePipeline();
            } finally {
                conn.close();
            }
        }

        private static String getApprovalKey(OAuth2Authentication authentication) {
            String userName = authentication.getUserAuthentication() == null ? ""
                    : authentication.getUserAuthentication().getName();
            return getApprovalKey(authentication.getOAuth2Request().getClientId(), userName);
        }

        private static String getApprovalKey(String clientId, String userName) {
            return clientId + (userName == null ? "" : ":" + userName);
        }

        @Override
        public void removeAccessToken(OAuth2AccessToken accessToken) {
            removeAccessToken(accessToken.getValue());
        }

        @Override
        public OAuth2AccessToken readAccessToken(String tokenValue) {
            byte[] key = serializeKey(ACCESS + tokenValue);
            byte[] bytes = null;
            RedisConnection conn = getConnection();
            try {
                bytes = conn.stringCommands().get(key);
            } finally {
                conn.close();
            }
            OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
            return accessToken;
        }

        public void removeAccessToken(String tokenValue) {
            byte[] accessKey = serializeKey(ACCESS + tokenValue);
            byte[] authKey = serializeKey(AUTH + tokenValue);
            byte[] accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + tokenValue);
            RedisConnection conn = getConnection();
            try {
                conn.openPipeline();
                conn.stringCommands().get(accessKey);
                conn.stringCommands().get(authKey);
                conn.keyCommands().del(accessKey);
                conn.keyCommands().del(accessToRefreshKey);
                // Don't remove the refresh token - it's up to the caller to do that
                conn.keyCommands().del(authKey);
                List<Object> results = conn.closePipeline();
                byte[] access = (byte[]) results.get(0);
                byte[] auth = (byte[]) results.get(1);

                OAuth2Authentication authentication = deserializeAuthentication(auth);
                if (authentication != null) {
                    String key = authenticationKeyGenerator.extractKey(authentication);
                    byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + key);
                    byte[] unameKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication));
                    byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId());
                    conn.openPipeline();
                    conn.keyCommands().del(authToAccessKey);
                    conn.listCommands().lRem(unameKey, 1, access);
                    conn.listCommands().lRem(clientId, 1, access);
                    conn.keyCommands().del(serialize(ACCESS + key));
                    conn.closePipeline();
                }
            } finally {
                conn.close();
            }
        }

        @Override
        public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {
            byte[] refreshKey = serializeKey(REFRESH + refreshToken.getValue());
            byte[] refreshAuthKey = serializeKey(REFRESH_AUTH + refreshToken.getValue());
            byte[] serializedRefreshToken = serialize(refreshToken);
            RedisConnection conn = getConnection();
            try {
                conn.openPipeline();
                conn.stringCommands().set(refreshKey, serializedRefreshToken);
                conn.stringCommands().set(refreshAuthKey, serialize(authentication));
                if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
                    ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken;
                    Date expiration = expiringRefreshToken.getExpiration();
                    if (expiration != null) {
                        int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L)
                                .intValue();
                        conn.keyCommands().expire(refreshKey, seconds);
                        conn.keyCommands().expire(refreshAuthKey, seconds);
                    }
                }
                conn.closePipeline();
            } finally {
                conn.close();
            }
        }

        @Override
        public OAuth2RefreshToken readRefreshToken(String tokenValue) {
            byte[] key = serializeKey(REFRESH + tokenValue);
            byte[] bytes = null;
            RedisConnection conn = getConnection();
            try {
                bytes = conn.stringCommands().get(key);
            } finally {
                conn.close();
            }
            OAuth2RefreshToken refreshToken = deserializeRefreshToken(bytes);
            return refreshToken;
        }

        @Override
        public void removeRefreshToken(OAuth2RefreshToken refreshToken) {
            removeRefreshToken(refreshToken.getValue());
        }

        public void removeRefreshToken(String tokenValue) {
            byte[] refreshKey = serializeKey(REFRESH + tokenValue);
            byte[] refreshAuthKey = serializeKey(REFRESH_AUTH + tokenValue);
            byte[] refresh2AccessKey = serializeKey(REFRESH_TO_ACCESS + tokenValue);
            byte[] access2RefreshKey = serializeKey(ACCESS_TO_REFRESH + tokenValue);
            RedisConnection conn = getConnection();
            try {
                conn.openPipeline();
                conn.keyCommands().del(refreshKey);
                conn.keyCommands().del(refreshAuthKey);
                conn.keyCommands().del(refresh2AccessKey);
                conn.keyCommands().del(access2RefreshKey);
                conn.closePipeline();
            } finally {
                conn.close();
            }
        }

        @Override
        public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {
            removeAccessTokenUsingRefreshToken(refreshToken.getValue());
        }

        private void removeAccessTokenUsingRefreshToken(String refreshToken) {
            byte[] key = serializeKey(REFRESH_TO_ACCESS + refreshToken);
            List<Object> results = null;
            RedisConnection conn = getConnection();
            try {
                conn.openPipeline();
                conn.stringCommands().get(key);
                conn.keyCommands().del(key);
                results = conn.closePipeline();
            } finally {
                conn.close();
            }
            if (results == null) {
                return;
            }
            byte[] bytes = (byte[]) results.get(0);
            String accessToken = deserializeString(bytes);
            if (accessToken != null) {
                removeAccessToken(accessToken);
            }
        }

        @Override
        public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) {
            byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(clientId, userName));
            List<byte[]> byteList = null;
            RedisConnection conn = getConnection();
            try {
                byteList = conn.listCommands().lRange(approvalKey, 0, -1);
            } finally {
                conn.close();
            }
            if (byteList == null || byteList.size() == 0) {
                return Collections.<OAuth2AccessToken> emptySet();
            }
            List<OAuth2AccessToken> accessTokens = new ArrayList<OAuth2AccessToken>(byteList.size());
            for (byte[] bytes : byteList) {
                OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
                accessTokens.add(accessToken);
            }
            return Collections.<OAuth2AccessToken> unmodifiableCollection(accessTokens);
        }

        @Override
        public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) {
            byte[] key = serializeKey(CLIENT_ID_TO_ACCESS + clientId);
            List<byte[]> byteList = null;
            RedisConnection conn = getConnection();
            try {
                byteList = conn.listCommands().lRange(key, 0, -1);
            } finally {
                conn.close();
            }
            if (byteList == null || byteList.size() == 0) {
                return Collections.<OAuth2AccessToken> emptySet();
            }
            List<OAuth2AccessToken> accessTokens = new ArrayList<OAuth2AccessToken>(byteList.size());
            for (byte[] bytes : byteList) {
                OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
                accessTokens.add(accessToken);
            }
            return Collections.<OAuth2AccessToken> unmodifiableCollection(accessTokens);
        }

    }

使用postman

springboot2.0.X使用spring security oauth2

springboot2.0.X使用spring security oauth2

结果如下:

springboot2.0.X使用spring security oauth2

springboot2.0.X使用spring security oauth2