天天看點

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

暢購商城文章系列

暢購商城:分布式檔案系統FastDFS

暢購商城:商品的SPU和SKU概念

暢購商城:Lua、OpenResty、Canal實作廣告緩存

暢購商城:微服務網關和JWT令牌(上)

暢購商城:微服務網關和JWT令牌(下)

暢購商城:Spring Security Oauth2 JWT(上)

暢購商城:Spring Security Oauth2 JWT(下)

暢購商城:購物車

暢購商城:訂單

暢購商城:微信支付

暢購商城:秒殺(上)

暢購商城:秒殺(下)

暢購商城:分布式事務

暢購商城:叢集高可用

目錄

  • 暢購商城文章系列
  • 學習目标
  • 前言
  • 1. 資源伺服器授權配置
    • 1.1 資源服務授權配置
    • 1.2 使用者微服務資源授權
  • 2. Oauth對接微服務
    • 2.1 令牌加入到Header中
    • 2.2 SpringSecurity權限控制
      • 2.2.1 角色加載
      • 2.2.2 角色權限控制
  • 3. Oauth動态加載資料
    • 3.1 用戶端資料加載
      • 3.1.1 資料介紹
      • 3.1.2 加載資料改造
    • 3.2 使用者資料加載
  • 4. 購物車
    • 4.1 購物車分析
    • 4.2 訂單購物車微服務
    • 4.3 添加購物車
      • 4.3.1 思路分析
      • 4.3.2 代碼實作
    • 4.4 購物車清單
      • 4.4.1 思路分析
      • 4.4.2 代碼實作
      • 4.4.3 問題處理
  • 5. 使用者身份識别
    • 5.1 購物車需求分析
    • 5.2 微服務之間認證(令牌傳遞)
    • 5.3 網關過濾
    • 5.4 訂單對接網關+oauth
    • 5.5 擷取使用者資料
      • 5.5.1 資料分析
      • 5.5.2 代碼實作

學習目标

  • 資源伺服器授權配置
  • 掌握Oauth認證微服務動态加載資料
  • 掌握購物車流程
  • 掌握購物車渲染流程
  • Oauth2.0認證并擷取使用者令牌資料
  • 微服務與微服務之間的認證

前言

1. 資源伺服器授權配置

1.1 資源服務授權配置

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

基本上所有微服務都是資源服務

(1)配置公鑰 認證服務生成令牌采用非對稱加密算法,認證服務采用私鑰加密生成令牌,對外向資源服務提供公鑰,資源服務使 用公鑰 來校驗令牌的合法性。 将公鑰拷貝到 public.key檔案中,将此檔案拷貝到每一個需要的資源服務工程的classpath下 ,例如:使用者微服務.

(2)添加依賴

<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 使用者微服務資源授權

使用者每次通路微服務的時候,需要先申請令牌,令牌申請後,每次将令牌放到頭檔案中,才能通路微服務。

頭檔案中每次需要添加一個Authorization頭資訊,頭的結果為bearer token。

将上面生成的公鑰public.key拷貝到changgou-service-user微服務工程的resources目錄下,

(1)引入依賴

在changgou-service-user微服務工程pom.xml中引入oauth依賴

<!--oauth依賴-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
           

(2)資源授權配置

在changgou-service-user工程中建立com.changgou.user.config.ResourceServerConfig,代碼如下:

@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();    //其他位址需要認證授權
    }
}
           

2. Oauth對接微服務

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

使用者每次通路微服務的時候,先去oauth2.0服務登入,登入後再通路微服務網關,微服務網關将請求轉發給其他微服務處理。

  1. 使用者登入成功後,會将令牌資訊存入到cookie中(一般建議存入到頭檔案中)
  2. 使用者攜帶Cookie中的令牌通路微服務網關
  3. 微服務網關先擷取頭檔案中的令牌資訊,如果Header中沒有Authorization令牌資訊,則取參數中找,參數中如果沒有,則取Cookie中找Authorization,最後将令牌資訊封裝到Header中,并調用其他微服務
  4. 其他微服務會擷取頭檔案中的Authorization令牌資訊,然後比對令牌資料是否能使用公鑰解密,如果解密成功說明使用者已登入,解密失敗,說明使用者未登入

2.1 令牌加入到Header中

修改changgou-gateway-web的全局過濾器com.changgou.filter.AuthorizeFilter,實作将令牌資訊添加到頭檔案中,代碼如下:

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

2.2 SpringSecurity權限控制

由于我們項目使用了微服務,任何使用者都有可能使用任意微服務,此時我們需要控制相關權限,例如:普通使用者角色不能使用使用者的删除操作,隻有管理者才可以使用,那麼這個時候就需要使用到SpringSecurity的權限控制功能了。

2.2.1 角色加載

