<p id="m2nkj"><option id="m2nkj"><big id="m2nkj"></big></option></p>
    <strong id="m2nkj"></strong>
    <ruby id="m2nkj"></ruby>

    <var id="m2nkj"></var>
  • SpringCloud+SpringBoot+OAuth2+Spring Security+Redis微服務(wù)統(tǒng)一認證授權(quán)

    共 80740字,需瀏覽 162分鐘

     ·

    2021-09-30 07:45

    點擊上方 Java學習之道,選擇 設(shè)為星標

    每天18:30點,干貨準時奉上!

    來源: blog.csdn.net/WYA1993/article/details/85050120
    作者: myCat、

    Part1

    因為目前做了一個基于Spring Cloud的微服務(wù)項目,所以了解到了OAuth2,打算整合一下OAuth2來實現(xiàn)統(tǒng)一授權(quán)。關(guān)于OAuth是一個關(guān)于授權(quán)的開放網(wǎng)絡(luò)標準,目前的版本是2.0,這里我就不多做介紹了。

    Part2開發(fā)環(huán)境

    • Windows10
    • Intellij Idea2018.2
    • jdk1.8
    • redis3.2.9
    • Spring Boot 2.0.2 Release
    • Spring Cloud Finchley.RC2
    • Spring 5.0.6

    Part3項目目錄

    • eshop                                      —— 父級工程,管理jar包版本
    • eshop-server                 —— Eureka服務(wù)注冊中心
    • eshop-gateway              —— Zuul網(wǎng)關(guān)
    • eshop-auth                    —— 授權(quán)服務(wù)
    • eshop-member              —— 會員服務(wù)
    • eshop-email                   —— 郵件服務(wù)(暫未使用)
    • eshop-common              —— 通用類 關(guān)于如何構(gòu)建一個基本的Spring Cloud 微服務(wù)這里就不贅述了

    Part4授權(quán)服務(wù)

    首先構(gòu)建eshop-auth服務(wù),引入相關(guān)依賴


    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

        <parent>
            <artifactId>eshop-parent</artifactId>
            <groupId>com.curise.eshop</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
        <artifactId>eshop-auth</artifactId>
        <packaging>war</packaging>
        <description>授權(quán)模塊</description>
     
        <dependencies>
            <dependency>
                <groupId>com.curise.eshop</groupId>
                <artifactId>eshop-common</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-oauth2</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-security</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
            </dependency>
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
            </dependency>
        </dependencies>
     
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </project>

    接下來,配置Mybatis、redis、eureka,貼一下配置文件


    server:
      port: 1203
     
    spring:
      application:
        name: eshop-auth
      redis:
        database: 0
        host: 192.168.0.117
        port: 6379
        password:
        jedis:
          pool:
            max-active: 8
            max-idle: 8
            min-idle: 0
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/eshop_member?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
        username: root
        password: root
      druid:
        initialSize: 5 #初始化連接大小
        minIdle: 5     #最小連接池數(shù)量
        maxActive: 20  #最大連接池數(shù)量
        maxWait: 60000 #獲取連接時最大等待時間,單位毫秒
        timeBetweenEvictionRunsMillis: 60000 #配置間隔多久才進行一次檢測,檢測需要關(guān)閉的空閑連接,單位是毫秒
        minEvictableIdleTimeMillis: 300000   #配置一個連接在池中最小生存的時間,單位是毫秒
        validationQuery: SELECT 1 from DUAL  #測試連接
        testWhileIdle: true                  #申請連接的時候檢測,建議配置為true,不影響性能,并且保證安全性
        testOnBorrow: false                  #獲取連接時執(zhí)行檢測,建議關(guān)閉,影響性能
        testOnReturn: false                  #歸還連接時執(zhí)行檢測,建議關(guān)閉,影響性能
        poolPreparedStatements: false        #是否開啟PSCache,PSCache對支持游標的數(shù)據(jù)庫性能提升巨大,oracle建議開啟,mysql下建議關(guān)閉
        maxPoolPreparedStatementPerConnectionSize: 20 #開啟poolPreparedStatements后生效
        filters: stat,wall,log4j #配置擴展插件,常用的插件有=>stat:監(jiān)控統(tǒng)計  log4j:日志  wall:防御sql注入
        connectionProperties: 'druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000' #通過connectProperties屬性來打開mergeSql功能;慢SQL記錄
     
     
    eureka:
      instance:
        prefer-ip-address: true
        instance-id: ${spring.cloud.client.ip-address}:${server.port}
      client:
        service-url:
          defaultZone: http://localhost:1111/eureka/
     
    mybatis:
      type-aliases-package: com.curise.eshop.common.entity
      configuration:
        map-underscore-to-camel-case: true  #開啟駝峰命名,l_name -> lName
        jdbc-type-for-null: NULL
        lazy-loading-enabled: true
        aggressive-lazy-loading: true
        cache-enabled: true #開啟二級緩存
        call-setters-on-nulls: true #map空列不顯示問題
      mapper-locations:
        - classpath:mybatis/*.xml

    AuthApplication添加@EnableDiscoveryClient@MapperScan注解。

    接下來配置認證服務(wù)器AuthorizationServerConfig ,并添加@Configuration@EnableAuthorizationServer注解,其中ClientDetailsServiceConfigurer配置在內(nèi)存中,當然也可以從數(shù)據(jù)庫讀取,以后慢慢完善。

    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
     
        @Autowired
        private AuthenticationManager authenticationManager;
     
        @Autowired
        private DataSource dataSource;
     
        @Autowired
        private RedisConnectionFactory redisConnectionFactory;
     
        @Autowired
        private MyUserDetailService userDetailService;
     
        @Bean
        public TokenStore tokenStore() {
            return new RedisTokenStore(redisConnectionFactory);
        }
     
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            security
                    .allowFormAuthenticationForClients()
                    .tokenKeyAccess("permitAll()")
                    .checkTokenAccess("isAuthenticated()");
        }
     
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
           // clients.withClientDetails(clientDetails());
            clients.inMemory()
                    .withClient("android")
                    .scopes("read")
                    .secret("android")
                    .authorizedGrantTypes("password""authorization_code""refresh_token")
                    .and()
                    .withClient("webapp")
                    .scopes("read")
                    .authorizedGrantTypes("implicit")
                    .and()
                    .withClient("browser")
                    .authorizedGrantTypes("refresh_token""password")
                    .scopes("read");
        }
        @Bean
        public ClientDetailsService clientDetails() {
            return new JdbcClientDetailsService(dataSource);
        }
     
        @Bean
        public WebResponseExceptionTranslator webResponseExceptionTranslator(){
            return new MssWebResponseExceptionTranslator();
        }
     
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.tokenStore(tokenStore())
                    .userDetailsService(userDetailService)
                    .authenticationManager(authenticationManager);
            endpoints.tokenServices(defaultTokenServices());
            //認證異常翻譯
           // endpoints.exceptionTranslator(webResponseExceptionTranslator());
        }
     
        /**
         * <p>注意,自定義TokenServices的時候,需要設(shè)置@Primary,否則報錯,</p>
         * @return
         */

        @Primary
        @Bean
        public DefaultTokenServices defaultTokenServices(){
            DefaultTokenServices tokenServices = new DefaultTokenServices();
            tokenServices.setTokenStore(tokenStore());
            tokenServices.setSupportRefreshToken(true);
            //tokenServices.setClientDetailsService(clientDetails());
            // token有效期自定義設(shè)置,默認12小時
            tokenServices.setAccessTokenValiditySeconds(60*60*12);
            // refresh_token默認30天
            tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7);
            return tokenServices;
        }
    }

    在上述配置中,認證的token是存到redis里的,如果你這里使用了Spring5.0以上的版本的話,使用默認的RedisTokenStore認證時會報如下異常:

    nested exception is java.lang.NoSuchMethodError: org.springframework.data.redis.connection.RedisConnection.set([B[B)V

    原因是spring-data-redis 2.0版本中set(String,String)被棄用了,要使用RedisConnection.stringCommands().set(…),所有我自定義一個RedisTokenStore,代碼和RedisTokenStore一樣,只是把所有conn.set(…)都換成conn..stringCommands().set(…),測試后方法可行。

    public class RedisTokenStore 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 RedisTokenStore(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 this.connectionFactory.getConnection();
        }
     
        private byte[] serialize(Object object) {
            return this.serializationStrategy.serialize(object);
        }
     
        private byte[] serializeKey(String object) {
            return this.serialize(this.prefix + object);
        }
     
        private OAuth2AccessToken deserializeAccessToken(byte[] bytes) {
            return (OAuth2AccessToken)this.serializationStrategy.deserialize(bytes, OAuth2AccessToken.class);
        }
     
        private OAuth2Authentication deserializeAuthentication(byte[] bytes) {
            return (OAuth2Authentication)this.serializationStrategy.deserialize(bytes, OAuth2Authentication.class);
        }
     
        private OAuth2RefreshToken deserializeRefreshToken(byte[] bytes) {
            return (OAuth2RefreshToken)this.serializationStrategy.deserialize(bytes, OAuth2RefreshToken.class);
        }
     
        private byte[] serialize(String string) {
            return this.serializationStrategy.serialize(string);
        }
     
        private String deserializeString(byte[] bytes) {
            return this.serializationStrategy.deserializeString(bytes);
        }
     
        @Override
        public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
            String key = this.authenticationKeyGenerator.extractKey(authentication);
            byte[] serializedKey = this.serializeKey(AUTH_TO_ACCESS + key);
            byte[] bytes = null;
            RedisConnection conn = this.getConnection();
            try {
                bytes = conn.get(serializedKey);
            } finally {
                conn.close();
            }
            OAuth2AccessToken accessToken = this.deserializeAccessToken(bytes);
            if (accessToken != null) {
                OAuth2Authentication storedAuthentication = this.readAuthentication(accessToken.getValue());
                if (storedAuthentication == null || !key.equals(this.authenticationKeyGenerator.extractKey(storedAuthentication))) {
                    this.storeAccessToken(accessToken, authentication);
                }
            }
            return accessToken;
        }
     
        @Override
        public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
            return this.readAuthentication(token.getValue());
        }
     
        @Override
        public OAuth2Authentication readAuthentication(String token) {
            byte[] bytes = null;
            RedisConnection conn = this.getConnection();
            try {
                bytes = conn.get(this.serializeKey("auth:" + token));
            } finally {
                conn.close();
            }
            OAuth2Authentication auth = this.deserializeAuthentication(bytes);
            return auth;
        }
     
        @Override
        public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {
            return this.readAuthenticationForRefreshToken(token.getValue());
        }
     
        public OAuth2Authentication readAuthenticationForRefreshToken(String token) {
            RedisConnection conn = getConnection();
            try {
                byte[] bytes = conn.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.rPush(approvalKey, serializedAccessToken);
                }
                conn.rPush(clientId, serializedAccessToken);
                if (token.getExpiration() != null) {
                    int seconds = token.getExpiresIn();
                    conn.expire(accessKey, seconds);
                    conn.expire(authKey, seconds);
                    conn.expire(authToAccessKey, seconds);
                    conn.expire(clientId, seconds);
                    conn.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.expire(refreshToAccessKey, seconds);
                            conn.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) {
            this.removeAccessToken(accessToken.getValue());
        }
     
        @Override
        public OAuth2AccessToken readAccessToken(String tokenValue) {
            byte[] key = serializeKey(ACCESS + tokenValue);
            byte[] bytes = null;
            RedisConnection conn = getConnection();
            try {
                bytes = conn.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.get(accessKey);
                conn.get(authKey);
                conn.del(accessKey);
                conn.del(accessToRefreshKey);
                // Don't remove the refresh token - it's up to the caller to do that
                conn.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.del(authToAccessKey);
                    conn.lRem(unameKey, 1, access);
                    conn.lRem(clientId, 1, access);
                    conn.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.expire(refreshKey, seconds);
                        conn.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.get(key);
            } finally {
                conn.close();
            }
            OAuth2RefreshToken refreshToken = deserializeRefreshToken(bytes);
            return refreshToken;
        }
     
        @Override
        public void removeRefreshToken(OAuth2RefreshToken refreshToken) {
            this.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.del(refreshKey);
                conn.del(refreshAuthKey);
                conn.del(refresh2AccessKey);
                conn.del(access2RefreshKey);
                conn.closePipeline();
            } finally {
                conn.close();
            }
        }
     
        @Override
        public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {
            this.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.get(key);
                conn.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.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.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);
        }
    }

    配置資源服務(wù)器

    @Configuration
    @EnableResourceServer
    @Order(3)
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()
                .exceptionHandling()
                .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
                .and()
                .requestMatchers().antMatchers("/api/**")
                .and()
                .authorizeRequests()
                .antMatchers("/api/**").authenticated()
                .and()
                .httpBasic();
        }
    }

    配置Spring Security

    @Configuration
    @EnableWebSecurity
    @Order(2)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private MyUserDetailService userDetailService;
     
        @Bean
        public PasswordEncoder passwordEncoder() {
            //return new BCryptPasswordEncoder();
            return new NoEncryptPasswordEncoder();
        }
     
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.requestMatchers().antMatchers("/oauth/**")
                    .and()
                    .authorizeRequests()
                    .antMatchers("/oauth/**").authenticated()
                    .and()
                    .csrf().disable();
        }
     
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
        }
     
        /**
         * 不定義沒有password grant_type
         *
         * @return
         * @throws Exception
         */

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

    可以看到ResourceServerConfig 是比SecurityConfig 的優(yōu)先級低的。

    二者的關(guān)系:

    • ResourceServerConfig 用于保護oauth相關(guān)的endpoints,同時主要作用于用戶的登錄(form login,Basic auth)
    • SecurityConfig 用于保護oauth要開放的資源,同時主要作用于client端以及token的認證(Bearer auth)

    所以我們讓SecurityConfig優(yōu)先于ResourceServerConfig,且在SecurityConfig 不攔截oauth要開放的資源,在ResourceServerConfig 中配置需要token驗證的資源,也就是我們對外提供的接口。所以這里對于所有微服務(wù)的接口定義有一個要求,就是全部以/api開頭。

    如果這里不這樣配置的話,在你拿到access_token去請求各個接口時會報 invalid_token 的提示。

    另外,由于我們自定義認證邏輯,所以需要重寫UserDetailService

    @Service("userDetailService")
    public class MyUserDetailService implements UserDetailsService {
     
        @Autowired
        private MemberDao memberDao;
     
        @Override
        public UserDetails loadUserByUsername(String memberName) throws UsernameNotFoundException {
            Member member = memberDao.findByMemberName(memberName);
            if (member == null) {
                throw new UsernameNotFoundException(memberName);
            }
            Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
            // 可用性 :true:可用 false:不可用
            boolean enabled = true;
            // 過期性 :true:沒過期 false:過期
            boolean accountNonExpired = true;
            // 有效性 :true:憑證有效 false:憑證無效
            boolean credentialsNonExpired = true;
            // 鎖定性 :true:未鎖定 false:已鎖定
            boolean accountNonLocked = true;
            for (Role role : member.getRoles()) {
                //角色必須是ROLE_開頭,可以在數(shù)據(jù)庫中設(shè)置
                GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getRoleName());
                grantedAuthorities.add(grantedAuthority);
                //獲取權(quán)限
                for (Permission permission : role.getPermissions()) {
                    GrantedAuthority authority = new SimpleGrantedAuthority(permission.getUri());
                    grantedAuthorities.add(authority);
                }
            }
            User user = new User(member.getMemberName(), member.getPassword(),
                    enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, grantedAuthorities);
            return user;
        }
     
    }

    密碼驗證為了方便我使用了不加密的方式,重寫了PasswordEncoder,實際開發(fā)還是建議使用BCryptPasswordEncoder。

    public class NoEncryptPasswordEncoder implements PasswordEncoder {
     
        @Override
        public String encode(CharSequence charSequence) {
            return (String) charSequence;
        }
     
        @Override
        public boolean matches(CharSequence charSequence, String s) {
            return s.equals((String) charSequence);
        }
    }

    另外,OAuth的密碼模式需要AuthenticationManager支持

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

    定義一個Controller,提供兩個接口,/api/member用來獲取當前用戶信息,/api/exit用來注銷當前用戶


    @RestController
    @RequestMapping("/api")
    public class MemberController {
     
        @Autowired
        private MyUserDetailService userDetailService;
     
        @Autowired
        private ConsumerTokenServices consumerTokenServices;
     
        @GetMapping("/member")
        public Principal user(Principal member) {
            return member;
        }
     
        @DeleteMapping(value = "/exit")
        public Result revokeToken(String access_token) {
            Result result = new Result();
            if (consumerTokenServices.revokeToken(access_token)) {
                result.setCode(ResultCode.SUCCESS.getCode());
                result.setMessage("注銷成功");
            } else {
                result.setCode(ResultCode.FAILED.getCode());
                result.setMessage("注銷失敗");
            }
            return result;
        }
    }

    Part5會員服務(wù)配置

    引入依賴

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

        <parent>
            <artifactId>eshop-parent</artifactId>
            <groupId>com.curise.eshop</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
        <artifactId>eshop-member</artifactId>
        <packaging>war</packaging>
        <description>會員模塊</description>
     
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-oauth2</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-security</artifactId>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
            </dependency>
        </dependencies>
     
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </project>

    配置資源服務(wù)器


    @Configuration
    @EnableResourceServer
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
     
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http
                    .csrf().disable()
                    .exceptionHandling()
                    .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
                    .and()
                    .requestMatchers().antMatchers("/api/**")
                    .and()
                    .authorizeRequests()
                    .antMatchers("/api/**").authenticated()
                    .and()
                    .httpBasic();
        }
    }

    配置文件配置

    spring:
      application:
        name: eshop-member
     
    server:
      port: 1201
     
    eureka:
      instance:
        prefer-ip-address: true
        instance-id: ${spring.cloud.client.ip-address}:${server.port}
      client:
        service-url:
          defaultZone: http://localhost:1111/eureka/
     
    security:
      oauth2:
        resource:
          id: eshop-member
          user-info-uri: http://localhost:1202/auth/api/member
          prefer-token-info: false

    MemberApplication主類配置

    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class MemberApplication {
     
        public static void main(String[] args) {
            SpringApplication.run(MemberApplication.class,args);
        }
    }

    提供對外接口

    @RestController
    @RequestMapping("/api")
    public class MemberController {
     
        @GetMapping("hello")
        @PreAuthorize("hasAnyAuthority('hello')")
        public String hello(){
            return "hello";
        }
     
        @GetMapping("current")
        public Principal user(Principal principal) {
            return principal;
        }
     
        @GetMapping("query")
        @PreAuthorize("hasAnyAuthority('query')")
        public String query() {
            return "具有query權(quán)限";
        }
    }

    Part6配置網(wǎng)關(guān)

    引入依賴

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

        <parent>
            <artifactId>eshop-parent</artifactId>
            <groupId>com.curise.eshop</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
        <packaging>jar</packaging>
        <artifactId>eshop-gateway</artifactId>
        <description>網(wǎng)關(guān)</description>
     
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-oauth2</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-security</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
        </dependencies>
     
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </project>

    配置文件


    server:
      port: 1202
     
    spring:
      application:
        name: eshop-gateway
     
    #--------------------eureka---------------------
    eureka:
      instance:
        prefer-ip-address: true
        instance-id: ${spring.cloud.client.ip-address}:${server.port}
      client:
        service-url:
          defaultZone: http://localhost:1111/eureka/
     
    #--------------------Zuul-----------------------
    zuul:
      routes:
        member:
          path: /member/**
          serviceId: eshop-member
          sensitiveHeaders: "*"
        auth:
          path: /auth/**
          serviceId: eshop-auth
          sensitiveHeaders: "*"
      retryable: false
      ignored-services: "*"
      ribbon:
        eager-load:
          enabled: true
      host:
        connect-timeout-millis: 3000
        socket-timeout-millis: 3000
      add-proxy-headers: true
    #---------------------OAuth2---------------------
    security:
      oauth2:
        client:
          access-token-uri: http://localhost:${server.port}/auth/oauth/token
          user-authorization-uri: http://localhost:${server.port}/auth/oauth/authorize
          client-id: web
        resource:
          user-info-uri:  http://localhost:${server.port}/auth/api/member
          prefer-token-info: false
    #----------------------超時配置-------------------
    ribbon:
      ReadTimeout: 3000
      ConnectTimeout: 3000
      MaxAutoRetries: 1
      MaxAutoRetriesNextServer: 2
      eureka:
        enabled: true
    hystrix:
      command:
        default:
          execution:
            timeout:
              enabled: true
            isolation:
              thread:
                timeoutInMilliseconds: 3500

    ZuulApplication主類

    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableZuulProxy
    @EnableOAuth2Sso
    public class ZuulApplication {
        public static void main(String[] args) {
            SpringApplication.run(ZuulApplication.classargs);
        }
    }

    Spring Security配置

    @Configuration
    @EnableWebSecurity
    @Order(99)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable();
        }
    }

    接下來分別啟動eshop-server、eshop-member、eshop-auth、eshop-gateway。

    先發(fā)送一個請求測試一下未認證的效果

    獲取認證

    使用access_token請求auth服務(wù)下的用戶信息接口

    使用access_token請求member服務(wù)下的用戶信息接口

    請求member服務(wù)的query接口

    請求member服務(wù)的hello接口,數(shù)據(jù)庫里并沒有給用戶hello權(quán)限

    刷新token

    注銷

    Part7獲取認證時返回401

    獲取認證時返回401,如下:

    {
        "timestamp""2019-08-13T03:25:27.161+0000",
        "status"401,
        "error""Unauthorized",
        "message""Unauthorized",
        "path""/oauth/token"
    }

    原因是在發(fā)起請求的時候沒有添加Basic Auth認證,如下圖:添加Basic Auth認證后會在headers添加一個認證消息頭添加Basic Auth認證的信息在代碼中有體現(xiàn):

    Part8客戶端信息和token信息從MySQL數(shù)據(jù)庫中獲取

    現(xiàn)在客戶端信息都是存在內(nèi)存中的,生產(chǎn)環(huán)境肯定不可以這么做,要支持客戶端的動態(tài)添加或刪除,所以我選擇把客戶端信息存到MySQL中。

    首先,創(chuàng)建數(shù)據(jù)表,數(shù)據(jù)表的結(jié)構(gòu)官方已經(jīng)給出,地址在

    https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql

    其次,需要修改一下sql腳本,把主鍵的長度改為128,LONGVARBINARY類型改為blob,調(diào)整后的sql腳本:


    create table oauth_client_details (
      client_id VARCHAR(128) PRIMARY KEY,
      resource_ids VARCHAR(256),
      client_secret VARCHAR(256),
      scope VARCHAR(256),
      authorized_grant_types VARCHAR(256),
      web_server_redirect_uri VARCHAR(256),
      authorities VARCHAR(256),
      access_token_validity INTEGER,
      refresh_token_validity INTEGER,
      additional_information VARCHAR(4096),
      autoapprove VARCHAR(256)
    );
     
    create table oauth_client_token (
      token_id VARCHAR(256),
      token BLOB,
      authentication_id VARCHAR(128) PRIMARY KEY,
      user_name VARCHAR(256),
      client_id VARCHAR(256)
    );
     
    create table oauth_access_token (
      token_id VARCHAR(256),
      token BLOB,
      authentication_id VARCHAR(128) PRIMARY KEY,
      user_name VARCHAR(256),
      client_id VARCHAR(256),
      authentication BLOB,
      refresh_token VARCHAR(256)
    );
     
    create table oauth_refresh_token (
      token_id VARCHAR(256),
      token BLOB,
      authentication BLOB
    );
     
    create table oauth_code (
      code VARCHAR(256), authentication BLOB
    );
     
    create table oauth_approvals (
     userId VARCHAR(256),
     clientId VARCHAR(256),
     scope VARCHAR(256),
     status VARCHAR(10),
     expiresAt TIMESTAMP,
     lastModifiedAt TIMESTAMP
    );
     
     
    -- customized oauth_client_details table
    create table ClientDetails (
      appId VARCHAR(128) PRIMARY KEY,
      resourceIds VARCHAR(256),
      appSecret VARCHAR(256),
      scope VARCHAR(256),
      grantTypes VARCHAR(256),
      redirectUrl VARCHAR(256),
      authorities VARCHAR(256),
      access_token_validity INTEGER,
      refresh_token_validity INTEGER,
      additionalInformation VARCHAR(4096),
      autoApproveScopes VARCHAR(256)
    );

    調(diào)整后的sql腳步也放到了GitHub中,需要的可以自行下載然后在eshop_member數(shù)據(jù)庫創(chuàng)建數(shù)據(jù)表,將客戶端信息添加到oauth_client_details表中如果你的密碼不是明文,記得client_secret需要加密后存儲。

    然后修改代碼,配置從數(shù)據(jù)庫讀取客戶端信息接下來啟動服務(wù)測試即可。

    獲取授權(quán)

    獲取用戶信息

    刷新token

    打開數(shù)據(jù)表發(fā)現(xiàn)token這些信息并沒有存到表中,因為tokenStore使用的是redis方式,我們可以替換為從數(shù)據(jù)庫讀取。修改配

    重啟服務(wù)再次測試

    查看數(shù)據(jù)表,發(fā)現(xiàn)token數(shù)據(jù)已經(jīng)存到表里了。

    Part9源碼

    關(guān)于代碼和數(shù)據(jù)表sql已經(jīng)上傳到GitHub。

    注意把數(shù)據(jù)庫和redis替換成自己的地址

    地址:https://github.com/WYA1993/springcloud_oauth2.0

    -- END --

     | 更多精彩文章 -



    加我微信,交個朋友
    長按/掃碼添加↑↑↑

    瀏覽 55
    點贊
    評論
    收藏
    分享

    手機掃一掃分享

    分享
    舉報
    評論
    圖片
    表情
    推薦
    點贊
    評論
    收藏
    分享

    手機掃一掃分享

    分享
    舉報
    <p id="m2nkj"><option id="m2nkj"><big id="m2nkj"></big></option></p>
    <strong id="m2nkj"></strong>
    <ruby id="m2nkj"></ruby>

    <var id="m2nkj"></var>
  • 欧美操色 | 一区二区三区无码区 | 中文字幕在线免费观看视频 | 免费1级片骚逼 | 日韩AV中文 | 亚洲欧美日韩激情 | 欧美乱伦色| 欧美精品久久久久久久多人混战 | 影音先锋成人无码影院 | 亚洲第6页 |