1. 程式人生 > 其它 >SpringCloud微服務間安全呼叫實現

SpringCloud微服務間安全呼叫實現

如果對你有用,請記得點贊哦!!!!

SpringCloud服務間安全呼叫實現

目前專案中用到了微服務認證這塊的技術,查看了相當一大部分的資料發現現在網上的資料都很不全,很零散。而且很多都執行不起來。

簡單的介紹一下認證。
傳統的專案都是使用session來管理使用者的登入資訊,返回前端sessionId儲存在cookie中。但是在分散式情況下利用session去管理的場景就很少了,也不是不可以,如果你對session很熱愛使用它的話,那就沒必要繼續往下看了。

在我們微服務restful風格下,應該怎麼去保證安全資訊呢。相信大家可能瞭解過token,也就是我們熟說的令牌,有人問我什麼是token,Token 和 Session ID 不同,並非只是一個 key。Token 一般會包含使用者的相關資訊,通過驗證 Token 就可以完成身份校驗。

這裡我們先簡單介紹一下我們使用的token格式。

採用jwt(JSON WEB TOKEN)
JWT 是由三段資訊構成的,第一段為頭部(Header),第二段為載荷(Payload),第三段為簽名(Signature)。每一段內容都是一個 JSON 物件,將每一段 JSON 物件採用 BASE64 編碼,將編碼後的內容用. 連結一起就構成了 JWT 字串。

認證協議採用Oauth2.0
OAuth 是一種開放的協議,為桌面程式或者基於 BS 的 web 應用提供了一種簡單的,標準的方式去訪問需要使用者授權的 API 服務。OAUTH 認證授權具有以下特點:

簡單:不管是 OAuth 服務提供者還是應用開發者,都很容易於理解與使用;

安全:沒有涉及到使用者金鑰等資訊,更安全更靈活;

開放:任何服務提供商都可以實現 OAuth,任何軟體開發商都可以使用 OAuth;

關於jwt和Oauth我們就不做過多的介紹了。大家直接去百度一大堆,這種內容寫出來就沒有技術含量了。

思考一個問題:就是在分散式情況下,我們怎麼去生成這個token?在哪裡生成是最好的呢?

現在我們舉例說明,有三個服務,會員,訂單,支付,這個三個服務,他們都需要使用者驗證,我們不可能在每個系統中去寫一個認證過程,所以,我們的認證是一個單獨的服務。

認證流程:使用者提供使用者資訊,到認證中心驗證,成功就返回一個token,訪問其他服務的時候在請求頭攜帶上token資訊,服務只需要去解析token的值就可以了,這樣做既滿足了微服務輕量級的需求,也避免了了瀏覽器禁用cookie的情況,豈不美哉。

我們要知道一個事情,對於我們的認證伺服器來說,只有兩種服務,那就是認證伺服器和資源伺服器,所有我們這裡的會員,訂單,支付服務,對於認證伺服器來說都是資源伺服器。

搭建認證服務

我們採用SpringSecurity Oauth2.0 JWT Redis搭建服務

採用SpringSecurity的密碼模式,其他模式請自行百度參考,這裡提供當前認證的最需求的做法。

pom.xml

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.2.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <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.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.40</version>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Dalston.RC1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.cdhenren.AuthApplication</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

配置檔案application.yml

server:
  port: 8080
spring:
  redis:
    host: 127.0.0.1
    password: null
    port: 6379
    pool:
      max-idle: 100
      min-idle: 1
      max-active: 1000
      max-wait: -1
logging:
  level: 
    per.lx: DEBUG

配置認證服務

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
    // 資源ID
    private static final String SOURCE_ID = "order";
    private static final int ACCESS_TOKEN_TIMER = 60 * 60 * 24;
    private static final int REFRESH_TOKEN_TIMER = 60 * 60 * 24 * 30;

    @Autowired
    AuthenticationManager authenticationManager;
    @Autowired
    RedisConnectionFactory redisConnectionFactory;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory().withClient("myapp").resourceIds(SOURCE_ID).authorizedGrantTypes("password", "refresh_token")
                .scopes("all").authorities("ADMIN").secret("lxapp").accessTokenValiditySeconds(ACCESS_TOKEN_TIMER)
                .refreshTokenValiditySeconds(REFRESH_TOKEN_TIMER);
    }

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

    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        // 允許表單認證
        oauthServer.allowFormAuthenticationForClients();
    }

    // JWT
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter() {
            /***
             * 重寫增強token方法,用於自定義一些token總需要封裝的資訊
             */
            @Override
            public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
                String userName = authentication.getUserAuthentication().getName();
                // 得到使用者名稱,去處理資料庫可以拿到當前使用者的資訊和角色資訊(需要傳遞到服務中用到的資訊)
                final Map<String, Object> additionalInformation = new HashMap<>();
                // Map假裝使用者實體
                Map<String, String> userinfo = new HashMap<>();
                userinfo.put("id", "1");
                userinfo.put("username", "LiaoXiang");
                userinfo.put("qqnum", "438944209");
                userinfo.put("userFlag", "1");
                additionalInformation.put("userinfo", JSON.toJSONString(userinfo));
                ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
                OAuth2AccessToken enhancedToken = super.enhance(accessToken, authentication);
                return enhancedToken;
            }
        };
        // 測試用,資源服務使用相同的字元達到一個對稱加密的效果,生產時候使用RSA非對稱加密方式
        accessTokenConverter.setSigningKey("SigningKey");
        return accessTokenConverter;
    }

    @Bean
    public TokenStore tokenStore() {
        RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
        return tokenStore;
    }

}

