整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

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》中隐藏的一些彩蛋和细节是在发售七年后才被发现的,是在我们推出完整版之后。直到那时,人们才发现了一些东西。我们新加入了一些东西,但有些东西是‘一直’存在的。”

以上就是今天的晨报内容,我们会在每天的早晨,以快讯的形式为大家播报昨日深夜至今日凌晨国内外发生的重要资讯。我是优格,祝愿大家身体健康、工作顺利,我们明天再见。