前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >SpringSecurity入坑(五)

SpringSecurity入坑(五)

作者头像
是小张啊喂
发布于 2021-08-09 09:36:56
发布于 2021-08-09 09:36:56
89500
代码可运行
举报
文章被收录于专栏:软件软件
运行总次数:0
代码可运行

如何自定义实现验证?

基于SpringSecurity做基本权限验证,在之前都写的差不多了,顺便加入了在登录时,动态验证码的验证,这些都是在SpringSecuity提供好的基础上,那如何自定义这些登录的实现,仔细看一下,不管是基于内存验证jdbc验证...

都需要配置configure(AuthenticationManagerBuilder auth)身份验证管理器生成器,验证方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 根据传入的自定义{@link AuthenticationProvider} 认证提供者 添加身份验证。
 * 由于{@link AuthenticationProvider}实现是未知的,因此所有自定义操作必须在外部完成,
 * 并且{@link AuthenticationManagerBuilder} 会立即返回。
 */
auth.authenticationProvider(AuthenticationProvider authenticationProvider)

AuthenticationProvider是个接口,说明继承了AuthenticationProvider均可实现自定义认证的方法,SpringSecurity官方文档第9.2.1指出AuthenticationProvider,Spring Security实现的最简单的方法是DaoAuthenticationProvider所以我们实现该方法即可

MyAuthenticationProvider

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.shaojie.authority.security;

import com.shaojie.authority.component.MyWebAuthenticationDetails;
import com.shaojie.authority.exception.VerificationCodeException;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author: ShaoJie
 * @data: 2020年02月10日 20:26
 * @Description: 自定义认证过程
 * <p>
 * 因为 DaoAuthenticationProvider 也是继承的 AbstractUserDetailsAuthenticationProvider
 * 所以这里就只继承 DaoAuthenticationProvider
 */
public class MyAuthenticationProvider extends DaoAuthenticationProvider {

    /**
     * 用户认证提供者
     */
    @Autowired
    private UserDetailsService userDetailsService;
    
    /**
     * 密码加密
     */
    @Autowired
    public PasswordEncoder passwordEncoder;

    public MyAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.setUserDetailsService(userDetailsService);
        this.setPasswordEncoder(passwordEncoder);
    }

    /**
     * 允许子类针对给定的身份验证请求对返回的(或缓存的)UserDetails 进行任何其他检查。
     * 通常,子类至少会将 Authentication#getCredentials()与 UserDetails#getPassword()比较。
     * 如果需要自定义逻辑来比较 UserDetails 和或
     * UsernamePasswordAuthenticationToken 的其他*属性,则这些属性也应出现在此方法中。
     *
     * @param userDetails    用户信息
     * @param authentication 认证方式
     */
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                  UsernamePasswordAuthenticationToken authentication) {
                                                  
         // getCredentials() 属于 authentication 通常用来获取主体的凭据 通常为用户的密码
        // 这里的自定义密码校验是 MyAuthenticationProvider 继承  AbstractUserDetailsAuthenticationProvider
        if (authentication.getCredentials() == null) {
            throw new BadCredentialsException(
                    this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "密码不能为空"));
        } else {
            String password = authentication.getCredentials().toString();
            if (!password.equals(userDetails.getPassword())) {
                this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials",
                        "密码错误");
            }
        }
        // 使用父类的方法校验用户
        super.retrieveUser(userDetails.getUsername(), authentication);
    }
    
}

这里对这个UsernamePasswordAuthenticationToken应该比较疑惑,查看源码发现该类是Authentication认证方式的实现,认证方式中包含了对信息的获取

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
	 * The credentials that prove the principal is correct. This is usually a password,
	 * but could be anything relevant to the <code>AuthenticationManager</code>. Callers
	 * are expected to populate the credentials.
	 *
	 * @return the credentials that prove the identity of the <code>Principal</code>
	 */
	Object getCredentials();

	/**
	 * Stores additional details about the authentication request. These might be an IP
	 * address, certificate serial number etc.
	 *
	 * @return additional details about the authentication request, or <code>null</code>
	 * if not used
	 */
	Object getDetails();

简单点翻译就是Object getCredentials()方法可以获取账号的认证主体信息,通常来说就是密码这些主要的信息,Object getDetails()方法存储有关身份验证请求的其他详细信息。 但是如何能够实现对验证码的校验呢?SpringSecurity官方文档第10.16.1指出预身份验证过滤器具有一个authenticationDetailsSource属性,默认情况下,它将创建一个WebAuthenticationDetails对象来存储其他信息,那也就是说需要修改SpringSecurity实现自定义的验证配置

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().authenticationDetailsSource(new WebAuthenticationDetailsSource());
    }

authenticationDetailsSource源码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    /**
	 * Specifies a custom {@link AuthenticationDetailsSource}. The default is
	 * {@link WebAuthenticationDetailsSource}. 
	 * 指定一个自定义{@link AuthenticationDetailsSource} 默认为 {@link WebAuthenticationDetailsSource}. 
	 *
	 * @param authenticationDetailsSource the custom {@link AuthenticationDetailsSource}
	 * @return the {@link FormLoginConfigurer} for additional customization
	 */
	public final T authenticationDetailsSource(
			AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
		this.authenticationDetailsSource = authenticationDetailsSource;
		return getSelf();
	}

authenticationDetailsSource属性需要AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource参数,但是现在需要一个WebAuthenticationDetails对象来存储其他信息

MyWebAuthenticationDetails实现校验

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.shaojie.authority.component;

import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.web.authentication.WebAuthenticationDetails;

import javax.servlet.http.HttpServletRequest;

/**
 * @author: ShaoJie
 * @data: 2020年02月10日 21:54
 * @Description:
 */
@Slf4j
public class MyWebAuthenticationDetails extends WebAuthenticationDetails {

    /**
     * 验证码是否正确
     */
    private boolean imageCodeIsRight;

    public boolean getImageCodeIsRight() {
        return this.imageCodeIsRight;
    }

    /**
     * Records the remote address and will also set the session Id if a session already
     * exists (it won't create one). 保存会话
     * 补充用户提交的验证码和 session 保存的验证码
     *
     * @param request that the authentication request was received from
     */
    public MyWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        // 后去验证表单的值 --> 图形验证码
        String captcha = request.getParameter("captcha");
        log.info("表单的验证码captcha: {}", captcha);
        // 取出 访问时 已经添加在 session 中的验证码
        String sessionCaptcha = (String) request.getSession().getAttribute("captcha");
        log.info("session的验证码: {}", sessionCaptcha);
        // 判断两次的值是否值一样的
        if (!StrUtil.isEmpty(sessionCaptcha)) {
            // 清楚当前的验证码 无论是否成功或是失败 客户端登录失败应刷新当前的验证码
            request.getSession().removeAttribute("captcha");
            // 当验证码正确 修改当前的状态
            if (!StrUtil.isEmpty(captcha) && captcha.equals(sessionCaptcha)) {
                this.imageCodeIsRight = true;
            }
        }
    }
}

MyWebAuthenticationDetailsSource

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.shaojie.authority.component;

import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * @author: ShaoJie
 * @data: 2020年02月10日 21:56
 * @Description:
 */
@Component
public class MyWebAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {

    /**
     * 当类希望创建新的身份验证详细信息实例时由类调用。
     *
     * @param context 请求对象,可以由身份验证详细信息使用
     * @return
     */
    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
        return new MyWebAuthenticationDetails(context);
    }
}

MyWebAuthenticationDetailsSourceHttpServletRequest传递给MyWebAuthenticationDetails,用于获取用户提交的信息,并验证。这时候就可以在自定义验证中加入对验证码的校验

修改后的MyAuthenticationProvider

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.shaojie.authority.security;

import com.shaojie.authority.component.MyWebAuthenticationDetails;
import com.shaojie.authority.exception.VerificationCodeException;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author: ShaoJie
 * @data: 2020年02月10日 20:26
 * @Description: 自定义认证过程
 * <p>
 * 因为 DaoAuthenticationProvider 也是继承的 AbstractUserDetailsAuthenticationProvider
 * 所以这里就只继承 DaoAuthenticationProvider
 */