在changgou-user-oauth服務中,com.changgou.oauth.config.UserDetailsServiceImpl該類實作了加載使用者相關資訊,如下代碼:

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

上述代碼給登入使用者定義了三個角色,分别為salesman,accountant,user,這一塊我們目前使用的是寫死方式将角色寫死了,後面會從資料庫加載。

2.2.2 角色權限控制

在每個微服務中,需要擷取使用者的角色,然後根據角色識别是否允許操作指定的方法,Spring Security中定義了四個支援權限控制的表達式注解,分别是@PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter。其中前兩者可以用來在方法調用前或者調用後進行權限檢查,後兩者可以用來對集合類型的參數或者傳回值進行過濾。在需要控制權限的方法上,我們可以添加@PreAuthorize注解,用于方法執行前進行權限檢查,校驗使用者目前角色是否能通路該方法。

(1)開啟@PreAuthorize

在changgou-user-service的ResourceServerConfig類上添加@EnableGlobalMethodSecurity注解,用于開啟@PreAuthorize的支援,代碼如下:

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

(2)方法權限控制

在changgoug-service-user微服務的com.changgou.user.controller.UserController類的delete()方法上添權重限控制注解@PreAuthorize,代碼如下:

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

(3)測試

我們使用Postman測試,先建立令牌,然後将令牌數存放到頭檔案中通路微服務網關來調用user微服務的delete方法.

知識點說明:

  1. 如果希望一個方法能被多個角色通路,配置:@PreAuthorize(“hasAnyAuthority(‘admin’,‘user’)”)
  2. 如果希望一個類都能被多個角色通路,在類上配置:@PreAuthorize(“hasAnyAuthority(‘admin’,‘user’)”)

3. Oauth動态加載資料

前面OAuth我們用的資料都是靜态的,在現實工作中,資料都是從資料庫加載的,是以我們需要調整一下OAuth服務,從資料庫加載相關資料。

  • 用戶端資料[生成令牌相關資料]
  • 使用者登入賬号密碼從資料庫加載

3.1 用戶端資料加載

3.1.1 資料介紹

(1)用戶端靜态資料

在changgou-user-oauth的

com.changgou.oauth.config.AuthorizationServerConfig

類中配置了用戶端靜态資料,主要用于配置用戶端資料,代碼如下:

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

(2)用戶端表結構介紹

建立一個資料庫changgou_oauth,并在資料庫中建立一張表,表主要用于記錄用戶端相關資訊,表結構如下:

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;
           

字段說明:

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

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所需的一個認證表結構,不能随意更改。

3.1.2 加載資料改造

(1)修改連接配接配置

從資料庫加載資料,我們需要先配置資料庫連接配接,在changgou-user-oauth的application.yml中配置連接配接資訊,如下代碼:

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方法,将之前靜态的用戶端資料變成從資料庫加載,修改如下:

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

(3)UserDetailsServiceImpl修改

将之前的加密方式去掉即可,代碼如下:

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

3.2 使用者資料加載

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

因為我們目前整套系統是對内提供登入通路,是以每次使用者登入的時候oauth需要調用使用者微服務查詢使用者資訊,如上圖:

我們需要在使用者微服務中提供使用者資訊查詢的方法,并在oauth中使用feign調用即可。

在真實工作中,使用者和管理者對應的oauth認證伺服器會分開,網關也會分開,我們今天的課堂案例隻實作使用者相關的認證即可。

(1)Feign建立

在changgou-service-user-api中建立com.changgou.user.feign.UserFeign,代碼如下:

@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方法,添加一個新的位址,用于加載使用者資訊,代碼如下:

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

(3)放行查詢使用者方法

因為oauth需要調用查詢使用者資訊,需要在changgou-service-user中放行/user/load/{id}方法,修改ResourceServerConfig,添加對/user/load/{id}的放行操作,代碼如下:

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

(4)oauth調用查詢使用者資訊

oauth引入對user-api的依賴

<!--依賴使用者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查詢使用者資訊,代碼如下:

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

(5)feign開啟

修改com.changgou.OAuthApplication開啟Feign用戶端功能

@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();
    }

}
           

4. 購物車

購物車分為使用者登入購物車和未登入購物車操作,國内知名電商京東使用者登入和不登入都可以操作購物車,如果使用者不登入,操作購物車可以将資料存儲到Cookie或者WebSQL或者SessionStorage中,使用者登入後購物車資料可以存儲到Redis中,再将之前未登入加入的購物車合并到Redis中即可。

淘寶天貓則采用了另外一種實作方案,使用者要想将商品加入購物車,必須先登入才能操作購物車。

我們今天實作的購物車是天貓解決方案,即使用者必須先登入才能使用購物車功能。

總結:

購物車類型:2種

1.使用者未登入和已登入都可以使用購物車【友善】

  • 不登入,購物車資料存入到用戶端中(Cookie或者WebSQL(WebSQL更安全) [HS])
  • 已登入,可以存儲到服務端(MySQL、Redis、MongoDB)
  • 狀态切換:使用者未登入,商品加入購物車 - > 登入(之前可以存着購物車資料),購物車合并
2.使用者未登入不能使用購物車,已登入才能使用購物車【資料更安全】
  • 已登入,可以存儲到服務端(MySQL、Redis、MongoDB)

4.1 購物車分析

(1)需求分析

使用者在商品詳細頁點選加入購物車,送出商品SKU編号和購買數量,添加到購物車。

(2)購物車實作思路

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

我們實作的是使用者登入後的購物車,使用者将商品加入購物車的時候,直接将要加入購物車的詳情存入到Redis即可。每次檢視購物車的時候直接從Redis中擷取。

(3)表結構分析

使用者登入後将商品加入購物車,需要存儲商品詳情以及購買數量,購物車詳情表如下:

changgou_order資料中tb_order_item表:

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表:

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,如下圖:

依賴引入:

<dependency>
    <groupId>com.changgou</groupId>
    <artifactId>changgou-service-order-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
           

(2)application.yml配置

在changgou-service-order的resources中添加application.yml配置檔案,代碼如下:

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中建立啟動類,代碼如下:

@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 思路分析

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

使用者添加購物車,隻需要将要加入購物車的商品存入到Redis中即可。一個使用者可以将多件商品加入購物車,存儲到Redis中的資料可以采用Hash類型。

選Hash類型可以将使用者的使用者名作為namespace的一部分,将指定商品加入購物車,則往對應的namespace中增加一個key和value,key是商品ID,value是加入購物車的商品詳情,如下圖:

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

4.3.2 代碼實作

(1)feign建立

下訂單需要調用feign檢視商品資訊,我們先建立feign分别根據ID查詢Sku和Spu資訊,在changgou-service-goods-api工程中的SkuFeign和SpuFeign根據ID查詢方法如下:

com.changgou.goods.feign.SkuFeign

/***
 * 根據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

/***
 * 根據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接口,代碼如下:

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,代碼如下:

@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,代碼如下:

@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用戶端:

@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);
    }
}
           

4.4 購物車清單

4.4.1 思路分析

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

接着我們實作一次購物車清單操作。因為存的時候是根據使用者名往Redis中存儲使用者的購物車資料的,是以我們這裡可以将使用者的名字作為key去Redis中查詢對應的資料。

4.4.2 代碼實作

(1)業務層

業務層接口

修改changgou-service-order微服務的com.changgou.order.service.CartService接口,添加購物車清單方法,代碼如下:

/***
 * 查詢使用者的購物車資料
 * @param username
 * @return
 */
List<OrderItem> list(String username);
           

業務層接口實作類

修改changgou-service-order微服務的com.changgou.order.service.impl.CartServiceImpl類,添加購物車清單實作方法,代碼如下:

/***
 * 查詢使用者購物車資料
 * @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類,添加購物車清單查詢方法,代碼如下:

/***
 * 查詢使用者購物車清單
 * @return
 */
@GetMapping(value = "/list")
public Result list(){
    //使用者名
    String username="szitheima";
    List<OrderItem> orderItems = cartService.list(username);
    return new Result(true,StatusCode.OK,"購物車清單查詢成功!",orderItems);
}
           

4.4.3 問題處理

(1)删除商品購物車

我們發現個問題,就是使用者将商品加入購物車,無論數量是正負,都會執行添加購物車,如果數量如果<=0,應該移除該商品的。

修改changgou-service-order的com.changgou.order.service.impl.CartServiceImpl的add方法,添加如下代碼:

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

(2)資料精度丢失問題

SkuId是Long類型,在頁面輸出的時候會存在精度丢失問題,我們隻需要在OrderItem的SkuId上加上字元串序列化類型就可以了,代碼如下:

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

5. 使用者身份識别

5.1 購物車需求分析

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

購物車功能已經做完了,但使用者我們都是寫死寫死的。使用者要想将商品加入購物車,必須得先登入授權,登入授權後再經過微服務網關,微服務網關需要過濾判斷使用者請求是否存在令牌,如果存在令牌,才能再次通路微服務,此時網關會通過過濾器将令牌資料再次存入到頭檔案中,然後通路模闆渲染服務,模闆渲染服務再調用訂單購物車微服務,此時也需要将令牌資料存入到頭檔案中,将令牌資料傳遞給購物車訂單微服務,到了購物車訂單微服務的時候,此時微服務需要校驗令牌資料,如果令牌正确,才能使用購物車功能,并解析令牌資料擷取使用者資訊。

5.2 微服務之間認證(令牌傳遞)

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

