微服務之間的通訊安全(二)-使用JWT優化認證機制


1、使用JWT來解決認證中存在的問題

  之前說認證中存在的問題是效率低,每次都要取認證服務器進行校驗;不安全,傳遞用戶信息是放到請求頭中的明文。這兩個問題的解決方案就是JWT。JWT官網掃盲連接https://jwt.io/introduction/

  因為我們之前發出去的令牌都是一些無意義的串,而JWT中可以包含一些用戶信息,這樣前端發請求過來,網關就不需要去認證服務器校驗了,我們只需要校驗這個JWT是否被串改,並且從里面將用戶信息讀出來就可以了,往下轉發傳遞和服務與服務之間進行調用時,只需要傳遞JWT就可以了。並且Spring給我們提供了工具,不用我們自己寫代碼就可以完成。我們要將架構改成下圖:

2、認證服務器改造,使其發送JWT令牌

2.1、將之前API安全-https中使用keytool生成的證書copy到resources下

2.2、OAuth2認證服務器配置類,將tokenStore設置為JwtTokenStore,並對暴露獲取令牌簽名的驗證密鑰

/**
 * OAuth2認證服務器配置類
 * 需要繼承AuthorizationServerConfigurerAdapter類,覆蓋里面三個configure方法
 * 並添加@EnableAuthorizationServer注解,指定當前應用做為認證服務器
 *
 * @author caofanqi
 * @date 2020/1/31 18:04
 */
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {


    @Resource
    private AuthenticationManager authenticationManager;

    @Resource
    private DataSource dataSource;

    @Resource
    private UserDetailsService userDetailsService;

    /**
     * 配置授權服務器的安全性
     * checkTokenAccess:驗證令牌需要什么條件,isAuthenticated():需要經過身份認證。
     * 此處的passwordEncoders是為client secrets配置的。
     * tokenKeyAccess:設置對獲取令牌簽名的驗證密鑰需要通過身份認證
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .checkTokenAccess("isAuthenticated()")
                .passwordEncoder(new BCryptPasswordEncoder())
                .tokenKeyAccess("isAuthenticated()");
    }


    /**
     * 配置客戶端服務
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //從數據庫中讀取
        clients.jdbc(dataSource);
    }

    /**
     * 配置授權服務器終端的非安全特征
     * authenticationManager 校驗用戶信息是否合法
     * tokenStore:token存儲
     * userDetailsService:配合刷新令牌使用
     * tokenEnhancer:令牌增強器
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager)
//                .tokenStore(new JdbcTokenStore(dataSource))
                .tokenStore(new JwtTokenStore(jwtTokenEnhancer()))
                .tokenEnhancer(jwtTokenEnhancer())
                .userDetailsService(userDetailsService);
    }


    /**
     *  jwt令牌增強器,使用KeyPair提高安全度。
     *  聲明為spring bean是為了讓資源服務器可以獲取令牌簽名的驗證密鑰 ,TokenKeyEndpoint類中的 /oauth/token_key
     */
    @Bean
    public JwtAccessTokenConverter jwtTokenEnhancer() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        //jwtAccessTokenConverter.setSigningKey("123456");
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("cfq.key"), "123456".toCharArray());
        jwtAccessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("cfq"));
        return jwtAccessTokenConverter;
    }

}

2.3、啟動資源服務器獲取令牌

  可以發現,我們現在獲取到的令牌比以前長了,我們將他復制到jwt官網,可以看到如下,解析后JWT的PAYLOAD中存放這一些數據,aud:該令牌可以訪問的資源服務器,user_name:申請令牌的用戶,scope:令牌的scope,exp:令牌的過期時間,authorities:申請令牌用戶的角色信息,client_id:申請令牌的客戶端id,jti:相當於該令牌的id。當然,我們也可以在這里面加入一些信息,但是不建議,因為jwt只是防篡改,任何人都能看到里面的數據,往里面加入一些業務信息,有可能導致信息泄漏。

3、網關和資源服務器改造