安全認證配置

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    // 請配置這個,以保證在重新整理Token時能成功重新整理
    @Autowired
    public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
        // 配置使用者來源於資料庫
        auth.userDetailsService(userDetailsService());
    }

    @Bean
    @Override
    protected UserDetailsService userDetailsService() {

        // 這裡是新增兩個使用者到記憶體中去,實際中是從#下面去通過資料庫判斷使用者是否存在
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("user_1").password("123456").authorities("USER").build());
        manager.createUser(User.withUsername("user_2").password("123456").authorities("USER").build());
        return manager;

        // #####################實際開發中在下面寫從資料庫獲取資料###############################
        // return new UserDetailsService() {
        // @Override
        // public UserDetails loadUserByUsername(String username) throws
        // UsernameNotFoundException {
        // // 通過使用者名稱獲取使用者資訊
        // boolean isUserExist = false;
        // if (isUserExist) {
        // //建立spring security安全使用者和對應的許可權(從資料庫查詢)
        // User user = new User("username", "password",
        // AuthorityUtils.createAuthorityList("admin", "manager"));
        // return user;
        // } else {
        // throw new UsernameNotFoundException("使用者[" + username + "]不存在");
        // }
        // }
        // };

    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        http.requestMatchers().anyRequest().and().authorizeRequests().antMatchers("/oauth/**").permitAll();
        // @formatter:on
    }
}

到這裡一個認證伺服器就搭建成功了。

我們寫一個啟動類來啟動

@SpringBootApplication
public class AuthApp {
    public static void main(String[] args) {
        SpringApplication.run(AuthApp.class, args);
    }
}

啟動後發現控制檯輸出了我們的請求連線(表明整合成功)

然後我們用postman進行請求模擬

請求引數中的資訊來自於配置檔案,不同的話不能生成成功。使用者資訊請自行修改程式碼從資料庫獲取

可以看到我們的返回資訊中包含的資訊,其中有固定的資訊和我們自定義的資訊。

順便看看我們的Redis中儲存的認證資訊,可以看到redis新增了很多資訊

到這裡我們的認證服務就搭建成功了,就可以用來訪問其他的服務了
#######################################################################

我們這裡來搭建資源伺服器,也就是通過TOKEN去訪問的服務

pom.xml

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.2.RELEASE</version>
        <relativePath />
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <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.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.40</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.7.0</version>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Dalston.RC1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.cdhenren.AuthApplication</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

新建一個資源服務配置檔案ResourceConfiguration.java
我們這裡配置以/order/*開頭的請求不用認證

@Configuration
@EnableResourceServer
public class ResourceConfiguration extends ResourceServerConfigurerAdapter {

    private static final String SOURCE_ID = "order";

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Override
    @CrossOrigin
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(SOURCE_ID).stateless(true);
        resources.tokenServices(defaultTokenServices());
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        // @formatter:off
                // 我們這裡放開/order/*的請求,以/order/*開頭的請求不用認證
        http.authorizeRequests().antMatchers("/order/*").permitAll().and().authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS).permitAll().anyRequest().authenticated();
        // @formatter:on
    }

    // 自定義的Token儲存器,存到Redis中
    @Bean
    public TokenStore tokenStore() {
        RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
        return tokenStore;
    }

    // Token轉換器
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter() {
        };
        accessTokenConverter.setSigningKey("SigningKey");
        return accessTokenConverter;
    }

    /**
     * 建立一個預設的資源服務token
     */
    @Bean
    public ResourceServerTokenServices defaultTokenServices() {
        final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        // 使用自定義的Token轉換器
        defaultTokenServices.setTokenEnhancer(accessTokenConverter());
        // 使用自定義的tokenStore
        defaultTokenServices.setTokenStore(tokenStore());
        return defaultTokenServices;
    }
}

然後我們寫一個工具類,去獲取二手手遊賣號平臺地圖我們在認證端傳過來的userinfo

public class AuthUtils {
    public static String getReqUser(HttpServletRequest req) {
        String header = req.getHeader("Authorization");
        String token = StringUtils.substringAfter(header, "bearer");
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey("SigningKey".getBytes("UTF-8")).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            return null;
        }
        String localUser = (String) claims.get("userinfo");
        // 拿到當前使用者
        return localUser;
    }
}

到這裡我們就配置完了我們的所有請求,我們編寫一個Controller進行驗證。

@RestController
public class TestEndpoints {
    @GetMapping("/product/{id}")
    public String getProduct(@PathVariable String id, HttpServletRequest req) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        System.out.println("使用者名稱  : " + JSON.toJSONString(authentication.getPrincipal()));
        System.out.println("封裝的傳遞資訊  : " + AuthUtils.getReqUser(req));
        return "(Need Auth Request)product id : " + id;
    }

    @GetMapping("/order/{id}")
    public String getOrder(@PathVariable String id) {
        return "(No Auth Request)order id : " + id;
    }
}

這下我們就測試完成了
首先訪問我們不需要認證的請求:127.0.0.1:8081/order/1,可以正常訪問返回資料

然後不帶我們的token進行訪問另外一個連結(需要認證):127.0.0.1:8081/product/1

可以看到返回沒有認證不能訪問的提示,如下圖

下面我們在請求頭裡面帶上我們的token,這裡注意一下,token是攜帶在Header中的Authorization屬性中,而且我們需要用token型別+token的方式進行傳遞,這裡我們的型別預設是bearer,加上我們的請求頭就可以正常訪問到資料了。如下圖

現在,我們可以直接拿去整合到我們真實的微服務專案中去了,保證我們的專案從這一篇部落格開始。

如果對你有用,請記得點贊。