如上圖:因為微服務之間并沒有傳遞頭檔案,是以我們可以定義一個攔截器,每次微服務調用之前都先檢查下頭檔案,将請求的頭檔案中的令牌資料再放入到header中,再調用其他微服務即可。

從資料庫加載查詢使用者資訊

1:沒有令牌,Feign調用之前,生成令牌(admin)

2:Feign調用之前,令牌需要攜帶過去

3:Feign調用之前,令牌需要存放到Header檔案中

4:請求 -> Feign調用 -> 攔截器 RequestInterceptor -> Feign調用之前執行攔截

(1)建立攔截器

在changgou-web-order服務中建立一個com.changgou.order.interceptor.FeignInterceptor攔截器,并将所有頭檔案資料再次加入到Feign請求的微服務頭檔案中,代碼如下:

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服務中啟動類裡建立對象執行個體

/***
 * 建立攔截器Bean對象
 * @return
 */
@Bean
public FeignInterceptor feignInterceptor(){
    return new FeignInterceptor();
           

(3)測試

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

我們發現這塊的ServletRequestAttributes始終為空,RequestContextHolder.getRequestAttributes()該方法是從ThreadLocal變量裡面取得相應資訊的,當hystrix斷路器的隔離政策為THREAD時,是無法取得ThreadLocal中的值。

  • 開啟Feign熔斷:預設是線程池隔離
  • 使用者目前請求的時候對應線程的資料,如果開啟了熔斷,預設是線程池隔離,會開啟新的線程,需要将熔斷政策換成信号量隔離,此時不會開啟新的線程。

隔離差別:

比較項 線程池隔離 信号量隔離
線程 與調用線程非相同線程 與調用線程相同(jetty線程)
開銷 排隊、排程、上下文開銷等 無線程切換,開銷低
異步 支援 不支援
并發支援 支援(最大線程池大小) 支援(最大信号量上限)

解決方案:hystrix隔離政策換為SEMAPHORE

修改changgou-web-order的application.yml配置檔案,在application.yml中添加如下代碼,代碼如下:

#hystrix 配置
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000
          strategy: SEMAPHORE
           
暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

(4)工具類抽取

微服務之間互相認證的情況非常多,我們可以把上面的攔截器抽取出去,放到changgou-common的entity包中,其他工程需要用,直接建立一個@Bean對象即可。

5.3 網關過濾

為了不給微服務帶來一些無效的請求,我們可以在網關中過濾使用者請求,先看看頭檔案中是否有Authorization,如果有再看看cookie中是否有Authorization,如果都通過了才允許請求到達微服務。

5.4 訂單對接網關+oauth

(1)application.yml配置

修改微服務網關changgou-gateway-web的application.yml配置檔案,添加order的路由過濾配置,配置如下:

#訂單微服務
            - 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過濾類,用于過濾需要使用者登入的位址,代碼如下:

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的過濾方法,将是否需要使用者登入過濾也加入其中,代碼如下:

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

(4)內建OAuth進行安全校驗

修改changgou-service-order的pom.xml,添加oauth的依賴

<!--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,配置需要攔截的路徑,這裡需要攔截所有請求路徑,代碼如下:

@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.5 擷取使用者資料

5.5.1 資料分析

使用者登入後,資料會封裝到SecurityContextHolder.getContext().getAuthentication()裡面,我們可以将資料從這裡面取出,然後轉換成OAuth2AuthenticationDetails,在這裡面可以擷取到令牌資訊、令牌類型等,代碼如下:

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别

這裡的tokenValue是加密之後的令牌資料,remoteAddress是使用者的IP資訊,tokenType是令牌類型。

我們可以擷取令牌加密資料後,使用公鑰對它進行解密,如果能解密說明說句無誤,如果不能解密使用者也沒法執行到這一步。解密後可以從明文中擷取使用者資訊。

5.5.2 代碼實作

(1)讀取公鑰

在changgou-service-order微服務中建立com.changgou.order.config.TokenDecode類,用于解密令牌資訊,在類中讀取公鑰資訊,代碼如下:

@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實作。

/***
 * 讀取令牌資料
 */
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方法,用于從容器中擷取令牌資訊,代碼如下:

/***
 * 擷取使用者資訊
 * @return
 */
public Map<String,String> getUserInfo(){
    //擷取授權資訊
    OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) SecurityContextHolder.getContext().getAuthentication().getDetails();
    //令牌解碼
    return dcodeToken(details.getTokenValue());
}
           

(4)控制層擷取使用者資料

在CartController中注入TokenDecode,并調用TokenDecode的getUserInfo方法擷取使用者資訊,代碼如下:

注入TokenDecode:

@Autowired
private TokenDecode tokenDecode;
           

擷取使用者名:

暢購商城:購物車暢購商城文章系列學習目标前言1. 資源伺服器授權配置2. Oauth對接微服務3. Oauth動态加載資料4. 購物車5. 使用者身份識别