3.1、在網關上對JWT進行認證,不再向認證服務器發請求認證

  3.1.1、將之前的一系列過濾器刪掉,因為除了流控和審計,剩下的SpringSecurity和SpringSecurityOauth都為我們提供了,我們直接用就好了

  3.1.2、引入oauth2依賴

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

  3.1.3、application.yml配置獲取令牌簽名的驗證密鑰地址,因為認證服務器設置了需要認證,我們還要配上client-id和client-secret

server:
  port: 9010

zuul:
  routes:
    token:
      url: http://auth.caofanqi.cn:9020
      path: /token/**
    order:
      url: http://order.caofanqi.cn:9080
      path: /order/**
  sensitive-headers:

security:
  oauth2:
    client:
      client-id: gateway
      client-secret: 123456
    resource:
      jwt:
        key-uri: http://auth.caofanqi.cn:9020/oauth/token_key

  3.1.4、網關資源服務器配置,放過申請令牌請求

/**
 * 網關資源服務器配置
 *
 * @author caofanqi
 * @date 2020/2/8 22:30
 */
@Configuration
@EnableResourceServer
public class GatewayResourceServerConfig extends ResourceServerConfigurerAdapter {


    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("gateway");
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                //放過申請令牌的請求不需要身份認證
                .antMatchers("/token/**").permitAll()
                .anyRequest().authenticated();
    }

}

  3.1.5、Order和Price資源服務器配置,也是需要引入oauth2依賴,配置獲取令牌簽名的驗證密鑰地址,client-id和client-secret,但是調用服務的請求需要由RestTemplate替換為OAuth2RestTemplate,這樣就會將在我們調用別的服務時,將jwt一並傳遞過去。獲取用戶信息,通過@AuthenticationPrincipal注解進行獲取。

/**
 * 訂單微服務
 *
 * @author caofanqi
 * @date 2020/1/31 14:22
 */
@EnableResourceServer
@SpringBootApplication
public class OrderApiApplication {


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

    /**
     *  將OAuth2RestTemplate聲明為spring bean,OAuth2ProtectedResourceDetails,OAuth2ClientContext springboot會自動幫我們注入
     */
    @Bean
    public OAuth2RestTemplate oAuth2RestTemplate(OAuth2ProtectedResourceDetails resource, OAuth2ClientContext context){
        return new OAuth2RestTemplate(resource,context);
    }

}
/**
 * 訂單控制層
 *
 * @author caofanqi
 * @date 2020/1/31 14:26
 */
@Slf4j
@RestController
@RequestMapping("/orders")
public class OrderController {

    @Resource
    private OAuth2RestTemplate oAuth2RestTemplate;

    @PostMapping
    public OrderDTO create(@RequestBody OrderDTO orderDTO, @AuthenticationPrincipal String username) {
        log.info("username is :{}", username);
        PriceDTO price = oAuth2RestTemplate.getForObject("http://127.0.0.1:9070/prices/" + orderDTO.getProductId(), PriceDTO.class);
        log.info("price is : {}", price.getPrice());
        return orderDTO;
    }


    @GetMapping("/{id}")
    public OrderDTO get(@PathVariable Long id, @AuthenticationPrincipal String username) {
        log.info("username is :{}", username);
        OrderDTO orderDTO = new OrderDTO();
        orderDTO.setId(id);
        orderDTO.setProductId(5 * id);
        return orderDTO;
    }

}

  3.1.6、測試,需要先啟動認證服務器,因為各資源服務器需要在啟動時獲取令牌簽名的驗證密鑰。

  獲取令牌,通過網關創建訂單,報錯403,是因為我們通過webApp申請的令牌可以訪問的資源服務器沒有添加gateway,

我們可以在resource_ids添加上gateway,也可以什么都不填,這樣發出去的令牌就可以訪問所有的資源服務器了。

我們這里,什么都不填寫,然后重新申請令牌,再次通過網關創建訂單,可以正常創建,並且在訂單服務和價格服務中可以獲取到username

  

  我們傳一個錯誤的令牌或者不傳令牌進行訪問,會返回401,這說明我們之前寫的邏輯SpringSecurity和SpringSecurityOauth都已經幫我們實現好了。

 

 

 

 

 項目源碼:https://github.com/caofanqi/study-security/tree/dev-jwt-authentication

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM