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.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