整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          Spring Security在前后端分離項目中的使

          Spring Security在前后端分離項目中的使用

          文章導讀

          Spring Security 是 Spring 家族中的一個安全管理框架,可以和Spring Boot項目很方便的集成。Spring Security框架的兩大核心功能:認證授權

          認證: 驗證當前訪問系統的是不是本系統的用戶,并且要確認具體是哪個用戶。簡單的理解就是登陸操作,如果可以登錄成功就說明您是本系統的用戶,如不能登錄就說明不是本系

          統的用戶!而且登錄成功以后需要記錄當前登錄用戶的信息!

          授權:經過認證后判斷當前用戶是否有權限進行某個操作!

          如上圖所示就是展示了當前登錄用戶可以操作的權限:用戶管理、角色管理、菜單管理等,并且針對角色管理可以進行新增、修改、刪除、導出等權限。

          而現在前后端分離開發成為了主流的開發方式,那么在前后端分離開發方式下如何使用Spring Security就是本文章需要重點研究的內容。

          2 Spring Security認證功能

          2.1 前端分離項目的認證流程

          要想了解如果使用Spring Security進行認證,那么就需要先了解一下前后端分離項目中的認證流程,如下所示:

          2.2 Spring Security原理初探

          要想使用Spring Security框架來實現上述的認證操作,就必須先要了解一個Spring Security框架的工作流程。

          2.2.1 過濾器鏈

          Spring Security的原理其實就是一個過濾器鏈,內部包含了提供各種功能的過濾器。這里我們可以看看入門案例中的過濾器。

          圖中只展示了核心過濾器,其它的非核心過濾器并沒有在圖中展示。

          UsernamePasswordAuthenticationFilter: 負責處理我們在登陸頁面填寫了用戶名密碼后的登陸請求。

          ExceptionTranslationFilter:處理過濾器鏈中拋出的任何AccessDeniedException和AuthenticationException 。

          FilterSecurityInterceptor:負責權限校驗的過濾器。

          2.2.2 認證流程

          Spring Security的認證流程大致如下所示:

          概念速查:

          Authentication接口: 它的實現類,表示當前訪問系統的用戶,封裝了用戶相關信息。

          AuthenticationManager接口:定義了認證Authentication的方法

          UserDetailsService接口:加載用戶特定數據的核心接口。里面定義了一個根據用戶名查詢用戶信息的方法。

          UserDetails接口:提供核心用戶信息。通過UserDetailsService根據用戶名獲取處理的用戶信息要封裝成UserDetails對象返回。然后將這些信息封裝到Authentication對象中。

          2.3 認證實現

          在前后端分離項目中,前端請求的是我們自己定義的認證接口。因為在認證成功以后就需要針對當前用戶生成token,Spring Security中提供的原始認證就無法實現了。在我們自定

          義的認證接口中,需要調用Spring Security的API借助于Spring Security實現認證。

          2.3.1 思路分析

          認證:

          1、自定義認證接口

          ① 調用ProviderManager的方法進行認證 如果認證通過生成jwt

          ② 把用戶信息存入redis中

          2、自定義UserDetailsService

          ① 在這個實現類中去查詢數據庫

          校驗

          1、定義Jwt認證過濾器

          ① 獲取token

          ② 解析token獲取其中的userid

          ③ 從redis中獲取用戶信息

          ④ 存入SecurityContextHolder

          2.3.2 集成Redis

          添加依賴

          <!--redis依賴-->
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-data-redis</artifactId>
          </dependency>

          添加redis配置

          在application.yml文件中添加Redis的相關配置

          spring:
            redis:
              host: 127.0.0.1
              port: 6379

          2.3.3 集成Mybatis Plus

          添加依賴

          <!-- 引入mybatis plus的依賴 -->
          <dependency>
              <groupId>com.baomidou</groupId>
              <artifactId>mybatis-plus-boot-starter</artifactId>
              <version>3.4.3</version>
          </dependency>
          
          <!-- 數據庫驅動 -->
          <dependency>
              <groupId>mysql</groupId>
              <artifactId>mysql-connector-java</artifactId>
          </dependency>
          
          <!-- lombok依賴包 -->
          <dependency>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
          </dependency>

          創建數據庫表

          CREATE TABLE `sys_user` (
            `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
            `user_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '用戶名',
            `nick_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '昵稱',
            `password` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '密碼',
            `status` CHAR(1) DEFAULT '0' COMMENT '賬號狀態(0正常 1停用)',
            `email` VARCHAR(64) DEFAULT NULL COMMENT '郵箱',
            `phone_number` VARCHAR(32) DEFAULT NULL COMMENT '手機號',
            `sex` CHAR(1) DEFAULT NULL COMMENT '用戶性別(0男,1女,2未知)',
            `avatar` VARCHAR(128) DEFAULT NULL COMMENT '頭像',
            `user_type` CHAR(1) NOT NULL DEFAULT '1' COMMENT '用戶類型(0管理員,1普通用戶)',
            `create_by` BIGINT(20) DEFAULT NULL COMMENT '創建人的用戶id',
            `create_time` DATETIME DEFAULT NULL COMMENT '創建時間',
            `update_by` BIGINT(20) DEFAULT NULL COMMENT '更新人',
            `update_time` DATETIME DEFAULT NULL COMMENT '更新時間',
            `del_flag` INT(11) DEFAULT '0' COMMENT '刪除標志(0代表未刪除,1代表已刪除)',
            PRIMARY KEY (`id`)
          ) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用戶表'
          
          -- 插入數據
          insert into security.sys_user (id, user_name, nick_name, password, status, email, phone_number, sex, avatar, user_type, create_by, create_time, update_by, update_time, del_flag) values (1501123580308578309, 'zhangsan', '張三', '1234', '0', 'hly@itcast.cn', '1312103105', '0', 'http://www.itcast.cn', '1', 1, '2022-03-08 09:12:06', 1, '2022-03-08 09:12:06', 0);

          數據庫相關配置

          spring:
            datasource:
              url: jdbc:mysql://localhost:3306/security?characterEncoding=utf-8&serverTimezone=UTC
              username: root
              password: 1234
              driver-class-name: com.mysql.cj.jdbc.Driver
          # mybatis plus的配置
          mybatis-plus:
            configuration:
              log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
            global-config:
              db-config:
                id-type: assign_id   

          User實體類

          @Data
          @TableName(value="sys_user")
          public class User {
          
              @TableId
              private Long id ;                         // 唯一標識
              private String userName ;                // 用戶名
              private String nickName ;                // 昵稱
              private String password ;                // 密碼
              private String status ;                  // 狀態 賬號狀態(0正常 1停用)
              private String email ;                   // 郵箱
              private String phoneNumber ;            // 電話號碼
              private String sex ;                     // 性別  用戶性別(0男,1女,2未知)
              private String avatar ;                  // 用戶頭像
              private String userType ;                // 用戶類型 (0管理員,1普通用戶)
              private Long createBy ;                  // 創建人
              private Date createTime ;                // 創建時間
              private Long updateBy ;                  // 更新人
              private Date updateTime ;                // 更新時間
              private Integer delFlag ;                // 是否刪除  (0代表未刪除,1代表已刪除)
              
          }

          UserMapper接口

          public interface UserMapper extends BaseMapper<User> { }

          啟動類

          @SpringBootApplication
          @MapperScan(basePackages="com.itheima.security.mapper")
          public class SecurityApplication {
          
              public static void main(String[] args) {
                  SpringApplication.run(SecurityApplication.class , args) ;
              }
          
          }

          2.3.4 集成Junit

          添加依賴

          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-test</artifactId>
          </dependency>

          編寫測試類

          @SpringBootTest(classes=SecurityApplication.class)
          public class SecurityApplicationTest {
          
              @Autowired
              private UserMapper userMapper ;
          
              @Test
              public void findAll() {
                  List<User> selectList=userMapper.selectList(new LambdaQueryWrapper<User>());
                  selectList.forEach( s -> System.out.println(s) );
              }
          
          }

          2.3.5 UserDetailsService

          在Spring Security的整個認證流程中會調用會調用UserDetailsService中的loadUserByUsername方法根據用戶名稱查詢用戶數據。默認情況下調用的是

          InMemoryUserDetailsManager中的方法,該UserDetailsService是從內存中獲取用戶的數據。現在我們需要從數據庫中獲取用戶的數據,那么此時就需要自定義一個

          UserDetailsService來覆蓋默認的配置。

          UserDetailsServiceImpl

          @Service
          public class UserDetailsServiceImpl implements UserDetailsService {
          
              @Autowired
              private UserMapper userMapper ;
          
              @Override
              public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
          
                  // 根據用戶名查詢用戶數據
                  LambdaQueryWrapper<User> lambdaQueryWrapper=Wrappers.<User>lambdaQuery().eq(User::getUserName ,username) ;
                  User user=userMapper.selectOne(lambdaQueryWrapper);
          
                  // 如果查詢不到數據,說明用戶名或者密碼錯誤,直接拋出異常
                  if(user==null) {
                      throw new RuntimeException("用戶名或者密碼錯誤") ;
                  }
          
                  // 將查詢到的對象轉換成Spring Security所需要的UserDetails對象
                  return new LoginUser(user);
          
              }
          
          }

          LoginUser

          package com.itheima.security.domain;
          
          import org.springframework.security.core.GrantedAuthority;
          import org.springframework.security.core.userdetails.UserDetails;
          
          import java.util.Collection;
          
          // 用來封裝數據庫查詢出來的用戶數據
          @Data
          @NoArgsConstructor
          @AllArgsConstructor
          public class LoginUser implements UserDetails {
          
              private User user ;
              
              @Override
              public Collection<? extends GrantedAuthority> getAuthorities() {
                  return null;
              }
          
              @Override
              public String getPassword() {
                  return user.getPassword();
              }
          
              @Override
              public String getUsername() {
                  return user.getUserName();
              }
          
              @Override
              public boolean isAccountNonExpired() {          // 賬號是否沒有過期
                  return true;
              }
          
              @Override
              public boolean isAccountNonLocked() {           // 賬號是否沒有被鎖定
                  return true;
              }
          
              @Override
              public boolean isCredentialsNonExpired() {      // 賬號的憑證是否沒有過期
                  return true;
              }
          
              @Override
              public boolean isEnabled() {                    // 賬號是否可用
                  return true;
              }
          }

          測試認證

          先通過Spring Security提供的默認登錄接口進行認證的測試,需要啟動Redis。此時控制臺會輸出如下錯誤:

          報錯的原因:默認情況下Spring Security在獲取到UserDetailsService返回的用戶信息以后,會調用PasswordEncoder中的matches方法進行校驗,但是此時在Spring容器中并不

          存在任何的PasswordEncoder的對象,因此無法完成校驗操作。

          解決方案:

          ① 使用明文認證

          要使用明文進行認證,就需要在密碼字段值的前面添加{noop}字樣!

          ② 配置加密算法

          2.3.6 配置加密算法

          一般情況下關于密碼在數據庫中都是密文存儲的,在進行認證的時候都是基于密文進行校驗。具體的實現步驟:

          1、使用指定的加密算法【BCrypt】對密碼進行加密處理,將加密以后的密文存儲到數據庫中

          2、在Spring容器中注入一個PasswordEncoder對象,一般情況下注入的就是:BCryptPasswordEncoder

          我們可以定義一個Spring Security的配置類,Spring Security要求這個配置類要繼承WebSecurityConfigurerAdapter

          @Configuration
          public class SpringSecurityConfigurer extends WebSecurityConfigurerAdapter {
          
              @Bean
              public BCryptPasswordEncoder bCryptPasswordEncoder() {
                  return new BCryptPasswordEncoder() ;
              }
          
          }

          測試:將數據庫的用戶密碼更改為使用BCryptPasswordEncoder加密以后的密文

          @SpringBootTest(classes=SecurityApplication.class)
          public class SecurityApplicationTest {
          
              @Autowired
              private PasswordEncoder passwordEncoder ;
          
              @Test
              public void testBcrypt() {
                  // 加密測試
                  String encode=passwordEncoder.encode("1234");
                  System.out.println(encode);
          
                  // 校驗測試
                  boolean matches=passwordEncoder.matches("1234", "$2a$10$ZqVB18PPA3P/MR9So/i8N.1UvVb.PblNl2sbj6pQJNDCgqiZqNQUm");
                  System.out.println(matches);
              }
          }

          2.3.7 登錄接口

          整體實現思路:

          ① 接下我們需要自定義登陸接口,然后讓Spring Security對這個接口放行,讓用戶訪問這個接口的時候不用登錄也能訪問。

          ② 在接口中我們通過AuthenticationManager的authenticate方法來進行用戶認證,所以需要在Security Config中配置把AuthenticationManager注入容器。

          ③ 認證成功的話要生成一個jwt,將jwt令牌進行返回。并且為了讓用戶下回請求時能通過jwt識別出具體的是哪個用戶,在返回之前,我們需要把用戶信息存入redis,可以把用戶id

          作為key。

          攔截規則配置

          在SpringSecurityConfigurer中重寫configure(HttpSecurity http)方法:

          // 配置Spring Security的攔截規則
          @Override
          protected void configure(HttpSecurity http) throws Exception {
              http
                      .csrf().disable()                                                               // 關閉csrf
                      .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)     // 指定session的創建策略,不使用session
                      .and()                                                                          // 再次獲取到HttpSecurity對象
                      .authorizeRequests()                                                            // 進行認證請求的配置
                      .antMatchers("/user/login").anonymous()                         				// 對于登錄接口,允許匿名訪問
                      .anyRequest().authenticated();                                                  // 除了上面的請求以外所有的請求全部需要認證
          }

          Spring容器注冊AuthenticationManager

          在SpringSecurityConfigurer中重寫authenticationManagerBean方法:

          登錄接口定義

          UserController

          @RestController
          @RequestMapping(value="/user")
          public class UserController {
          
              @Autowired
              private UserService userService ;
          
              @PostMapping(value="/login")
              public ResponseResult<Map> login(@RequestBody User user) {
                  return userService.login(user) ;
              }
          
          }

          ResponseResult

          @Data
          @NoArgsConstructor
          @AllArgsConstructor
          public class ResponseResult<T> {
          
              private Integer code ;
              private String msg ;
              private T data ;
          
          }

          UserService

          @Service
          public class UserServiceImpl implements UserService {
          
              @Autowired
              private AuthenticationManager authenticationManager ;
          
              @Autowired
              private RedisTemplate<String , String> redisTemplate ;
          
              @Override
              public ResponseResult<Map> login(User user) {
          
                  // 創建Authentication對象
                  UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(user.getUserName() , user.getPassword()) ;
          
                  // 調用AuthenticationManager的authenticate方法進行認證
                  Authentication authentication=authenticationManager.authenticate(authenticationToken);
                  if(authentication==null) {
                      throw new RuntimeException("用戶名或密碼錯誤");
                  }
          
                  // 將用戶的數據存儲到Redis中
                  LoginUser loginUser=(LoginUser) authentication.getPrincipal();
                  String userId=loginUser.getUser().getId().toString();
                  redisTemplate.boundValueOps("login_user:" + userId).set(JSON.toJSONString(loginUser));
          
                  // 生成JWT令牌并進行返回
                  Map<String , String> params=new HashMap<>() ;
                  params.put("userId" , userId) ;
                  String token=JwtUtils.getToken(params);
          
                  // 構建返回數據
                  Map<String , String> result=new HashMap<>();
                  result.put("token" , token) ;
                  return new ResponseResult<Map>(200 , "操作成功" , result);
          
              }
          
          }

          2.3.8 認證過濾器

          當用戶在訪問我們受保護的資源的時候,就需要校驗用戶是否已經登錄。我們需要自定義一個過濾器進行實現。

          過濾器內部的邏輯:

          1、獲取請求頭中的token,對token進行解析

          2、取出其中的userid

          3、使用userid去redis中獲取對應的LoginUser對象。

          4、然后封裝Authentication對象存入SecurityContextHolder

          5、放行

          注意:這個過濾器需要將其加入到Spring Security的過濾器鏈中

          認證過濾器:

          @Component
          public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
          
              @Autowired
              private RedisTemplate<String , String> redisTemplate ;
          
              @Override
              protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
          
                  // 1、從請求頭中獲取token,如果請求頭中不存在token,直接放行即可!由Spring Security的過濾器進行校驗!
                  String token=request.getHeader("token");
                  if(token==null || "".equals(token)) {
                      filterChain.doFilter(request , response);
                      return ;
                  }
          
                  // 2、對token進行解析,取出其中的userId
                  String userId=null ;
                  try {
                      Claims claims=JwtUtils.getClaims(token);
                      userId=claims.get("userId").toString();
                  }catch (Exception e) {
                      e.printStackTrace();
                      throw new RuntimeException("token非法") ;
                  }
          
                  // 3、使用userId從redis中查詢對應的LoginUser對象
                  String loginUserJson=redisTemplate.boundValueOps("login_user:" + userId).get();
                  LoginUser loginUser=JSON.parseObject(loginUserJson, LoginUser.class);
                  if(loginUser !=null) {
                      // 4、然后將查詢到的LoginUser對象的相關信息封裝到UsernamePasswordAuthenticationToken對象中,然后將該對象存儲到Security的上下文對象中
                      UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(loginUser, null , null) ;
                      SecurityContextHolder.getContext().setAuthentication(authenticationToken); 
                  }
                  
                  // 5、放行
                  filterChain.doFilter(request , response);
              }
          
          }

          配置過濾器:

          2.3.9 退出登錄

          我們只需要定義一個退出接口,然后獲取SecurityContextHolder中的認證信息,刪除redis中對應的數據即可。

          UserService添加退出登錄接口:

          @Override
          public ResponseResult logout() {
          
              // 獲取登錄的用戶信息
              LoginUser loginUser=(LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
              Long userId=loginUser.getUser().getId();
          
              // 刪除Redis中的用戶數據
              redisTemplate.delete("login_user:" + userId) ;
          
              // 返回
              return new ResponseResult(200 , "退出成功" , null) ;
          
          }

          3 Spring Security授權功能

          3.1 權限系統的作用

          權限系統作用:保證系統的安全性

          舉例:例如一個學校圖書館的管理系統,如果是普通學生登錄以后使用借書和還書的功能,不可能讓他具有添加書籍信息,刪除書籍信息等功能。但是如果是一個圖書館管理員的賬

          號登錄了,應該就能看到并使用添加書籍信息,刪除書籍信息等功能。總結起來就是不同的用戶可以使用不同的功能,這就是權限系統要去實現的效果。

          權限功能的實現我們不能只依賴前端去根據用戶的權限來選擇顯示哪些菜單、哪些按鈕。因為如果有人知道了對應功能的接口地址就可以不通過前端,直接去發送請求來實現相關功

          能操作。所以我們還需要在后臺進行用戶權限的判斷,判斷當前用戶是否有相應的權限,必須具有所需權限才能進行相應的操作。

          3.2 授權基本流程

          在Spring Security中,會使用默認的FilterSecurityInterceptor來進行權限校驗。在FilterSecurityInterceptor中會從SecurityContextHolder獲取其中的Authentication,然后

          獲取其中的權限信息。當前用戶是否擁有訪問當前資源所需的權限。所以我們在項目中只需要把當前登錄用戶的權限信息也存入Authentication。然后設置我們的資源所需要的權限

          即可。

          3.3 入門案例

          3.3.1 資源添加所需權限

          Spring Security為我們提供了基于注解的權限控制方案,這也是我們項目中主要采用的方式。我們可以使用注解去指定訪問對應的資源所需的權限。但是要使用它我們需要先開啟

          相關配置。

          開啟權限配置功能

          在啟動類上添加@EnableGlobalMethodSecurity(prePostEnabled=true)

          方法添加所需權限

          不給用戶添加任何權限信息進行測試,返回信息為:

          {
              "timestamp": "2022-07-04T06:31:47.821+00:00",
              "status": 403,
              "error": "Forbidden",
              "path": "/hello"
          }

          3.3.2 用戶添加所擁有的權限

          UserDetailsServiceImpl

          在UserDetailsServiceImpl中構建測試的權限數據,并將其設置給LoginUser對象:

          LoginUser

          LoginUser接收權限數據,并且對getAuthorities方法進行改造,返回Spring Security所需要的權限對象:

          JwtAuthenticationTokenFilter

          在JWT過濾器中需要從Redis中獲取LoginUser對象,在構建UsernamePasswordAuthenticationToken對象的時候,為其設置權限數據:

          3.4 從數據庫查詢權限信息

          3.4.1 RBAC權限模型

          RBAC權限模型(Role-Based Access Control)即:基于角色的權限控制。這是目前最常被開發者使用也是相對易用、通用權限模型。

          3.4.2 環境準備

          數據庫環境準備

          權限表(菜單表):

          CREATE TABLE `sys_menu` (
            `id` bigint(20) NOT NULL AUTO_INCREMENT,
            `menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜單名',
            `path` varchar(200) DEFAULT NULL COMMENT '路由地址',
            `component` varchar(255) DEFAULT NULL COMMENT '組件路徑',
            `visible` char(1) DEFAULT '0' COMMENT '菜單狀態(0顯示 1隱藏)',
            `status` char(1) DEFAULT '0' COMMENT '菜單狀態(0正常 1停用)',
            `perms` varchar(100) DEFAULT NULL COMMENT '權限標識',
            `icon` varchar(100) DEFAULT '#' COMMENT '菜單圖標',
            `create_by` bigint(20) DEFAULT NULL,
            `create_time` datetime DEFAULT NULL,
            `update_by` bigint(20) DEFAULT NULL,
            `update_time` datetime DEFAULT NULL,
            `del_flag` int(11) DEFAULT '0' COMMENT '是否刪除(0未刪除 1已刪除)',
            `remark` varchar(500) DEFAULT NULL COMMENT '備注',
            PRIMARY KEY (`id`)
          ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜單表';
          
          # 插入基礎數據
          insert into security.sys_menu (id, menu_name, path, component, visible, status, perms, icon, create_by, create_time, update_by, update_time, del_flag, remark) values (1543917775762886657, '添加用戶', '/user/addUser', 'addUser', '0', '0', 'system:user:add', 'icon-add', 1, '2022-07-04 11:20:57', 1, '2022-07-04 11:20:57', 0, '添加用戶按鈕');
          insert into security.sys_menu (id, menu_name, path, component, visible, status, perms, icon, create_by, create_time, update_by, update_time, del_flag, remark) values (1543918065589379073, '查看用戶列表', '/user/userList', 'userList', '0', '0', 'system:user:list', 'icon-list', 1, '2022-07-04 11:22:06', 1, '2022-07-04 11:22:06', 0, '查看用戶列表用戶按鈕');

          角色表:

          CREATE TABLE `sys_role` (
            `id` bigint(20) NOT NULL AUTO_INCREMENT,
            `name` varchar(128) DEFAULT NULL,
            `role_key` varchar(100) DEFAULT NULL COMMENT '角色權限字符串',
            `status` char(1) DEFAULT '0' COMMENT '角色狀態(0正常 1停用)',
            `del_flag` int(1) DEFAULT '0' COMMENT 'del_flag',
            `create_by` bigint(200) DEFAULT NULL,
            `create_time` datetime DEFAULT NULL,
            `update_by` bigint(200) DEFAULT NULL,
            `update_time` datetime DEFAULT NULL,
            `remark` varchar(500) DEFAULT NULL COMMENT '備注',
            PRIMARY KEY (`id`)
          ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
          
          # 插入測試數據
          insert into security.sys_role (id, name, role_key, status, del_flag, create_by, create_time, update_by, update_time, remark) values (1, '系統管理員', 'admin', '0', 0, 1, '2022-07-04 19:25:06', 1, '2022-07-04 19:25:19', '系統管理員');
          insert into security.sys_role (id, name, role_key, status, del_flag, create_by, create_time, update_by, update_time, remark) values (2, '普通用戶', 'user', '0', 0, 1, '2022-07-04 19:25:48', 1, '2022-07-04 19:25:52', '普通用戶角色');

          角色菜單中間表:

          CREATE TABLE `sys_role_menu` (
            `role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
            `menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜單id',
            PRIMARY KEY (`role_id`,`menu_id`)
          ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
          
          # 插入基礎測試數據
          insert into security.sys_role_menu (role_id, menu_id) values (1, 1543917775762886657);
          insert into security.sys_role_menu (role_id, menu_id) values (1, 1543918065589379073);
          insert into security.sys_role_menu (role_id, menu_id) values (2, 1543918065589379073);

          用戶表:

          CREATE TABLE `sys_user` (
            `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
            `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用戶名',
            `nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵稱',
            `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密碼',
            `status` char(1) DEFAULT '0' COMMENT '賬號狀態(0正常 1停用)',
            `email` varchar(64) DEFAULT NULL COMMENT '郵箱',
            `phone_number` varchar(32) DEFAULT NULL COMMENT '手機號',
            `sex` char(1) DEFAULT NULL COMMENT '用戶性別(0男,1女,2未知)',
            `avatar` varchar(128) DEFAULT NULL COMMENT '頭像',
            `user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用戶類型(0管理員,1普通用戶)',
            `create_by` bigint(20) DEFAULT NULL COMMENT '創建人的用戶id',
            `create_time` datetime DEFAULT NULL COMMENT '創建時間',
            `update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
            `update_time` datetime DEFAULT NULL COMMENT '更新時間',
            `del_flag` int(11) DEFAULT '0' COMMENT '刪除標志(0代表未刪除,1代表已刪除)',
            PRIMARY KEY (`id`)
          ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用戶表';
          
          # 插入測試數據
          insert into security.sys_user (id, user_name, nick_name, password, status, email, phone_number, sex, avatar, user_type, create_by, create_time, update_by, update_time, del_flag) values (1501123580308578309, 'zhangsan', '張三', '$2a$10$ZqVB18PPA3P/MR9So/i8N.1UvVb.PblNl2sbj6pQJNDCgqiZqNQUm', '0', 'hly@itcast.cn', '1312103105', '0', 'http://www.itcast.cn', '1', 1, '2022-03-08 09:12:06', 1, '2022-03-08 09:12:06', 0);
          insert into security.sys_user (id, user_name, nick_name, password, status, email, phone_number, sex, avatar, user_type, create_by, create_time, update_by, update_time, del_flag) values (1501123580308578310, 'admin', '系統管理員', '$2a$10$ZqVB18PPA3P/MR9So/i8N.1UvVb.PblNl2sbj6pQJNDCgqiZqNQUm', '0', 'hly@itcast.cn', '1312103105', '0', 'http://www.itcast.cn', '1', 1, '2022-03-08 09:12:06', 1, '2022-03-08 09:12:06', 0);

          用戶角色中間表:

          CREATE TABLE `sys_user_role` (
            `user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用戶id',
            `role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id',
            PRIMARY KEY (`user_id`,`role_id`)
          ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
          
          # 插入基礎數據
          insert into security.sys_user_role (user_id, role_id) values (1501123580308578309, 2);
          insert into security.sys_user_role (user_id, role_id) values (1501123580308578310, 1);

          SQL測試查詢某一個用戶所具有的權限:

          SELECT distinct m.perms FROM sys_user u
              left join sys_user_role ur on ur.user_id=u.id
              left join sys_role_menu rm on rm.role_id=ur.role_id
              left join sys_menu m on m.id=rm.menu_id
          WHERE u.id=1501123580308578310 ;

          Menu實體類

          // 菜單表(Menu)實體類
          @TableName(value="sys_menu")
          @Data
          @AllArgsConstructor
          @NoArgsConstructor
          public class Menu {
          
              @TableId
              private Long id;
              private String menuName;        // 菜單名
              private String path;            // 路由地址
              private String component;       // 組件路徑
              private String visible;         // 菜單狀態(0顯示 1隱藏)
              private String status;          // 菜單狀態(0正常 1停用)
              private String perms;           // 權限標識
              private String icon;            // 菜單圖標
              private Long createBy;          // 創建人
              private Date createTime;        // 創建時間
              private Long updateBy;          // 更新人
              private Date updateTime;        // 更新時間
              private Integer delFlag;        // 是否刪除(0未刪除 1已刪除)
              private String remark;          // 備注
              
          }

          MenuMapper接口

          // 操作菜單表的Mapper接口
          public interface MenuMapper extends BaseMapper<Menu> {
          
              // 查詢某一個用戶的權限信息
              public abstract List<String> findUserMenuById(Long userId) ;
          
          }

          application.yml修改

          MenuMapper.xml映射文件

          <?xml version="1.0" encoding="UTF-8" ?>
          <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
          <mapper namespace="com.itheima.security.mapper.MenuMapper">
          
              <select id="findUserMenuById" resultType="java.lang.String">
                  SELECT distinct m.perms FROM sys_user u
                       left join sys_user_role ur on ur.user_id=u.id
                       left join sys_role_menu rm on rm.role_id=ur.role_id
                       left join sys_menu m on m.id=rm.menu_id
                  WHERE u.id=#{userId} ;
              </select>
          
          </mapper>

          3.4.3 UserDetailsService修改

          從數據庫中查詢該用戶的真實權限信息:

          4 自定義失敗處理

          4.1 實現思路

          我們還希望在認證失敗或者是授權失敗的情況下也能和我們的接口一樣返回相同結構的json,這樣可以讓前端能對響應進行統一的處理。要實現這個功能我們需要知道

          SpringSecurity的異常處理機制。

          在SpringSecurity中,如果我們在認證或者授權的過程中出現了異常會被ExceptionTranslationFilter捕獲到。在ExceptionTranslationFilter中會去判斷是認證失敗還是授權失敗出

          現的異常。

          ① 如果是認證過程中出現的異常會被封裝成AuthenticationException然后調用AuthenticationEntryPoint對象的方法去進行異常處理。

          ② 如果是授權過程中出現的異常會被封裝成AccessDeniedException然后調用AccessDeniedHandler對象的方法去進行異常處理。

          所以如果我們需要自定義異常處理,我們只需要自定義AuthenticationEntryPoint和AccessDeniedHandler然后配置給Spring Security即可。

          4.5 代碼實現

          4.5.1 認證失敗處理器

          @Component
          public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
          
              @Override
              public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                  ResponseResult result=new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "認證失敗請重新登錄", null);
                  String json=JSON.toJSONString(result) ;
                  WebUtils.renderString(response,json);
              }
          
          }

          4.5.2 授權失敗處理器

          @Component
          public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
          
              @Override
              public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
          
                  ResponseResult result=new ResponseResult(HttpStatus.FORBIDDEN.value(), "權限不足" , null);
                  String json=JSON.toJSONString(result);
                  WebUtils.renderString(response,json);
          
              }
          
          }

          4.5.3 Spring Security配置處理器

          實現步驟:

          1、先注入對應的處理器

          2、使用HttpSecurity對象的方法去配置

          5 跨域處理

          5.1 跨域說明

          瀏覽器出于安全的考慮,使用 XMLHttpRequest對象發起 HTTP請求時必須遵守同源策略,否則就是跨域的HTTP請求,默認情況下是被禁止的。

          同源策略要求源相同才能正常進行通信,所謂的源相同指定是:協議、域名、端口號都完全一致。

          前后端分離項目,前端項目和后端項目一般都不是同源的,所以肯定會存在跨域請求的問題。

          所以我們就要處理一下,讓前端能進行跨域請求。

          5.2 解決方案

          5.2.1 Spring Boot項目添加跨域請求配置

          @Configuration
          public class CorsConfig implements WebMvcConfigurer {
          
              @Override
              public void addCorsMappings(CorsRegistry registry) {
                // 設置允許跨域的路徑
                  registry.addMapping("/**")
                          // 設置允許跨域請求的域名
                          .allowedOriginPatterns("*")
                          // 是否允許cookie
                          .allowCredentials(true)
                          // 設置允許的請求方式
                          .allowedMethods("GET", "POST", "DELETE", "PUT")
                          // 設置允許的header屬性
                          .allowedHeaders("*")
                          // 跨域允許時間
                          .maxAge(3600);
              }
          }

          5.2.2 Spring Security開啟跨域訪問支持

          由于我們的資源都會收到Spring Security的保護,所以想要跨域訪問還要讓Spring Security運行跨域訪問。

          //SpringSecurityConfigurer#configure 允許跨域
          http.cors();

          6 其他問題說明

          6.1 其他權限校驗方式

          我們前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法進行校驗。Spring Security還為我們提供了其它方法例如:hasAnyAuthority,hasRole,

          hasAnyRole等。

          6.1.2 hasAnyAuthority

          hasAnyAuthority方法可以傳入多個權限,只有用戶有其中任意一個權限都可以訪問對應資源。

          6.1.3 hasRole

          hasRole要求有對應的角色才可以訪問,但是它內部會把我們傳入的參數拼接上 ROLE_ 后再去比較。所以這種情況下要用用戶對應的權限也要有 ROLE_ 這個前綴才可以。

          6.1.4 hasAnyRole

          hasAnyRole 有任意的角色就可以訪問。它內部也會把我們傳入的參數拼接上 ROLE_ 后再去比較。所以這種情況下要用用戶對應的權限也要有 ROLE_ 這個前綴才可以。

          6.2 基于配置的權限控制

          我們也可以在配置類中使用使用配置的方式對資源進行權限控制。

          注意: 如果此時在方法上使用了@PreAuthorize(value="hasAuthority('system:user:add')")指定了權限信息,那么就需要用于同時擁有兩個權限才可以進行訪問。

          6.3 CSRF

          CSRF是指跨站請求偽造(Cross-site request forgery),是web常見的攻擊之一。https://blog.csdn.net/freeking101/article/details/86537087

          Spring Security去防止CSRF攻擊的方式就是通過csrf_token。后端會生成一個csrf_token,前端發起請求的時候需要攜帶這個csrf_token,后端會有過濾器進行校驗,如果沒有攜

          帶或者是偽造的就不允許訪問。

          我們可以發現CSRF攻擊依靠的是cookie中所攜帶的認證信息。但是在前后端分離的項目中我們的認證信息其實是token,而token并不是存儲在cookie中,并且需要前端代碼去把

          token設置到請求頭中才可以,所以CSRF攻擊也就不用擔心了。

          7 總結

          本文章給大家介紹了一下在前后端分離項目中如何使用Spring Security完成認證和授權的相關操作,并且介紹一下如何自定義認證和授權失敗的處理器,以及如何解決跨域的相關

          問題。大家可以參考本文章實際操作一下,相信大家很快就可以掌握Spring Security在前后端分離項目中的使用。

          論是“雙十一”還是“6.18”,近幾年各大電商平臺造出了一個又一個購物節,期間還真有不少物買價廉的家電產品,不過還是需要我們具有一雙善于發現的慧眼,一不小心就可能和優惠錯過哦!今天筆者就為大家盤點一下,那些6.18真正值得買的家電產品!

          又一電商盛宴!6.18家電產品選購全攻略

          1.買一送一 三星空氣凈化器KJ720F-K7586WF

          作為三星旗下凈化器的旗艦款產品,三星KJ720F-K7586WF空氣凈化器擁有五重過濾技術,能夠有效過濾空氣中漂浮的毛發、甲醛和微塵,實現客廳空氣的整體凈化。目前這款產品京東售價9990元,買還送三星凈化器一臺,需要的朋友不妨關注下。

          多重濾網 層層凈化

          三星專利的超凈3離子群除菌技術,能夠有效去除空氣中的細菌,讓家人免遭各種細菌的侵害,營造健康潔凈的空氣環境。這款凈化器還配備PM2.5/PM10/氣味三個傳感器并獨立顯示,肉眼看不到的空氣質量用數值以及指示燈直觀顯示,凈化效果一目了然。

          三星空氣凈化器KJ720F-K7586WF京東售價9990元

          和普通凈化器不同的是,這款產品內部設有靜電發生裝置,能夠使灰塵顆粒物帶電而更易被集塵濾網規律性吸附,從而大大提高顆粒物CCM,延長濾網的使用壽命。相較濾網直接吸附,能夠提升約2倍濾網壽命。濾網壽命提醒方面,這款產品通過光折射率判斷濾網的污染程度,并計算剩余壽命,能夠更準確提示濾網的壽命。

          2.直降500元 戴森V6無線吸塵器

          當我們大掃除完成后,看著煥然一新的房間是不是心情頓時舒暢起來?然而打掃完后看似整潔的房間其實并不干凈,空氣中還有很多肉眼不可見的顆粒污染物,嚴重威脅家人的健康,尤其是過敏體質者,更容易受到感染。戴森V6 Fluffy手持無線吸塵器,使用最新的整機HEPA濾網,全面清潔的同時杜絕二次污染,為家人的健康保駕護航。6.18期間這款吸塵器直降500元,僅售4190元。

          戴森V6 Fluffy手持吸塵器僅售4190元

          排出的空氣比你呼吸的空氣更干凈

          傳統的吸塵器由于密封不嚴等問題,在工作過程中會排除有害過敏原,對家庭環境造成二次污染,嚴重威脅家人健康。戴森V6整機配備HEPA過濾系統,能吸附超過99.9%的小至PM0.3的微塵,后置馬達配備濾網及密封系統,有效防止灰塵逸出,杜絕二次污染,確保空氣潔凈。

          排出的空氣比你呼吸的空氣更潔凈

          整機HEPA過濾系統

          強大吸力,不懼頑固塵垢

          普通吸塵器吸力不夠,面對頑固灰塵束手無策?戴森V6配備第六代數碼馬達,憑借其獨特的2 Tier Redial雙層放射式氣旋技術以及鎳鈷錳鋰電池科技,15個內置氣旋同時運作,每分鐘旋轉高達110000次,可提供長達20分鐘的強勁吸力,面對“強敵”,游刃有余。無論是硬質地板還是天花板上的灰塵,配備可拆式長管及吸頭都可輕松搞定。

          內置2 Tier Radial cyclones 雙層放射式氣旋

          軟絨滾筒刷頭,輕松應對加大塵屑

          軟絨滾筒吸頭,輕松應對較大塵屑

          軟絨滾筒吸頭的設計,取代傳統有線吸塵器上的刷毛,能夠覆蓋整個清潔滾筒,可以同時吸除硬質地板上的殘屑及微塵,只需一次即可潔凈如新。滾筒內直驅式馬達的設計,可以做到全方位、直達邊緣的清潔,不留衛生死角,全面呵護家人健康。

          3.客廳必備:酷開50寸智能電視

          雖然電視的娛樂地位被手機和平板分割,但一個家最少不了的還是一臺大尺寸的電視。創維酷開K50J 50寸智能WIFI電視,因其不僅具有1080p全高清的50英寸液晶顯示屏,同時還采用了杜比專業級音效解碼,更通過內置的電視派整合了愛奇藝,優酷等視頻平臺的視頻資源,保障用戶看片找片沒煩惱。6.18提前預約搶購價,僅售1799元。

          酷開K50J智能電視

          酷開K50J智能電視

          酷開 K50J
          產品定位全高清電視,LED電視,網絡電視,智能電視
          屏幕尺寸50英寸
          分辨率1080P(1920*1080)
          屏幕比例16:9
          面板類型群創面板
          背光燈類型LED發光二極管
          最佳觀看距離4.1-5.0米
          對比度1200:1
          音效系統環繞音效
          輸出功率8W×2
          揚聲器2個揚聲器
          音效特點杜比解碼:支持
          網絡功能有線/WiFi,2.4GHz 單頻 WiFi
          USB媒體播放USB支持視頻格式:MVC,RMVB,RM,AVI,VOB,MPG,MKV等,視頻硬解碼:支持H.264、H.265硬解碼
          USB支持音頻格式:MP3,WMA等
          USB支持圖片格式:JPG,JPEG,BMP,GIF
          HDMI接口2*HDMI
          網絡接口1×網絡接口
          USB接口2×USB接口
          其他接口 1×音視頻接口(組)
          電源性能220V
          產品功耗100W
          待機功耗
          能效等級3級能效
          CPU四核CortexA7
          GPU四核Mali450
          RAM1GB 雙通道 DDR3
          ROM4GB eMMC高速閃存
          操作系統酷開系統
          遙控器支持
          外觀設計機身:超薄機身21.5mm
          邊框:極窄邊框12.7mm
          機身尺寸含底座:1125×262×670mm
          不含底座:1125×73×652mm
          機身重量含底座:13.2kg
          不含底座:12.8kg
          其它性能WiFi熱點,Airplay,Miracast
          包裝清單 電視機身 x1
          遙控器 x1
          快速使用指南 x1
          保修卡 x1
          保修政策 全國聯保,享受三包服務
          質保時間 1年
          質保備注 整機1年,主要部件2年
          客服電話 9510-5555
          電話備注 周一至周日8:30-22:00
          詳細內容 酷開TV官網嚴格按照國家三包政策,針對所售商品履行保修、換貨和退貨的義務,產品質量問題退換貨發生的來回運費由酷開TV官網承擔,非質量問題退換貨發生的來回運費均由用戶承擔。請客戶妥善保管好保修卡和購機發票,這些將是您的保修憑證。用戶可與酷開TV官網的客服中心聯系辦理退換貨事宜(撥打酷開TV官網客戶服務熱線:95105555)。進入官網>>

          酷開K50J智能電視詳細參數

          編輯點評:酷開K50J屏幕采用1920X1080高清分辨率,屏幕尺寸達到了50寸,支持無線wifi鏈接,內置ARM A7架構四核CPU和Mali-450四核GPU,同時還具有1GB雙通道DDR3內存,各種應用和游戲均可流暢運行。該電視還搭載了快速易用的酷開系統。不僅具有人性化的交互界面,同時還具有毫秒級快速響應能力,各種任務之間可以極速切換,帶來流暢的操作體驗。

          4.滿1500減200 松下三門冰箱空前低價

          這款外型時尚帶有獨立保鮮功能的冰箱——松下NR-C25EP2-S三門冰箱,它擁有獨立的保鮮室,兩檔溫度區間,分類保存,持久保鮮,相當實用。目前,這款松下冰箱在京東商城僅售2599元,且每滿1500減200,感興趣的朋友可以關注一下!

          松下NR-C25EP2-S三門冰箱

          該機通體白色,銀裝素裹,硬朗的機身線條顯得十分剛毅,隱藏式的把手嵌于門上,美觀優雅,便捷易用。機器的寬度僅為56.1cm,體積適中,易于安放,不大的機身卻又著245升的超大容量,再多食材也可一次收攬,足夠全家人享用。

          中門獨立保鮮室 保鮮持久

          內置松下原裝壓縮機,性能勁猛,動力強勁,極其省電,國家一級能效認證,同時振動更弱,噪音更小,即便正常運行也幾乎察覺不到它的存在,融入靜謐的家居環境中。中門特色的獨立保鮮室,具有兩檔溫度區間可調,不論是蔬菜水果飲料,還是肉類海鮮奶酪,均可輕松放入,持久保鮮,讓您隨時隨地即刻享受可口美味。

          細節設計彰顯大廠風采

          諸多的細節設計彰顯出松下對產品的用心:門自鎖系統,開合自如不反彈;氣囊式門封條,吸附力強,保溫效果顯著;鋼化玻璃擱板,美觀耐用;LED冷光源,亮度更高,照明范圍更廣闊。

          原裝壓縮機 節能靜音

          編輯點評:松下專注冰箱領域已經60余年,有著業界領先的研發實力和技術積累,其生產的壓縮機更是品質優異,質量可靠,松下的這款冰箱亦是擁有眾多優勢:簡約時尚的外型,獨立保鮮室的加入,性能強能耗低,省電靜音,還有許多人性化的細節設計等等,喜歡的朋友不要錯過了!

          5.成交價1699 TCL7公斤滾筒洗衣機

          一說到洗衣機你腦中浮現出怎樣的畫面?白色的四方盒子又笨又重噪音還挺大,也難怪,受制于技術的限制,傳統的機器基本都是那副模樣,可今天,筆者為大家帶來一款別具一格的新產品——TCL XQG70-F12102THB洗衣機,相信它的出現定會讓你眼前一亮。小巧的整體設計,一襲乳白色的外衣覆蓋表面,硬朗的機身線條剛正沉穩,正面圓形的大眼睛深邃而靈犀,炯炯有神。它不像是一臺洗衣機,倒仿佛是一個鮮活的小生命,朝氣而又不失優雅。

          TCL XQG70-F12102THB洗衣機【點此購買】

          該機不但擁有出色的機身外觀設計,內在的強大性能更是讓人為之贊嘆。搭載的源自德國設計CIM交流變頻電機,能效強勁,動力澎湃,同時振動更弱,噪音更小,節能減耗,壽命長久。

          變頻電機 性能強勁

          7公斤的大容量,再多的衣物也可一次洗凈,省心省力。中途加衣的貼心設計徹底解決漏洗煩惱,拯救一天好心情。創新式的采用了內凸式蜂巢水晶內筒,極大的降低了對衣物的磨損,精心呵護每一件衣服。大面積的顯示操控面板,字跡清晰一目了然,操作便捷,簡單上手。該機內置16種洗滌程序,面對多種衣物也從容不迫,讓洗衣真正做到了因地制宜。

          內凸式水晶內筒 呵護衣物

          模糊稱重設計的加入,它可以根據衣物的多少智能選擇合適的電量與水量,精準把控,為您省電更是省錢。1200轉的超高轉速,甩脫更干凈徹底,減少晾曬時間。90度高溫自潔功能有效清潔內筒,保持內筒干凈衛生,杜絕二次污染,全方位呵護您和家人的身體健康。

          眾多人性化的細節設計

          TCL的這款洗衣機是一款內外兼修的好產品,它有著出眾的外觀設計,強勁的洗滌效能,創新的內筒不傷衣物,大面積的顯示面板便捷易用,眾多實用貼心的功能和設計讓洗衣變得簡單有趣,使用隨心自如。性價比突出,喜歡的朋友不要錯過了哦!

          6.科龍KFR-50LW/EFVDN2z柜式空調

          這款科龍KFR-50LW/EFVDN2z柜式空調,有著高挑的機身和圓潤的造型,金色鋼化玻璃滑動面板鑲嵌其中,渾然天成。外觀優秀,其性能也十分強勁,冬天電輔制熱,三分鐘即可暖房,讓您提前溫暖愜意;夏天制冷迅速,僅需一分鐘讓您倍感涼爽舒適,無需等待。智能化的遠程控制,無論你在何處,空調狀態盡收眼底,運行狀況一目了然,并且可以實時操控,簡單便捷。搭載的高效品質壓縮機,性能強勁,動力十足,制溫速,噪音小,節能減耗。目前,這款空調在京東商城售價3999元,喜歡的朋友可以關注一下!

          科龍KFR-50LW/EFVDN2z柜式空調

          時尚優雅的外觀設計

          迅捷的制溫效果

          編輯點評:其實空調的角色,早已從基本的功能性制溫,發展到居家裝飾的效果,這款產品無疑是一個很好的解決方案,它擁有高貴典雅的外觀設計,強勁的性能制溫迅速,人性化的觸控操作簡單便捷,除甲醛技術呵護健康等等,它各方面素質表現都很優秀,目前京東商城售價3999元,喜歡的朋友不要錯過哦!

          7.方太HA21BE燃氣灶

          俗話說:民以食為天。想要做出美味可口的三餐,一款好爐具必不可少,接下來筆者今天就為大家奉上一款實用、健康的好產品——方太HA21BE燃氣灶。它采用三層防爆玻璃黑色面板,一體化機身,沉穩大氣,便于清潔。超大的爐頭,強勁火力,形成有效燃燒區域,提升熱效率。防漏氣、防熄火、防滲水、防內火的四防設計,重重防護,令烹飪無后顧之憂。獨有的精準火力控制能在大火爆炒時鎖住食物維生素不流失,在小火慢燉時充分釋放營養成分。目前,這款燃氣灶在京東商城僅售2780元,值得選購!

          方太HA21BE燃氣灶

          大氣沉穩的外觀設計

          直噴技術 火力強勁

          編輯點評:沉穩大氣的造型,安全的防護,勁猛的火力,簡單便捷的操控和貼心的細節設計,都是這款產品的優勢,目前京東商城售價2780元,極具性價比,喜歡的美食的朋友不要錯過哦!

          8.博瑞客U700家用加濕器

          目前市面上加濕器多數為國產代工產品,某些品牌憑借線下知名度,為了節省生產研發成本尋找一些代加工廠貼牌生產,只為低價,只為銷量和銷售額而全然不顧產品本質的提升,顧客使用體驗是否優越。只有高質量的產品才能保障家人的健康,而博瑞客U700正是這樣一款產品,為家人健康保駕護航。

          博瑞客U700大容量加濕器

          超聲波系列加濕器,加濕是通過震蕩片將水打碎然后通過風力系統吹入空氣當中,在將水霧化的過程中水中的礦物質也一同被分離并進入空氣當中-簡稱白粉,對人體呼吸系統有產生影響。BONECO品牌設計的超聲波加濕器系列為了解決這一問題,絕大多數安裝了去礦盒,可以有效去除水中礦物質,U700 也不例外裝備最新款去礦盒(去礦粒為黑色老款為白色),真正的健康加濕~

          博瑞客U700大容量加濕器

          這款產品還內置加熱功能,可手動打開和關閉,可以加熱噴出的霧氣,溫度大約在35℃左右,室內溫度會在一個十分舒適的程度,加熱到最佳程度僅需幾分鐘時間。

          博瑞客U700大容量加濕器

          嚴重的空氣污染讓人們對空氣凈化的需求越來越強烈,但單純的凈化并不能帶來持久的健康,由于空氣凈化器在工作中會帶走空氣中的水分,因此會使空氣更加干燥。博瑞客U700大容量加濕器采用智能環境監控技術,支持冷熱雙模式,可自動清洗,是一款相當不錯的產品。

          位玩家大家好,歡迎大家收看今天的游民晨播報,我是優格。今日要聞有:分析師稱XGP收入將暴漲;制作人稱《MVC》系列或有新作;《2077》制作人稱游戲中仍有大量彩蛋未被發現。我們將在隨后為你帶來以上新聞的詳細信息,敬請關注。

          重點關注:

          1分析師稱XGP收入將暴漲

          Ampere Analysis的研究主管Piers Harding-Rolls近日對XGP的漲價進行了分析,他表示,到2025 年,Game Pass的收入預計將上升到驚人的51億美元。與2022年相比,Microsoft的收入將增長200%以上,而分析師稱,Game Pass Ultimate的價格上漲將成為這一增長的催化劑。

          分析師表示,到2025年,消費者在該服務上的支出將增長高達15.3%。預計今年將小幅增長5.4%。

          2制作人稱《MVC》系列或有新作

          《漫威vs卡普空戰斗合集》已于近日登陸Switch、PS4以及PC平臺,而目前該系列的制作人松木修平在采訪中表示:“我們的開發團隊有著遠大的夢想。也許未來有機會推出一款新的《漫威vs卡普空》游戲。”

          松本修平承認,任何重振該系列的計劃都需要時間去執行。不過,工作室目前正集中精力做力所能及的事情。這也是為什么最近公布的系列作品會引起大家關注的原因。

          3《2077》制作人稱游戲中仍有大量彩蛋未被發現

          《賽博朋克2077:往日之影》制作人Pawel Sasko近日在播客中被問及:本體游戲和DLC中是否還有玩家未發現的彩蛋存在?他表示:我非常確定是有的。

          Sasko補充說:“我現在就知道幾個我之前從未發現過的例子,但玩家需要花費很長的時間去找。我們在《巫師3》中隱藏的一些彩蛋和細節是在發售七年后才被發現的,是在我們推出完整版之后。直到那時,人們才發現了一些東西。我們新加入了一些東西,但有些東西是‘一直’存在的。”

          以上就是今天的晨報內容,我們會在每天的早晨,以快訊的形式為大家播報昨日深夜至今日凌晨國內外發生的重要資訊。我是優格,祝愿大家身體健康、工作順利,我們明天再見。


          主站蜘蛛池模板: 久99精品视频在线观看婷亚洲片国产一区一级在线 | 在线观看视频一区二区| 亚洲区精品久久一区二区三区| 成人毛片无码一区二区| 久久99热狠狠色精品一区 | 欧洲精品码一区二区三区| 亚洲成AV人片一区二区| 国产成人久久精品麻豆一区| 成人区人妻精品一区二区不卡| 无码精品一区二区三区免费视频 | 无码乱码av天堂一区二区| 一区二区三区四区在线播放| 国精产品999一区二区三区有限| 一区二区三区视频在线| 另类一区二区三区| 一本大道东京热无码一区 | 亚洲国产综合精品中文第一区| 国产精品一区二区三区免费| 日韩成人一区ftp在线播放| 一区二区三区在线观看| 亚洲国产精品一区二区三区在线观看 | 无码国产精品一区二区免费3p| 成人区精品一区二区不卡| 亚洲高清偷拍一区二区三区| 国产一区在线播放| 中文字幕一区二区三区久久网站 | 中文字幕久久亚洲一区| 亚洲国产专区一区| 久久久久人妻一区精品果冻| 久久久久人妻一区精品| 日韩人妻无码一区二区三区 | 国产伦精品一区二区三区免.费 | 精品无码av一区二区三区| 亚洲一区二区三区在线观看精品中文| 亚洲爽爽一区二区三区| 日韩人妻无码一区二区三区久久99 | 亚洲中文字幕无码一区二区三区| 中文字幕一区在线| 亚洲爆乳无码一区二区三区| 日本视频一区在线观看免费| 亚洲一区二区三区丝袜|