SpringSecurity6+JWT实现前后端分离

这篇具有很好参考价值的文章主要介绍了SpringSecurity6+JWT实现前后端分离。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

本文使用mybatis-plus作为ORM框架,然后使用springsecurity6作为安全框架,采用JWT作为权限确认的方式。

其实我现在就刚入门这个springsecurity6而已,有许多都没有摸清它的使用,写下这篇文章只是想记录一下,下次想用的时候来看看。

一. 准备阶段

1.JWT工具类

上面说了使用JWT。那就得有一个能生成JWT和检测JWT的工具类,我在下面给出了一个简陋的工具类,包含了三个方法。这个工具类的jar包依赖如下:

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

本文的JWT工具类的加密算法就普普通通的HS256,token有效期就只有4个小时,这个token里面就只存有userName这一个信息,所以为了确保不会出错,在数据库中的userName这一列,不能出现一模一样的两行数据,换句话说,就是用户名不能重复。代码如下: 

@Component
@Slf4j
public class JwtUtil {

    //常量
    public static final long EXPIRE = 1000 * 60 * 60 * 4; //token过期时间,4个小时
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO"; //秘钥

    //生成token字符串的方法
    public String getToken(String userName){
        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                .setSubject("user")
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                .claim("userName", userName)//设置token主体部分 ,存储用户信息
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();
    }

    //验证token字符串是否是有效的  包括验证是否过期
    public boolean checkToken(String jwtToken) {
        if(jwtToken == null || jwtToken.isEmpty()){
            log.error("Jwt is empty");
            return false;
        }
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
            Claims body = claims.getBody();
            if ( body.getExpiration().after(new Date(System.currentTimeMillis()))){
                return true;
            } else
                return false;
        } catch (Exception e) {
            log.error(e.getMessage());
            return false;
        }
    }

    public Claims getTokenBody(String jwtToken){
        if(jwtToken == null || jwtToken.isEmpty()){
            log.error("Jwt is empty");
            return null;
        }
        try {
            return Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken).getBody();
        } catch (Exception e){
            log.error(e.getMessage());
            return null;
        }
    }
}

2.数据库连接

本项目使用mybatis-plus作为ORM框架,然后使用druid连接池,所需要的依赖如下:

<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.15</version>
        </dependency>

接着在application.yml中进行配置,我的数据库名字起名叫dubbd,你们可以根据自己数据库名字将dubbd更换。

server:
  port: 8080

datasource:
  url: localhost:3306/dubbd

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://${datasource.url}?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&failOverReadOnly=false&maxReconnects=10&allowPublicKeyRetrieval=true
    username: root
    password: 123456
    hikari:
      maximum-pool-size: 10
      max-lifetime: 1770000
    druid:
      validation-query: SELECT 1 FROM DUAL
      initial-size: 10
      min-idle: 10
      max-active: 200
      min-evictable-idle-time-millis: 300000
      test-on-borrow: false
      test-while-idle: true
      time-between-eviction-runs-millis: 30000
      pool-prepared-statements: true
      max-open-prepared-statements: 100

再然后,我们要在数据库的一个表中准备自己需要的用户数据,我的用户数据如下:

springsecurity6+jwt,spring boot,intellij-idea

导出后的sql脚本如下:

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `userName` varchar(100) DEFAULT NULL,
  `password` varchar(100) DEFAULT NULL,
  `role` varchar(20) DEFAULT NULL,
  `userId` varchar(100) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户信息表';

LOCK TABLES `user` WRITE;

INSERT INTO `user` VALUES ('huonzy','$2a$10$kNrjJ3.1wCSmSnjs1JI.RO6RMrQc.oRiJ93T2sefyCOzXb7yy.Mmm','user','1');

UNLOCK TABLES;

3.实现表和实体类的绑定

在上面,我们已经明确了我们的表名为user,所以要使用mybatis-plus,我们就要创建一个User类实体类,并使用注解来绑定这个表,因为我们同时还要使用User这个实体类作为springsecurity的用户类,所以我们在创建这个实体类时,还要实现UserDetails这个接口,下面是我们需要的依赖:

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

接下来是实体类的代码: 

@TableName("user")
@Data
public class User implements UserDetails {

    @TableField("userName")
    private String userName;

    @TableField("password")
    private String password;

    @TableId("userId")
    private String userId;

    @TableField("role")
    private String role;

    @Override//用户所拥有的权限,返回的列表中至少得有一个值,否则这个用户啥权限都没有
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority(role));
    }

    @Override//实现UserDetails的getPassword方法,返回实体类的password
    public String getPassword() {
        return password;
    }

    @Override//这个方法是UserDetails中的方法,必须实现
    public String getUsername() {
        return userName;
    }

    public String getUserName(){//这个是mybatis-plus需要用到的方法
        return this.userName;
    }

    @Override//返回true,代表用户账号没过期
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override//返回true,代表用户账号没被锁定
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override//返回true,代表用户密码没有过期
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override//返回true,代表用户账号还能够使用
    public boolean isEnabled() {
        return true;
    }
}

 最后就是建立mapper层的CRUD了:

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