public class MyAuthenticationProvider extends DaoAuthenticationProvider {

    /**
     * 用户认证提供者
     */
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 密码加密
     */
    @Autowired
    public PasswordEncoder passwordEncoder;

    public MyAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.setUserDetailsService(userDetailsService);
        this.setPasswordEncoder(passwordEncoder);
    }

    /**
     * 允许子类针对给定的身份验证请求对返回的(或缓存的)UserDetails 进行任何其他检查。
     * 通常,子类至少会将 Authentication#getCredentials()与 UserDetails#getPassword()比较。
     * 如果需要自定义逻辑来比较 UserDetails 和或
     * UsernamePasswordAuthenticationToken 的其他*属性,则这些属性也应出现在此方法中。
     *
     * @param userDetails    用户信息
     * @param authentication 认证方式
     * @throws AuthenticationException SneakyThrow 将避免javac坚持要求您捕获或向前抛出方法主体中语句声明它们生成的所有检查异常。
     */
    @SneakyThrows
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                  UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        // getCredentials() 属于 authentication 通常用来获取主体的凭据 通常为用户的密码
        // 这里的自定义密码校验是 MyAuthenticationProvider 继承  AbstractUserDetailsAuthenticationProvider
        if (authentication.getCredentials() == null) {
            throw new BadCredentialsException(
                    this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "密码不能为空"));
        } else {
            String password = authentication.getCredentials().toString();
            if (!password.equals(userDetails.getPassword())) {
                this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials",
                        "密码错误");
            }
        }

        // 当修改了继承的类 现在实现图形验证码的 自定义
        // 实现图片验证码的逻辑
        MyWebAuthenticationDetails details = (MyWebAuthenticationDetails) authentication.getDetails();
        // 验证 验证码是正确
        if (!details.getImageCodeIsRight()) {
            throw new VerificationCodeException();
        }
    }

}

到此还没有结束,还需要修改SpringSecurity的配置,主要配置的就是authenticationDetailsSource属性,将MyWebAuthenticationDetailsSource注入进来,并调用实现自定义的认证

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    @Autowired
    private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> myWebAuthenticationDetailsSource;
    
    /**
     * 验证
     *
     * @param http
     * @throws Exception
     */
    // 代替配置文件 <security:http></security:http>
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // 添加权限
        selectPurview(http);

        http.authorizeRequests()
                // antMatchers 设置拦截的请求  hasAnyAuthority 对应的权限名称
                // .hasAnyAuthority("PRODUCT_ADD") 用户所具有的权限
                // 可替换成 .hasRole() 针对角色做验证
//                .antMatchers("/product/add").hasAnyAuthority("PRODUCT_ADD")
//                .antMatchers("/product/update").hasAnyAuthority("PRODUCT_UPDATE")
//                .antMatchers("/product/list").hasAnyAuthority("PRODUCT_LIST")
//                .antMatchers("/product/delete").hasAnyAuthority("PRODUCT_DELETE")
                // permitAll 所有的权限都能访问
                .antMatchers("/login").permitAll()
                .antMatchers("/captcha.jpg").permitAll()
//                .antMatchers("/**")
                // fullyAuthenticated 不允许匿名用户查看
//                .fullyAuthenticated()
                // 设置所有的请求都必须经过验证才能访问
                .anyRequest().authenticated()
                .and()
                // httpbasic 登录
                // .httpBasic();
                // 表单登录
                .formLogin()
                //  登录请求的页面
                .loginPage("/login")
                // 处理登录请求的 地址
                .loginProcessingUrl("/index")
                .authenticationDetailsSource(myWebAuthenticationDetailsSource)
                // 定义 故障处理器
