學習目標
- 資源服務器授權配置
- 掌握OAuth認證微服務動態加載數據
- 掌握購物車流程
- 掌握購物車渲染流程
- OAuth2.0認證並獲取用戶令牌數據
- 微服務與微服務之間的認證
-
1 資源服務器授權配置
1.1 資源服務授權配置
基本上所有微服務都是資源服務
(1)配置公鑰 認證服務生成令牌採用非對稱加密算法,認證服務採用私鑰加密生成令牌,對外向資源服務提供公鑰,資源服務使 用公鑰 來校驗令牌的合法性。 將公鑰拷貝到 public.key文件中,將此文件拷貝到每一個需要的資源服務工程的classpath下 ,例如:用戶微服務.
(2)添加依賴
1 2 3 4
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency>
(3)配置每個系統的Http請求路徑安全控制策略以及讀取公鑰信息識別令牌,如下:
@Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize註解 public class ResourceServerConfig extends ResourceServerConfigurerAdapter { //公鑰 private static final String PUBLIC_KEY = "public.key"; /*** * 定義JwtTokenStore * @param jwtAccessTokenConverter * @return */ @Bean public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) { return new JwtTokenStore(jwtAccessTokenConverter); } /*** * 定義JJwtAccessTokenConverter * @return */ @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setVerifierKey(getPubKey()); return converter; } /** * 獲取非對稱加密公鑰 Key * @return 公鑰 Key */ private String getPubKey() { Resource resource = new ClassPathResource(PUBLIC_KEY); try { InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream()); BufferedReader br = new BufferedReader(inputStreamReader); return br.lines().collect(Collectors.joining("\n")); } catch (IOException ioe) { return null; } } /*** * Http安全配置,對每個到達系統的http請求鏈接進行校驗 * @param http * @throws Exception */ @Override public void configure(HttpSecurity http) throws Exception { //所有請求必須認證通過 http.authorizeRequests() //下邊的路徑放行 .antMatchers( "/user/add"). //配置地址放行 permitAll() .anyRequest(). authenticated(); //其他地址需要認證授權 } }
1.2 用戶微服務資源授權
將上面生成的公鑰public.key拷貝到changgou-service-user微服務工程的resources目錄下,如下圖:
(1)引入依賴
在changgou-service-user微服務工程pom.xml中引入oauth依賴
1 2 3 4 5
<!--oauth依賴--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency>
(2)資源授權配置
在changgou-service-user工程中創建com.changgou.user.config.ResourceServerConfig,代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
@Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize註解 public class ResourceServerConfig extends ResourceServerConfigurerAdapter { //公鑰 private static final String PUBLIC_KEY = "public.key"; /*** * 定義JwtTokenStore * @param jwtAccessTokenConverter * @return */ @Bean public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) { return new JwtTokenStore(jwtAccessTokenConverter); } /*** * 定義JJwtAccessTokenConverter * @return */ @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setVerifierKey(getPubKey()); return converter; } /** * 獲取非對稱加密公鑰 Key * @return 公鑰 Key */ private String getPubKey() { Resource resource = new ClassPathResource(PUBLIC_KEY); try { InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream()); BufferedReader br = new BufferedReader(inputStreamReader); return br.lines().collect(Collectors.joining("\n")); } catch (IOException ioe) { return null; } } /*** * Http安全配置,對每個到達系統的http請求鏈接進行校驗 * @param http * @throws Exception */ @Override public void configure(HttpSecurity http) throws Exception { //所有請求必須認證通過 http.authorizeRequests() //下邊的路徑放行 .antMatchers( "/user/add"). //配置地址放行 permitAll() .anyRequest(). authenticated(); //其他地址需要認證授權 } }
1.3 授權測試
用戶每次訪問微服務的時候,需要先申請令牌,令牌申請後,每次將令牌放到頭文件中,才能訪問微服務。
頭文件中每次需要添加一個Authorization
頭信息,頭的結果爲bearer token
。
(1)不攜帶令牌測試
訪問http://localhost:18089/user 不攜帶令牌,結果如下:
(2)攜帶正確令牌訪問
訪問http://localhost:18089/user 攜帶正確令牌,結果如下:
(3)攜帶錯誤令牌
訪問http://localhost:18089/user 攜帶不正確令牌,結果如下:
2 OAuth對接微服務
用戶每次訪問微服務的時候,先去oauth2.0服務登錄,登錄後再訪問微服務網關,微服務網關將請求轉發給其他微服務處理。
1 2 3 4
1.用戶登錄成功後,會將令牌信息存入到cookie中(一般建議存入到頭文件中) 2.用戶攜帶Cookie中的令牌訪問微服務網關 3.微服務網關先獲取頭文件中的令牌信息,如果Header中沒有Authorization令牌信息,則取參數中找,參數中如果沒有,則取Cookie中找Authorization,最後將令牌信息封裝到Header中,並調用其他微服務 4.其他微服務會獲取頭文件中的Authorization令牌信息,然後匹配令牌數據是否能使用公鑰解密,如果解密成功說明用戶已登錄,解密失敗,說明用戶未登錄
1.1 令牌加入到Header中
修改changgou-gateway-web的全局過濾器com.changgou.filter.AuthorizeFilter,實現將令牌信息添加到頭文件中,代碼如下:
測試:
訪問http://localhost:8001/api/user
,將生成的新令牌放到頭文件中,在令牌前面添加Bearer
,這裏主要由個空格,效果如下:
1.2 SpringSecurity權限控制
由於我們項目使用了微服務,任何用戶都有可能使用任意微服務,此時我們需要控制相關權限,例如:普通用戶角色不能使用用戶的刪除操作,只有管理員纔可以使用,那麼這個時候就需要使用到SpringSecurity的權限控制功能了。
1.2.1 角色加載
在changgou-user-oauth服務中,com.changgou.oauth.config.UserDetailsServiceImpl該類實現了加載用戶相關信息,如下代碼:
上述代碼給登錄用戶定義了三個角色,分別爲salesman
,accountant
,user
,這一塊我們目前使用的是硬編碼方式將角色寫死了,後面會從數據庫加載。
1.2.2 角色權限控制
在每個微服務中,需要獲取用戶的角色,然後根據角色識別是否允許操作指定的方法,Spring Security中定義了四個支持權限控制的表達式註解,分別是@PreAuthorize
、@PostAuthorize
、@PreFilter
和@PostFilter
。其中前兩者可以用來在方法調用前或者調用後進行權限檢查,後兩者可以用來對集合類型的參數或者返回值進行過濾。在需要控制權限的方法上,我們可以添加@PreAuthorize
註解,用於方法執行前進行權限檢查,校驗用戶當前角色是否能訪問該方法。
(1)開啓@PreAuthorize
在changgou-user-service
的ResourceServerConfig
類上添加@EnableGlobalMethodSecurity
註解,用於開啓@PreAuthorize的支持,代碼如下:
(2)方法權限控制
在changgoug-service-user
微服務的com.changgou.user.controller.UserController
類的delete()方法上添加權限控制註解@PreAuthorize
,代碼如下:
(3)測試
我們使用Postman測試,先創建令牌,然後將令牌數存放到頭文件中訪問微服務網關來調用user微服務的delete方法,效果如下:
地址:http://localhost:8001/api/user/leileia
提交方式:DELETE
發現上面無法訪問,因爲用戶登錄的時候,角色不包含admin角色,而delete方法需要admin角色,所以被攔截了。
我們再測試其他方法,其他方法沒有配置攔截,所以用戶登錄後就會放行。
訪問http://localhost:8001/api/user
效果如下:
知識點說明:
如果希望一個方法能被多個角色訪問,配置:@PreAuthorize("hasAnyAuthority('admin','user')")
如果希望一個類都能被多個角色訪問,在類上配置:@PreAuthorize("hasAnyAuthority('admin','user')")
3 OAuth動態加載數據
前面OAuth我們用的數據都是靜態的,在現實工作中,數據都是從數據庫加載的,所以我們需要調整一下OAuth服務,從數據庫加載相關數據。
- 客戶端數據[生成令牌相關數據]
- 用戶登錄賬號密碼從數據庫加載
3.1 客戶端數據加載
3.1.1 數據介紹
(1)客戶端靜態數據
在changgou-user-oauth
的com.changgou.oauth.config.AuthorizationServerConfig類中配置了客戶端靜態數據,主要用於配置客戶端數據,代碼如下:
(2)客戶端表結構介紹
創建一個數據庫changgou_oauth
,並在數據庫中創建一張表,表主要用於記錄客戶端相關信息,表結構如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
CREATE TABLE `oauth_client_details` ( `client_id` varchar(48) NOT NULL COMMENT '客戶端ID,主要用於標識對應的應用', `resource_ids` varchar(256) DEFAULT NULL, `client_secret` varchar(256) DEFAULT NULL COMMENT '客戶端祕鑰,BCryptPasswordEncoder加密算法加密', `scope` varchar(256) DEFAULT NULL COMMENT '對應的範圍', `authorized_grant_types` varchar(256) DEFAULT NULL COMMENT '認證模式', `web_server_redirect_uri` varchar(256) DEFAULT NULL COMMENT '認證後重定向地址', `authorities` varchar(256) DEFAULT NULL, `access_token_validity` int(11) DEFAULT NULL COMMENT '令牌有效期', `refresh_token_validity` int(11) DEFAULT NULL COMMENT '令牌刷新週期', `additional_information` varchar(4096) DEFAULT NULL, `autoapprove` varchar(256) DEFAULT NULL, PRIMARY KEY (`client_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
字段說明:
1 2 3 4 5 6 7
client_id:客戶端id resource_ids:資源id(暫時不用) client_secret:客戶端祕鑰 scope:範圍 access_token_validity:訪問token的有效期(秒) refresh_token_validity:刷新token的有效期(秒) authorized_grant_type:授權類型:authorization_code,password,refresh_token,client_credentials
導入2條記錄到表中,SQL如下:數據中密文分別爲changgou、szitheima
1 2
INSERT INTO `oauth_client_details` VALUES ('changgou', null, '$2a$10$wZRCFgWnwABfE60igAkBPeuGFuzk74V2jw3/trkdUZpnteCtJ9p9m', 'app', 'authorization_code,password,refresh_token,client_credentials', 'http://localhost', null, '432000000', '432000000', null, null); INSERT INTO `oauth_client_details` VALUES ('szitheima', null, '$2a$10$igxoCZxTbjWx5TrmfWEEpe/WFdwbUhbxik9BKTe9i64ZOSfnu/lqe', 'app', 'authorization_code,password,refresh_token,client_credentials', 'http://localhost', null, '432000000', '432000000', null, null);
上述表結構屬於SpringSecurity Oauth2.0所需的一個認證表結構,不能隨意更改。相關操作在其他類中有所體現,如:org.springframework.security.oauth2.provider.client.JdbcClientDetailsService
中的片段代碼如下:
3.1.2 加載數據改造
(1)修改連接配置
從數據庫加載數據,我們需要先配置數據庫連接,在changgou-user-oauth的application.yml中配置連接信息,如下代碼:
上圖代碼如下:
1 2 3 4 5
datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://192.168.211.132:3306/changgou_oauth?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=UTC username: root password: 123456
(2)修改客戶端加載源
修改changgou-user-oauth的com.changgou.oauth.config.AuthorizationServerConfig類的configure方法,將之前靜態的客戶端數據變成從數據庫加載,修改如下:
修改前:
修改後:
(3)UserDetailsServiceImpl修改
將之前的加密方式去掉即可,代碼如下:
修改前:
修改後:
(4)測試
授權碼模式測試
訪問:http://localhost:9001/oauth/authorize?client_id=szitheima&response_type=code&scop=app&redirect_uri=http://localhost
效果如下:
用戶名對應應用id,密碼對應祕鑰。賬號輸入:szitheima
密碼:szitheima
,效果如下:
密碼模式授權測試
我們之前編寫的賬號密碼登錄代碼如下,每次都會加載指定的客戶端ID和指定的祕鑰,所以此時的客戶端ID和祕鑰固定了,輸入的賬號密碼不再是客戶端ID和祕鑰了。
OAuth中的com.changgou.oauth.config.UserDetailsServiceImpl配置如下:
用戶每次輸入賬號和密碼,只要密碼是szitheima,即可登錄成功。
訪問地址http://localhost:9001/user/login
輸入賬號密碼均爲szitheima,效果如下:
3.2 用戶數據加載
因爲我們目前整套系統是對內提供登錄訪問,所以每次用戶登錄的時候oauth需要調用用戶微服務查詢用戶信息,如上圖:
我們需要在用戶微服務中提供用戶信息查詢的方法,並在oauth中使用feign調用即可。
在真實工作中,用戶和管理員對應的oauth認證服務器會分開,網關也會分開,我們今天的課堂案例只實現用戶相關的認證即可。
(1)Feign創建
在changgou-service-user-api中創建com.changgou.user.feign.UserFeign,代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12
@FeignClient(name="user") @RequestMapping("/user") public interface UserFeign { /*** * 根據ID查詢用戶信息 * @param id * @return */ @GetMapping("/load/{id}") Result<User> findById(@PathVariable String id); }
(2)修改UserController
修改changgou-service-user的UserController的findById方法,添加一個新的地址,用於加載用戶信息,代碼如下:
(3)放行查詢用戶方法
因爲oauth需要調用查詢用戶信息,需要在changgou-service-user中放行/user/load/{id}
方法,修改ResourceServerConfig,添加對/user/load/{id}
的放行操作,代碼如下:
(4)oauth調用查詢用戶信息
oauth引入對user-api的依賴
1 2 3 4 5 6
<!--依賴用戶api--> <dependency> <groupId>com.changgou</groupId> <artifactId>changgou-service-user-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
修改oauth的com.changgou.oauth.config.UserDetailsServiceImpl
的loadUserByUsername
方法,調用UserFeign查詢用戶信息,代碼如下:
(5)feign開啓
修改com.changgou.OAuthApplication
開啓Feign客戶端功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
@SpringBootApplication @EnableDiscoveryClient @EnableFeignClients(basePackages = {"com.changgou.user.feign"}) @MapperScan(basePackages = "com.changgou.auth.dao") public class OAuthApplication { public static void main(String[] args) { SpringApplication.run(OAuthApplication.class,args); } @Bean(name = "restTemplate") public RestTemplate restTemplate() { return new RestTemplate(); } }
(6)測試
我們換個數據庫中的賬號密碼登錄,分別輸入zhangsan,效果如下:
4 購物車
購物車分爲用戶登錄購物車和未登錄購物車操作,國內知名電商京東用戶登錄和不登錄都可以操作購物車,如果用戶不登錄,操作購物車可以將數據存儲到Cookie或者WebSQL或者SessionStorage中,用戶登錄後購物車數據可以存儲到Redis中,再將之前未登錄加入的購物車合併到Redis中即可。
淘寶天貓則採用了另外一種實現方案,用戶要想將商品加入購物車,必須先登錄才能操作購物車。
我們今天實現的購物車是天貓解決方案,即用戶必須先登錄才能使用購物車功能。
4.1 購物車分析
(1)需求分析
用戶在商品詳細頁點擊加入購物車,提交商品SKU編號和購買數量,添加到購物車。購物車展示頁面如下:
(2)購物車實現思路
我們實現的是用戶登錄後的購物車,用戶將商品加入購物車的時候,直接將要加入購物車的詳情存入到Redis即可。每次查看購物車的時候直接從Redis中獲取。
(3)表結構分析
用戶登錄後將商品加入購物車,需要存儲商品詳情以及購買數量,購物車詳情表如下:
changgou_order數據中tb_order_item表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
CREATE TABLE `tb_order_item` ( `id` varchar(20) COLLATE utf8_bin NOT NULL COMMENT 'ID', `category_id1` int(11) DEFAULT NULL COMMENT '1級分類', `category_id2` int(11) DEFAULT NULL COMMENT '2級分類', `category_id3` int(11) DEFAULT NULL COMMENT '3級分類', `spu_id` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT 'SPU_ID', `sku_id` bigint(20) NOT NULL COMMENT 'SKU_ID', `order_id` bigint(20) NOT NULL COMMENT '訂單ID', `name` varchar(200) COLLATE utf8_bin DEFAULT NULL COMMENT '商品名稱', `price` int(20) DEFAULT NULL COMMENT '單價', `num` int(10) DEFAULT NULL COMMENT '數量', `money` int(20) DEFAULT NULL COMMENT '總金額', `pay_money` int(11) DEFAULT NULL COMMENT '實付金額', `image` varchar(200) COLLATE utf8_bin DEFAULT NULL COMMENT '圖片地址', `weight` int(11) DEFAULT NULL COMMENT '重量', `post_fee` int(11) DEFAULT NULL COMMENT '運費', `is_return` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '是否退貨', PRIMARY KEY (`id`), KEY `item_id` (`sku_id`), KEY `order_id` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
購物車詳情表其實就是訂單詳情表結構,只是目前臨時存儲數據到Redis,等用戶下單後纔將數據從Redis取出存入到MySQL中。
在商城中一般都會有不同分類商品做折扣活動,下面有一張表記錄了對應分類商品的折扣活動,每類商品只允許參與一次折扣活動。
changgou_goods數據中tb_pref表:
1 2 3 4 5 6 7 8 9 10 11
CREATE TABLE `tb_pref` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID', `cate_id` int(11) DEFAULT NULL COMMENT '分類ID', `buy_money` int(11) DEFAULT NULL COMMENT '消費金額', `pre_money` int(11) DEFAULT NULL COMMENT '優惠金額', `start_time` date DEFAULT NULL COMMENT '活動開始日期', `end_time` date DEFAULT NULL COMMENT '活動截至日期', `type` char(1) DEFAULT NULL COMMENT '類型,1:普通訂單,2:限時活動', `state` char(1) DEFAULT NULL COMMENT '狀態,1:有效,0:無效', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
4.2 訂單購物車微服務
我們先搭建一個訂單購物車微服務工程,按照如下步驟實現即可。
(1)導入資源
搭建訂單購物車微服務,工程名字changgou-service-order並搭建對應的api工程changgou-service-order-api,將生成好的dao和相關文件拷貝到工程中,以及生成好的Pojo拷貝到API工程中。同時在changgou-service-order中引入changgou-service-order-api,如下圖:
依賴引入:
1 2 3 4 5
<dependency> <groupId>com.changgou</groupId> <artifactId>changgou-service-order-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
changgou-service-order:
changgou-service-order-api:
(2)application.yml配置
在changgou-service-order的resources中添加application.yml配置文件,代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
server: port: 18090 spring: application: name: order datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://192.168.211.132:3306/changgou_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC username: root password: 123456 redis: host: 192.168.211.132 port: 6379 main: allow-bean-definition-overriding: true eureka: client: service-url: defaultZone: http://127.0.0.1:7001/eureka instance: prefer-ip-address: true feign: hystrix: enabled: true
(3)創建啓動類
在changgou-service-order的resources中創建啓動類,代碼如下:
1 2 3 4 5 6 7 8 9
@SpringBootApplication @EnableEurekaClient @MapperScan(basePackages = {"com.changgou.order.dao"}) public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class,args); } }
4.3 添加購物車
4.3.1 思路分析
用戶添加購物車,只需要將要加入購物車的商品存入到Redis中即可。一個用戶可以將多件商品加入購物車,存儲到Redis中的數據可以採用Hash類型。
選Hash類型可以將用戶的用戶名作爲namespace的一部分,將指定商品加入購物車,則往對應的namespace中增加一個key和value,key是商品ID,value是加入購物車的商品詳情,如下圖:
4.3.2 代碼實現
(1)feign創建
下訂單需要調用feign查看商品信息,我們先創建feign分別根據ID查詢Sku和Spu信息,在changgou-service-goods-api工程中的SkuFeign和SpuFeign根據ID查詢方法如下:
com.changgou.goods.feign.SkuFeign
1 2 3 4 5 6
/*** * 根據ID查詢SKU信息 * @param id : sku的ID */ @GetMapping(value = "/{id}") public Result<Sku> findById(@PathVariable(value = "id", required = true) Long id);
com.changgou.goods.feign.SpuFeign
1 2 3 4 5 6 7
/*** * 根據SpuID查詢Spu信息 * @param id * @return */ @GetMapping("/{id}") public Result<Spu> findById(@PathVariable(name = "id") Long id);
(2)業務層
業務層接口
在changgou-service-order微服務中創建com.changgou.order.service.CartService接口,代碼如下:
1 2 3 4 5 6 7 8 9 10 11
public interface CartService { /*** * 添加購物車 * @param num:購買商品數量 * @param id:購買ID * @param username:購買用戶 * @return */ void add(Integer num, Long id, String username); }
業務層接口實現類
在changgou-service-order微服務中創建接口實現類com.changgou.order.service.impl.CartServiceImpl,代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
@Service public class CartServiceImpl implements CartService { @Autowired private RedisTemplate redisTemplate; @Autowired private SkuFeign skuFeign; @Autowired private SpuFeign spuFeign; /*** * 加入購物車 * @param num:購買商品數量 * @param id:購買ID * @param username:購買用戶 * @return */ @Override public void add(Integer num, Long id, String username) { //查詢SKU Result<Sku> resultSku = skuFeign.findById(id); if(resultSku!=null && resultSku.isFlag()){ //獲取SKU Sku sku = resultSku.getData(); //獲取SPU Result<Spu> resultSpu = spuFeign.findById(sku.getSpuId()); //將SKU轉換成OrderItem OrderItem orderItem = sku2OrderItem(sku,resultSpu.getData(), num); /****** * 購物車數據存入到Redis * namespace = Cart_[username] * key=id(sku) * value=OrderItem */ redisTemplate.boundHashOps("Cart_"+username).put(id,orderItem); } } /*** * SKU轉成OrderItem * @param sku * @param num * @return */ private OrderItem sku2OrderItem(Sku sku,Spu spu,Integer num){ OrderItem orderItem = new OrderItem(); orderItem.setSpuId(sku.getSpuId()); orderItem.setSkuId(sku.getId()); orderItem.setName(sku.getName()); orderItem.setPrice(sku.getPrice()); orderItem.setNum(num); orderItem.setMoney(num*orderItem.getPrice()); //單價*數量 orderItem.setPayMoney(num*orderItem.getPrice()); //實付金額 orderItem.setImage(sku.getImage()); orderItem.setWeight(sku.getWeight()*num); //重量=單個重量*數量 //分類ID設置 orderItem.setCategoryId1(spu.getCategory1Id()); orderItem.setCategoryId2(spu.getCategory2Id()); orderItem.setCategoryId3(spu.getCategory3Id()); return orderItem; } }
(3)控制層
在changgou-service-order微服務中創建com.changgou.order.controller.CartController,代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
@RestController @CrossOrigin @RequestMapping(value = "/cart") public class CartController { @Autowired private CartService cartService; /*** * 加入購物車 * @param num:購買的數量 * @param id:購買的商品(SKU)ID * @return */ @RequestMapping(value = "/add") public Result add(Integer num, Long id){ //用戶名 String username="szitheima"; //將商品加入購物車 cartService.add(num,id,username); return new Result(true, StatusCode.OK,"加入購物車成功!"); } }
(4)feign配置
修改com.changgou.OrderApplication
開啓Feign客戶端:
1 2 3 4 5 6 7 8 9 10
@SpringBootApplication @EnableEurekaClient @EnableFeignClients(basePackages = {"com.changgou.goods.feign"}) @MapperScan(basePackages = {"com.changgou.order.dao"}) public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class,args); } }
測試添加購物車,效果如下:
請求地址http://localhost:18090/cart/add?num=6&id=1148477873175142400
Redis緩存中的數據
4.4 購物車列表
4.4.1 思路分析
接着我們實現一次購物車列表操作。因爲存的時候是根據用戶名往Redis中存儲用戶的購物車數據的,所以我們這裏可以將用戶的名字作爲key去Redis中查詢對應的數據。
4.4.2 代碼實現
(1)業務層
業務層接口
修改changgou-service-order微服務的com.changgou.order.service.CartService接口,添加購物車列表方法,代碼如下:
1 2 3 4 5 6
/*** * 查詢用戶的購物車數據 * @param username * @return */ List<OrderItem> list(String username);
業務層接口實現類
修改changgou-service-order微服務的com.changgou.order.service.impl.CartServiceImpl類,添加購物車列表實現方法,代碼如下:
1 2 3 4 5 6 7 8 9 10 11
/*** * 查詢用戶購物車數據 * @param username * @return */ @Override public List<OrderItem> list(String username) { //查詢所有購物車數據 List<OrderItem> orderItems = redisTemplate.boundHashOps("Cart_"+username).values(); return orderItems; }
(2)控制層
修改changgou-service-order微服務的com.changgou.order.controller.CartController類,添加購物車列表查詢方法,代碼如下:
1 2 3 4 5 6 7 8 9 10 11
/*** * 查詢用戶購物車列表 * @return */ @GetMapping(value = "/list") public Result list(){ //用戶名 String username="szitheima"; List<OrderItem> orderItems = cartService.list(username); return new Result(true,StatusCode.OK,"購物車列表查詢成功!",orderItems); }
(3)測試
使用Postman訪問 GET http://localhost:18090/cart/list ,效果如下:
4.4.3 問題處理
(1)刪除商品購物車
我們發現個問題,就是用戶將商品加入購物車,無論數量是正負,都會執行添加購物車,如果數量如果<=0,應該移除該商品的。
修改changgou-service-order的com.changgou.order.service.impl.CartServiceImpl的add方法,添加如下代碼:
(2)數據精度丟失問題
SkuId是Long類型,在頁面輸出的時候會存在精度丟失問題,我們只需要在OrderItem的SkuId上加上字符串序列化類型就可以了,代碼如下:
5 用戶身份識別
5.1 購物車需求分析
購物車功能已經做完了,但用戶我們都是硬編碼寫死的。用戶要想將商品加入購物車,必須得先登錄授權,登錄授權後再經過微服務網關,微服務網關需要過濾判斷用戶請求是否存在令牌,如果存在令牌,才能再次訪問微服務,此時網關會通過過濾器將令牌數據再次存入到頭文件中,然後訪問模板渲染服務,模板渲染服務再調用訂單購物車微服務,此時也需要將令牌數據存入到頭文件中,將令牌數據傳遞給購物車訂單微服務,到了購物車訂單微服務的時候,此時微服務需要校驗令牌數據,如果令牌正確,才能使用購物車功能,並解析令牌數據獲取用戶信息。
5.2 微服務之間認證
如上圖:因爲微服務之間並沒有傳遞頭文件,所以我們可以定義一個攔截器,每次微服務調用之前都先檢查下頭文件,將請求的頭文件中的令牌數據再放入到header中,再調用其他微服務即可。
(1)創建攔截器
在changgou-web-order服務中創建一個com.changgou.order.interceptor.FeignInterceptor攔截器,並將所有頭文件數據再次加入到Feign請求的微服務頭文件中,代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
public class FeignInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate requestTemplate) { try { //使用RequestContextHolder工具獲取request相關變量 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes != null) { //取出request HttpServletRequest request = attributes.getRequest(); //獲取所有頭文件信息的key Enumeration<String> headerNames = request.getHeaderNames(); if (headerNames != null) { while (headerNames.hasMoreElements()) { //頭文件的key String name = headerNames.nextElement(); //頭文件的value String values = request.getHeader(name); //將令牌數據添加到頭文件中 requestTemplate.header(name, values); } } } } catch (Exception e) { e.printStackTrace(); } } }
(2)創建攔截器Bean
在changgou-web-order服務中啓動類裏創建對象實例
1 2 3 4 5 6 7 8
/*** * 創建攔截器Bean對象 * @return */ @Bean public FeignInterceptor feignInterceptor(){ return new FeignInterceptor(); }
(3)測試
我們發現這塊的ServletRequestAttributes始終爲空,RequestContextHolder.getRequestAttributes()該方法是從ThreadLocal變量裏面取得相應信息的,當hystrix斷路器的隔離策略爲THREAD時,是無法取得ThreadLocal中的值。
解決方案:hystrix隔離策略換爲SEMAPHORE
修改changgou-web-order的application.yml配置文件,在application.yml中添加如下代碼,代碼如下:
1 2 3 4 5 6 7 8 9
#hystrix 配置 hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 10000 strategy: SEMAPHORE
再次測試,效果如下:
(4)工具類抽取
微服務之間相互認證的情況非常多,我們可以把上面的攔截器抽取出去,放到changgou-common的entity包中,其他工程需要用,直接創建一個@Bean對象即可。
5.3 網關過濾
爲了不給微服務帶來一些無效的請求,我們可以在網關中過濾用戶請求,先看看頭文件中是否有Authorization,如果有再看看cookie中是否有Authorization,如果都通過了才允許請求到達微服務。
5.4 訂單對接網關+oauth
(1)application.yml配置
修改微服務網關changgou-gateway-web
的application.yml配置文件,添加order的路由過濾配置,配置如下:
上圖代碼如下:
1 2 3 4 5 6 7
#訂單微服務 - id: changgou_order_route uri: lb://order predicates: - Path=/api/cart/**,/api/categoryReport/**,/api/orderConfig/**,/api/order/**,/api/orderItem/**,/api/orderLog/**,/api/preferential/**,/api/returnCause/**,/api/returnOrder/**,/api/returnOrderItem/** filters: - StripPrefix=1
這裏注意使用的是yml格式,所以上面代碼中的空格也一併記得拷貝到application.yml文件中。
(2)過濾配置
在微服務網關changgou-gateway-web
中添加com.changgou.filter.URLFilter
過濾類,用於過濾需要用戶登錄的地址,代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
public class URLFilter { /** * 要放行的路徑 */ private static final String noAuthorizeurls = "/api/user/add,/api/user/login"; /** * 判斷 當前的請求的地址中是否在已有的不攔截的地址中存在,如果存在 則返回true 表示 不攔截 false表示攔截 * * @param uri 獲取到的當前的請求的地址 * @return */ public static boolean hasAuthorize(String uri) { String[] split = noAuthorizeurls.split(","); for (String s : split) { if (s.equals(uri)) { return true; } } return false; } }
(3)全局過濾器修改
修改之前的com.changgou.filter.AuthorizeFilter
的過濾方法,將是否需要用戶登錄過濾也加入其中,代碼如下:
(4)集成OAuth進行安全校驗
修改changgou-service-order的pom.xml,添加oauth的依賴
1 2 3 4 5
<!--oauth依賴--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency>
將公鑰拷貝到changgou-service-order工程的resources中
在changgou-service-order工程中創建com.changgou.order.config.ResourceServerConfig,配置需要攔截的路徑,這裏需要攔截所有請求路徑,代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
@Configuration @EnableResourceServer //開啓方法上的PreAuthorize註解 @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class ResourceServerConfig extends ResourceServerConfigurerAdapter { //公鑰 private static final String PUBLIC_KEY = "public.key"; /*** * 定義JwtTokenStore * @param jwtAccessTokenConverter * @return */ @Bean public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) { return new JwtTokenStore(jwtAccessTokenConverter); } /*** * 定義JJwtAccessTokenConverter * @return */ @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setVerifierKey(getPubKey()); return converter; } /** * 獲取非對稱加密公鑰 Key * @return 公鑰 Key */ private String getPubKey() { Resource resource = new ClassPathResource(PUBLIC_KEY); try { InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream()); BufferedReader br = new BufferedReader(inputStreamReader); return br.lines().collect(Collectors.joining("\n")); } catch (IOException ioe) { return null; } } /*** * Http安全配置,對每個到達系統的http請求鏈接進行校驗 * @param http * @throws Exception */ @Override public void configure(HttpSecurity http) throws Exception { //所有請求必須認證通過 http.authorizeRequests() .anyRequest(). authenticated(); //其他地址需要認證授權 } }
(5)測試
使用瀏覽器訪問 http://localhost:8001/api/cart/list
,效果如下:
未登錄:
使用Postman訪問 http://localhost:8001/api/cart/list
,效果如下:
已登錄:
5.5 獲取用戶數據
5.5.1 數據分析
用戶登錄後,數據會封裝到SecurityContextHolder.getContext().getAuthentication()
裏面,我們可以將數據從這裏面取出,然後轉換成OAuth2AuthenticationDetails
,在這裏面可以獲取到令牌信息、令牌類型等,代碼如下:
這裏的tokenValue是加密之後的令牌數據,remoteAddress是用戶的IP信息,tokenType是令牌類型。
我們可以獲取令牌加密數據後,使用公鑰對它進行解密,如果能解密說明說句無誤,如果不能解密用戶也沒法執行到這一步。解密後可以從明文中獲取用戶信息。
5.5.2 代碼實現
(1)讀取公鑰
在changgou-service-order微服務中創建com.changgou.order.config.TokenDecode類,用於解密令牌信息,在類中讀取公鑰信息,代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
@Component public class TokenDecode { //公鑰 private static final String PUBLIC_KEY = "public.key"; private static String publickey=""; /** * 獲取非對稱加密公鑰 Key * @return 公鑰 Key */ public String getPubKey() { if(!StringUtils.isEmpty(publickey)){ return publickey; } Resource resource = new ClassPathResource(PUBLIC_KEY); try { InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream()); BufferedReader br = new BufferedReader(inputStreamReader); publickey = br.lines().collect(Collectors.joining("\n")); return publickey; } catch (IOException ioe) { return null; } } }
(2)校驗解析令牌數據
在TokenDecode類中添加校驗解析令牌數據的方法,這裏用到了JwtHelper實現。
1 2 3 4 5 6 7 8 9 10 11
/*** * 讀取令牌數據 */ public Map<String,String> dcodeToken(String token){ //校驗Jwt Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(getPubKey())); //獲取Jwt原始內容 String claims = jwt.getClaims(); return JSON.parseObject(claims,Map.class); }
(3)獲取令牌數據
在TokenDecode類中添加一個getUserInfo方法,用於從容器中獲取令牌信息,代碼如下:
1 2 3 4 5 6 7 8 9 10
/*** * 獲取用戶信息 * @return */ public Map<String,String> getUserInfo(){ //獲取授權信息 OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) SecurityContextHolder.getContext().getAuthentication().getDetails(); //令牌解碼 return dcodeToken(details.getTokenValue()); }
完整代碼:
(4)控制層獲取用戶數據
在CartController中注入TokenDecode,並調用TokenDecode的getUserInfo方法獲取用戶信息,代碼如下:
注入TokenDecode:
1 2
@Autowired private TokenDecode tokenDecode;
獲取用戶名:
(5)測試
用戶登錄後測試http://localhost:8001/api/wcart/list
斷點調試可以獲取到用戶名信息:
請求結果:
5.6 代碼抽取
以後很有可能在很多微服務中都會用到該對象來獲取用戶信息,我們可以把它抽取出去。
在changgou-common工程中引入鑑權包
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<!--oauth依賴--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> <scope>provided</scope> </dependency> <!--鑑權--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> <scope>provided</scope> </dependency>
將TokenDecode拷貝到entity包中,並且刪除changgou-service-order中的TokenDecode,同時在changgou-service-order啓動類中創建該對象,並交給SpringIOC容器管理。