不要忘记在启动类下加上@MapperScan注解。

4.统一返回前后端交互的数据格式

前端登录时传给后端的,无疑就是用户名和密码了,我们创建一个类来接收者两个数据:

@Data
public class LoginReuqest {
    private String userName;
    private String password;
}

后端传给前端的,就是一个统一的格式:

@Data
@Builder
public class HnResult {
    private Integer code;
    private String msg;
    private Object data;

    public static HnResult ok(String message) {
        return HnResult.builder().code(200).msg(message).build();
    }

    public static HnResult ok() {
        return HnResult.builder().code(200).msg("成功").build();
    }

    public static HnResult error(String message){
        return HnResult.builder().code(500).msg(message).build();
    }

    public static HnResult error(){
        return HnResult.builder().code(500).msg("失败").build();
    }

    public <T> HnResult setData(T data){
        this.data = data;
        return this;
    }
}

到这里,准备都已经完成了,开始进入登录的用户信息确认阶段。

二.用户登录认证阶段

当一个用户访问一个网站时,那必须要进行登录的,没有账号的话那就去注册。用户登录只需要一次就够了,登录后,后面发出的请求就不需要再进行登录认证了。所以我们要确保这个过程只用经历一次。对于用户登录的信息确认,我们有两种实现方式,第一种就是实现UsernamePasswordAuthenticationFilter;第二种就是自定义一个控制层的用户登录接口,然后在这个接口里面使用AuthenticationManager来对用户信息进行确认。其实这两种方式差不多,UsernamePasswordAuthenticationFilter也会调用AuthenticationManager来对用户信息进行确认。第二种方式就是直接把UsernamePasswordAuthenticationFilter中的doFilterInternal方法里面的代码搬到了控制层的登录接口里面而已。所以我们在这里使用第二种方式,会了第二种方式,第一种方式也差不多会了。因为实现UsernamePasswordAuthenticationFilter,只需要实现doFilterInternal方法。

1.实现UserDetailsService

在前面的准备阶段,我们定义了一个实体类User并让其实现了UserDetails接口,之所以我们要实现这个接口,是因为springsecurity中的认证过程中采用的用户服务返回的数据格式就是UserDetails。这个用户服务就是UserDetailsService,同样的,这个也是一个接口,也需要创建一个服务类对其进行实现,我们只需要实现其loadUserByUsername方法就行了,代码如下:

@Service
@Slf4j
public class UserService implements UserDetailsService {

    @Resource
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName, username);
        try {
            User user = userMapper.selectOne(wrapper);//这里要确保userName是唯一的
            return user;
        }catch (Exception e){
            log.error("user is not find");
            return null;
        }
    }

}

接下来,就是对SpringSecurity进行修改配置了。

2.将PasswordEncoder注册成Bean

springsecurity6+jwt,spring boot,intellij-idea

 我们的用户登录认证流程其实就是上面的这幅图,里面用到的类,除了UsernamePasswordAuthenticationToken不需要注册成Bean外,其余都要注册成Bean。我们先注册PasswordEncoder,这个其实是一个接口,我们需要将其实现类注册成接口。我们需要创建一个配置类,用来容纳所有的springsecurity配置修改,然后在里面将PasswordEncoder注册成Bean,代码如下:

@Configuration//声明该类是一个配置类
@EnableWebSecurity//开启springsecurity配置修改
public class SecurityConfig {


    @Bean//PasswordEncoder的实现类为BCryptPasswordEncoder
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

3.将AuthenticationProvider注册成Bean

我们从上图可以看到,AuthenticationProvider使用了PasswordEncoder和UserDetailsService,所以我们在配置 AuthenticationProvider时要将这两个加上。将下面的代码加入上面创建的SecurityConfig配置类即可。

    @Resource
    UserService userService;

    @Bean
    public AuthenticationProvider authenticationProvider(){
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setPasswordEncoder(passwordEncoder());
        provider.setUserDetailsService(userService);
        return provider;
    }

4.将AuthenticationManager注册成Bean

将AuthenticationManager配置成Bean就十分简单了,也是将下面的代码加入SecurityConfig中:

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

5.将SecurityFilterChain注册成Bean

不知道大家有没有发现,我们上面并没有建立其AuthenticationManager和AuthenticationProvider的联系,那AuthenticationManager该如何使用AuthenticationProvider进行用户登录认证。AuthenticationManager在注册成Bean的时候,用的是配置里面的AuthenticationManager,所以我们需要在SecurityFilterChain中修改认证时使用的AuthenticationProvider变成我们自己注册的Bean,这样子AuthenticationManager就可以使用我们自己配置的AuthenticationProvider了。对SecurityFilterChain也需要写入SecurityConfig中,代码如下:

    @Resource
    JwtFilter jwtFilter;//后面jwt验证需要用到的过滤器,现在先不理它

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.formLogin(AbstractHttpConfigurer::disable)//取消默认登录页面的使用
                .logout(AbstractHttpConfigurer::disable)//取消默认登出页面的使用
                .authenticationProvider(authenticationProvider())//将自己配置的PasswordEncoder放入SecurityFilterChain中
                .csrf(AbstractHttpConfigurer::disable)//禁用csrf保护,前后端分离不需要
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))//禁用session,因为我们已经使用了JWT
                .httpBasic(AbstractHttpConfigurer::disable)//禁用httpBasic,因为我们传输数据用的是post,而且请求体是JSON
                .authorizeHttpRequests(request -> request.requestMatchers(HttpMethod.POST, "/user/login", "/user/register").permitAll().anyRequest().authenticated())//开放两个接口,一个注册,一个登录,其余均要身份认证
                .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);//将用户授权时用到的JWT校验过滤器添加进SecurityFilterChain中,并放在UsernamePasswordAuthenticationFilter的前面
        return httpSecurity.build();
    }

在上面的代码中,我禁用了默认登录和登出页面,因为我们是前后端分离,不需要后端提供页面。我禁用了session的创建和使用,因为我已经使用了JWT,即java web token,我禁用了CSRF保护,因为前后端分离不需要这种保护。

6.实现控制层登录接口

登录接口的实现其实可以按照上面那幅图的流程来写,不过在用户验证成功后要把信息存入SecurityContext中,想要获取SecurityContext就要通过SecurityContextHolder的getContext方法获取。

@RestController
@RequestMapping("/user/")
@Slf4j
public class UserController {

    @Resource
    AuthenticationManager authenticationManager;

    @PostMapping("login")
    public HnResult doLogin(@RequestBody LoginReuqest request){
        try{
            UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(request.getUserName(), request.getPassword());
            Authentication authentication = authenticationManager.authenticate(auth);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            UserDetails userDetails = (UserDetails) authentication.getPrincipal();
            String jwtToken = jwtUtil.getToken(userDetails.getUsername());
            return HnResult.ok("登录成功").setData(jwtToken);
        } catch (Exception e){
            log.error(e.getMessage());
            log.error("userName or password is not correct");
            return HnResult.error("登录失败");
        }
    }

}

7.接口测试

使用apifox进行测试这个登录接口能不能成功返回数据。

springsecurity6+jwt,spring boot,intellij-idea

三.用户注册阶段

用户注册也要构建一个控制层的接口,注意要把用户的密码经过PasswordEncoder.encode()编码后,再存入数据库。

1.在UserService中再添加一个插入用户的方法

public User insertUser(User user){
        try {
            LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(User::getUserName, user.getUserName());
            if (userMapper.selectList(wrapper).size() == 0){
                userMapper.insert(user);
                return user;
            }else {
                log.error("userName already exists");
                return null;
            }
        }catch (Exception e){
            log.error(e.getMessage());
            return null;
        }
    }

2.构建注册接口

注册接口里面,我们用到了一个ID生成方法,该方法需要下面这个依赖:

<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-extra -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-extra</artifactId>
            <version>5.8.21</version>
        </dependency>

接口代码如下:

    @Resource
    UserService userService;

    @Resource
    PasswordEncoder passwordEncoder;

    @PostMapping("register")
    public HnResult doRegister(@RequestBody User user){
        try {
            if (user.getPassword() != null && !user.getPassword().isEmpty()){
                String password = passwordEncoder.encode(user.getPassword());
                user.setPassword(password);
                user.setUserId(IdUtil.getSnowflakeNextIdStr());
                if (userService.insertUser(user) == null){
                    throw new Exception("用户名已存在");
                }
                String jwtToken = jwtUtil.getToken(user.getUserName());
                return HnResult.ok().setData(jwtToken);
            }else
                throw new Exception("密码为空");
        }catch (Exception e){
            log.error(e.getMessage());
            return HnResult.error("注册失败" + e.getMessage());
        }
    }

3.接口测试

springsecurity6+jwt,spring boot,intellij-idea

数据库里的确多了一条数据。

springsecurity6+jwt,spring boot,intellij-idea

 文章来源地址https://www.toymoban.com/news/detail-836645.html

四.用户权限验证阶段

用户在每发一个请求到后端这里,都要进行验证其权限是否足够访问这个接口,在我这里的话,只需要验证jwt是否可用就行了。

1.构建JWT验证过滤器