//                 .failureHandler()
                // 修改 spring 提供的 默认登陆参数
                .usernameParameter("userName")
                .passwordParameter("password")
                .and()
                // 开启记住我功能
                .rememberMe()
                .and()
                // 开启登出
                .logout()
                // 最大会话数
                .and()
                // 添加过滤器 将 过滤器添加在 UsernamePasswordAuthenticationFilter 之前 也就是在验证账号密码之前
                // 自定义实现 用户登录拦截
//                .addFilterBefore(new VerificationCodeFilter(),
//                        UsernamePasswordAuthenticationFilter.class)
                .and()
                .csrf()
        // 禁用跨域的保护
                .disable();
    }

自定义实现登录到这里,我发现文档虽然很傻,但是好像对比源码来看的话,还是有点东西的,基本上需要的都能找到。新手不建议直接阅读,源码有的地方写的属实是看不懂,很多的地方,不对比着来看话,可能就懵了,多看点源码靠谱,最近喜欢研究研究,很多技术我有点不感冒,感觉可能也看不太多,今年目标读一本书正在循序渐进,一起加油吧,有些地方可能比较粗糙,细看吧,我这个也菜。

有些地方就不多说了,整合之前就可以了,有问题可以查看我的GitHub

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-02-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
你有没有遇到要实现多种登录方式的场景丫 一起来看看咯 Spring Security 实现多种登录方式,如常规方式外的邮件、手机验证码登录
上一篇文章我写了 Security登录详细流程详解有源码有分析。掌握这个登录流程,我们才能更好的做Security的定制操作。
宁在春
2022/10/31
1.5K0
你有没有遇到要实现多种登录方式的场景丫 一起来看看咯 Spring Security 实现多种登录方式,如常规方式外的邮件、手机验证码登录
【SpringSecurity系列(十一)】自定义认证逻辑
《深入浅出Spring Security》一书已由清华大学出版社正式出版发行,感兴趣的小伙伴戳这里->->>深入浅出Spring Security,一本书学会 Spring Security。
江南一点雨
2021/05/11
1.6K0
【SpringSecurity系列(十一)】自定义认证逻辑
SpringSecurity认证流程分析
AuthenticationManager是认证管理器 它定义了Spring Security过滤器要如何执行认证操作。AuthenticationManager在认证后会返回一个Authentication对象,它是一个接口,默认实现类是ProviderManager
周杰伦本人
2022/10/25
6720
Spring Security 可以同时对接多个用户表?
其实只要看懂了松哥前面的文章,这个需求是可以做出来的。因为一个核心点就是 ProviderManager,搞懂了这个,其他的就很容易了。
江南一点雨
2020/07/16
3.5K1
SpringBoot Security密码加盐
本文由 小马哥 创作,采用 知识共享署名4.0 国际许可协议进行许可 本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名 最后编辑时间为: 2022/12/25 01:41
IT小马哥
2022/12/25
1.2K0
Spring Security 架构与源码分析
Spring Security 主要实现了Authentication(认证,解决who are you? ) 和 Access Control(访问控制,也就是what are you allowe
JadePeng
2018/07/31
7710
Spring Security 架构与源码分析
SpringBoot集成SpringSecurity - 表单登录添加验证码(四)
源码地址:https://github.com/springsecuritydemo/microservice-auth-center04
用户1212940
2022/04/13
2K0
SpringBoot集成SpringSecurity - 表单登录添加验证码(四)
SpringSecurity入坑(二)
上一次实现了基于内存验证 缺少了实际的可用性现在修改一下 SpringSecurity的配置来实现基本的数据库验证
是小张啊喂
2021/04/14
3370
Spring Boot + Spring Cloud 实现权限管理系统 后端篇(二十五):Spring Security 版本
到目前为止,我们使用的权限认证框架是 Shiro,虽然 Shiro 也足够好用并且简单,但对于 Spring 官方主推的安全框架 Spring Security,用户群也是甚大的,所以我们这里把当前的代码切分出一个 shiro-cloud 分支,作为 Shiro + Spring Cloud 技术的分支代码,dev 和 master 分支将替换为 Spring Security + Spring Cloud 的技术栈,并在后续计划中集成 Spring Security OAuth2 实现单点登录功能。
朝雨忆轻尘
2019/06/19
1.5K0
👍SpringSecurity单体项目最佳实践
用户7630333
2023/12/07
2890
👍SpringSecurity单体项目最佳实践
SpringSecurity认证专题之【AuthenticationManager】
  哈喽,大家好,最近有段时间没有写博客了,今天开始我会陆续给大家整理出SpringSecurity原理源码相关的文件,本篇文章主要是给大家介绍下认证体系中最基础的AuthenticationManager的内容,让你对它从整体上面有一个认知。
用户4919348
2020/10/23
7880
深入Spring Security魔幻山谷-获取认证机制核心原理讲解
本文基于Springboot+Vue+Spring Security框架而写的原创学习笔记,demo代码参考《Spring Boot+Spring Cloud+Vue+Element项目实战:手把手教你开发权限管理系统》一书。
朱季谦
2020/09/08
4990
深入Spring Security魔幻山谷-获取认证机制核心原理讲解
【SpringSecurity系列02】SpringSecurity 表单认证逻辑源码解读
前面一节,通过简单配置即可实现SpringSecurity表单认证功能,而今天这一节将通过阅读源码的形式来学习SpringSecurity是如何实现这些功能, 前方高能预警,本篇分析源码篇幅较长。
yukong
2019/04/23
1.1K0
【SpringSecurity系列02】SpringSecurity 表单认证逻辑源码解读
【第二篇】SpringSecurity的第一次美好约会
  首先来看看在spring-security-core中的SecurityContextHolder,这个是一个非常基础的对象,存储了当前应用的上下文SecurityContext,而在SecurityContext可以获取Authentication对象。也就是当前认证的相关信息会存储在Authentication对象中。
用户4919348
2022/05/10
3190
【第二篇】SpringSecurity的第一次美好约会
springboot+jjwt+security完美解决restful接口无状态鉴权
springboot本身已经提供了很好的spring security的支持,我们只需要实现(或者重写)一部分接口来实现我们的个性化设置即可。本文浅显易懂,没有深入原理(后面文章会将,有需要的小伙伴稍等等~~~)。 思路: 1.通过spring security做授权拦截操作 2.通过jwt根据用户信息生成token以供后面调用 3.将生成的token放到HttpServletResponse头信息中 4.使用的时候从response头中获取token放在request头中提交到后台做认证即可 5.默认超时时间10天
小尘哥
2018/09/29
2.2K0
Spring-security-oauth2之DaoAuthenticationProvider
    Spring-security-oauth2的版本是2.3.5.RELEASE
克虏伯
2019/04/15
2.7K0
Security 登录认证流程详细分析 源码与图相结合
对于一门技术,会使用是说明我们对它已经有了一个简单了解,把脉络都掌握清楚,我们才能更好的使用它,以及更好的实现定制化。
宁在春
2022/10/31
5940
Security 登录认证流程详细分析 源码与图相结合
Spring Security源码分析一:Spring Security认证过程
为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类
java干货
2021/02/19
1.6K1
Spring Security源码分析一:Spring Security认证过程
Spring Security (一) Architecture Overview
国庆 + 中秋,先祝大家双节快乐!接一下推荐一下国庆还在写干货的有为少年,也就是本篇的作者,文末卡片点击可关注他的个人公众号! 一直以来我都想写一写Spring Security系列的文章,但是整个Spring Security体系强大却又繁杂。陆陆续续从最开始的guides接触它,项目中看了一些源码,到最近这个月为了写一写这个系列的文章,阅读了好几遍文档,最终打算尝试一下,写一个较为完整的系列文章。 较为简单或者体量较小的技术,完全可以参考着demo直接上手,但系统的学习一门技术则不然。以我的认知,一
程序猿DD
2018/02/01
1.1K0
Spring Security (一) Architecture Overview
Spring和Security整合详解
Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
品茗IT
2019/09/12
1K0
推荐阅读
相关推荐
你有没有遇到要实现多种登录方式的场景丫 一起来看看咯 Spring Security 实现多种登录方式,如常规方式外的邮件、手机验证码登录
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验