代码如下:

@Component
@Slf4j
public class JwtFilter extends OncePerRequestFilter {

    @Resource
    JwtUtil jwtUtil;

    @Resource
    UserService userService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String jwtToken = request.getHeader("token");//从请求头中获取token
        if (jwtToken != null && !jwtToken.isEmpty() && jwtUtil.checkToken(jwtToken)){
            try {//token可用
                Claims claims = jwtUtil.getTokenBody(jwtToken);
                String userName = (String) claims.get("userName");
                UserDetails user = userService.loadUserByUsername(userName);
                if (user != null){
                    UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                    SecurityContextHolder.getContext().setAuthentication(auth);
                }
            } catch (Exception e){
                log.error(e.getMessage());
            }
        }else {
            log.warn("token is null or empty or out of time, probably user is not log in !");
        }
        filterChain.doFilter(request, response);//继续过滤
    }
}

2.将JWT过滤器添加进SecurityFilterChain

这一步骤我们在配置 SecurityFilterChain的时候已经做过了,大家可以返回到第二部分那里去看。

3.验证和测试

将之前注册好的用户的token放入请求头中,参数名为token,参数值则为之前返回的jwt。随便写一个接口,然后访问这个接口。

    @PostMapping("check")
    public HnResult doCheck(){
        log.info("权限验证成功");
        return HnResult.ok();
    }

springsecurity6+jwt,spring boot,intellij-idea

 

到了这里,关于SpringSecurity6+JWT实现前后端分离的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • SpringSecurity6 | 问题答疑

    ✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏: Java从入门到精通 ✨特色专栏: MySQL学习 🥭本文内容:SpringSecurity6 | 问题答疑 📚个人知识库: [Leo知识库]https://gaoziman.gitee.io/blogs/),欢迎大家

    2024年02月04日
    浏览(39)
  • SpringSecurity6 | 修改默认用户

    ✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏: Java从入门到精通 ✨特色专栏: MySQL学习 🥭本文内容:SpringSecurity6 | 修改默认用户 📚个人知识库: [Leo知识库]https://gaoziman.gitee.io/blogs/),欢迎

    2024年02月04日
    浏览(52)
  • SpringSecurity6 | 自动配置(上)

    ✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏: Java从入门到精通 ✨特色专栏: MySQL学习 🥭本文内容:SpringSecurity6 | 自动配置(上) 🖥️个人小站 :个人博客,欢迎大家访问 📚个人知识库:

    2024年02月04日
    浏览(55)
  • SpringSecurity6 | 默认登录页

    ✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏: Java从入门到精通 ✨特色专栏: MySQL学习 🥭本文内容:SpringSecurity6 | 默认登录页 📚个人知识库 :知识库,欢迎大家访问 学习参考 : 讲师:

    2024年02月04日
    浏览(44)
  • SpringSecurity6 | 自定义登录页面

    ✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏: Java从入门到精通 ✨特色专栏: MySQL学习 🥭本文内容:SpringSecurity6 | 自定义登录页面 📚个人知识库: [Leo知识库]https://gaoziman.gitee.io/blogs/),欢

    2024年02月05日
    浏览(54)
  • SpringSecurity6 | HelloWorld入门案例

    ✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏: Java从入门到精通 ✨特色专栏: MySQL学习 🥭本文内容:SpringSecurity6 | HelloWorld入门案例 📚个人知识库 :知识库,欢迎大家访问

    2024年02月07日
    浏览(36)
  • SpringSecurity6 | 默认用户生成(上)

    ✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏: Java从入门到精通 ✨特色专栏: MySQL学习 🥭本文内容:SpringSecurity6 | 默认用户生成(上) 📚个人知识库: [Leo知识库]https://gaoziman.gitee.io/blogs/),欢

    2024年02月05日
    浏览(42)
  • SpringSecurity6 | 默认用户生成(下)

    ✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏: 循序渐进学SpringSecurity6 ✨特色专栏: MySQL学习 🥭本文内容:SpringSecurity6 | 默认用户生成(下) 📚个人知识库 :知识库,欢迎大家访问 学习参考

    2024年02月04日
    浏览(36)
  • SpringSecurity6 | 退出登录后的跳转

    ✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏: 循序渐进学SpringSecurity6 ✨特色专栏: MySQL学习 🥭本文内容: SpringSecurity6 | 登录失败后的JSON处理 📚个人知识库: Leo知识库,欢迎大家访问 学

    2024年02月03日
    浏览(54)
  • SpringSecurity6 | 登录失败后的跳转

    ✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏: 循序渐进学SpringSecurity6 ✨特色专栏: MySQL学习 🥭本文内容: SpringSecurity6 | 失败后的跳转 📚个人知识库: Leo知识库,欢迎大家访问 学习参考

    2024年02月03日
    浏览(37)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包