• Spring Security 安全框架 (一) 基础操作


    1.password 登录密码

    在 springboot 项目中 , 引入依赖

            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-securityartifactId>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    启动时

    在这里插入图片描述

    1.1.默认密码

    启动时 , 控制台可以看到 生成一个UUID 作为 密码

    Using generated security password: 876205ea-25bd-47b2-9c68-e2ac52377915
    
    • 1

    用户名为 user

    1.2. 配置文件配置密码

    在 application.properties 配置文件 中加入

    # 设置 用户名密码
    spring.security.user.name=admin
    spring.security.user.password=123
    
    • 1
    • 2
    • 3

    1.3.密码生成器

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.password.NoOpPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    /**
     * security 配置文件
     */
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        /**
         * 密码生成器
         * @return
         */
        @Bean
        PasswordEncoder passwordEncoder() {
            // 无加密密码
            return NoOpPasswordEncoder.getInstance();
        }
    
        /**
         * 设置 用户 密码 及 角色
         * @param auth
         * @throws Exception
         */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                    .withUser("yuan@123")
                    .password("123")
                    .roles("admin");
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    2.登录页面

     /**
         * 配置忽略掉的 URL 地址,一般对于静态文件
         * @param web
         * @throws Exception
         */
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/js/**", "/css/**","/img/**","/font/**");
        }
    
        /**
         *  请求属性配置
         * @param http
         * @throws Exception
         */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .loginPage("/login.html")  //转向登录页面
                    .loginProcessingUrl("/doLogin")  // 登录请求
                    .usernameParameter("username")   // 账号标识
                    .passwordParameter("password")   // 密码标识
                    //.successForwardUrl("/success")  // 登录成功跳转(内部转, 登录成功跳转到指定请求)
                    .defaultSuccessUrl("/success")    // 登录成功跳转(重定向, 登录成功就回到之前访问的资源)
                    .failureForwardUrl("/login.html")
                    .failureUrl("/login.html")
                    .permitAll()
                    .and()
                    .logout()
                    //.logoutUrl("/logout")  // GET方式 调用logout
                    .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "POST"))  // POST 方式调用 logout
                    .logoutSuccessUrl("/login.html")  // 退出转向
                    .invalidateHttpSession(true)  // 清空session
                    .clearAuthentication(true)    // 清空认证信息
                    .permitAll()
                    .and()
                    .csrf().disable();
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class HelloController {
    
        @RequestMapping("/hello")
        public String sayHello(){
            return "hello";
        }
    
    
        @RequestMapping("/success")
        public String success(){
            return "success";
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    3.返回信息

    3.1.登录成功返回

    //.successForwardUrl("/success")
                    //.defaultSuccessUrl("/success")
                    .successHandler((request,response, authentication)->{
                        response.setContentType("application/json;charset=utf-8");
                        PrintWriter out = response.getWriter();
                        out.write(new ObjectMapper().writeValueAsString(authentication.getPrincipal()));
                        out.flush();
                        out.close();
                    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    {
        "password": null,
        "username": "yuan@123",
        "authorities": [
            {
                "authority": "ROLE_admin"
            }
        ],
        "accountNonExpired": true,
        "accountNonLocked": true,
        "credentialsNonExpired": true,
        "enabled": true
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    3.2.登录失败返回

    //.failureForwardUrl("/login.html")
                    //.failureUrl("/login.html")
                    .failureHandler((request,response, exception)->{
                        response.setContentType("application/json;charset=utf-8");
                        PrintWriter out = response.getWriter();
                        String msg = "";
                        if (exception instanceof LockedException) {
                            msg = "账户被锁定,请联系管理员!";
                        } else if (exception instanceof CredentialsExpiredException) {
                            msg = "密码过期,请联系管理员!";
                        } else if (exception instanceof AccountExpiredException) {
                            msg = "账户过期,请联系管理员!";
                        } else if (exception instanceof DisabledException) {
                            msg = "账户被禁用,请联系管理员!";
                        } else if (exception instanceof BadCredentialsException) {
                            msg = "用户名或者密码输入错误,请重新输入!";
                        }
                        out.write(new ObjectMapper().writeValueAsString(msg));
                        out.flush();
                        out.close();
                    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    3.3.未登录请求

    .exceptionHandling()
    .authenticationEntryPoint((req, resp, authException) -> {
    		resp.setContentType("application/json;charset=utf-8");
    		PrintWriter out = resp.getWriter();
    		out.write("尚未登录,请先登录");
    		out.flush();
    		out.close();
    	}
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3.4.登出注销

     .logout()
     .logoutUrl("/logout")
     //.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "POST"))
     //.logoutSuccessUrl("/login.html")
     //.invalidateHttpSession(true)
     //.clearAuthentication(true)
     .logoutSuccessHandler((req, resp, authentication) -> {
     	resp.setContentType("application/json;charset=utf-8");
     	PrintWriter out = resp.getWriter();
    	 out.write("注销成功");
     	out.flush();
    	 out.close();
     })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    4.角色授权

    4.1.设置账号

    将原来方法注释, 使用新的方法

     ///**
        // * 设置 用户 密码 及 角色
        // * @param auth
        // * @throws Exception
        // */
        //@Override
        //protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //    auth.inMemoryAuthentication()
        //            .withUser("yuan")
        //            .password("123")
        //            .roles("admin");
        //}
    
    
        /**
         * pring Security 支持多种数据源,例如内存、数据库、LDAP 等,
         * 这些不同来源的数据被共同封装成了一个 UserDetailService 接口,
         * 任何实现了该接口的对象都可以作为认证数据源。
         * @return
         */
        @Override
        @Bean
        protected UserDetailsService userDetailsService() {
            // 在内存中存储, 创建两个账号 , 分别赋 admin 和  user 权限
            InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
            manager.createUser(User.withUsername("admin").password("123").roles("admin").build());
            manager.createUser(User.withUsername("yuan").password("123").roles("user").build());
            return manager;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    4.2.增加响应方法

    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class HelloController {
    
        /**
         * 只要登录就只可以访问
         * @return
         */
        @RequestMapping("/hello")
        public String sayHello(){
            return "hello";
        }
    
        /**
         * 只有 admin 角色才能访问
         * @return
         */
        @GetMapping("/admin/hello")
        public String admin() {
            return "admin";
        }
    
        /**
         *  admin,  user 角色都可以访问
         * @return
         */
        @GetMapping("/user/hello")
        public String user() {
            return "user";
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    4.3.设置角色权限

     /**
         *  请求属性配置
         * @param http
         * @throws Exception
         */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/admin/**").hasRole("admin")
                    .antMatchers("/user/**").hasRole("user")
                    .anyRequest().authenticated()
                    .and()
                    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    将使用 admin 访问 /user/hello 时会报错

    {
        "timestamp": "2021-11-05T14:27:39.537+00:00",
        "status": 403,
        "error": "Forbidden",
        "message": "",
        "path": "/user/hello"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    4.4.角色继承

        /**
         * 角色继承
         * @return
         */
        @Bean
        RoleHierarchy roleHierarchy() {
            RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
            hierarchy.setHierarchy("ROLE_admin > ROLE_user");
            return hierarchy;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这样 使用 admin 访问 /user/hello 就可以了

    5.访问数据库

    5.1.数据库

    5.2.实体类

    实现 UserDetails 接口 , 覆盖对应的方法

    
    import com.baomidou.mybatisplus.annotation.IdType;
    import com.baomidou.mybatisplus.annotation.TableField;
    import com.baomidou.mybatisplus.annotation.TableId;
    import com.baomidou.mybatisplus.annotation.TableName;
    import java.io.Serializable;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.List;
    
    import lombok.Data;
    import org.apache.ibatis.mapping.FetchType;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    /**
     * 
     * @TableName sys_user
     */
    @TableName(value ="sys_user")
    @Data
    public class UserEntity implements UserDetails, Serializable {
        /**
         * 
         */
        @TableId(type = IdType.AUTO)
        private Integer userId;
    
        /**
         * 
         */
        private String userName;
    
        /**
         * 
         */
        private String userPass;
    
        /**
         * 
         */
        private String salt;
    
        /**
         * 
         */
        private String nickName;
    
        @TableField(exist = false)
        private static final long serialVersionUID = 1L;
    
    
        // 账户是否没有过期
        @TableField(exist = false)
        private boolean accountNonExpired = true;
    
        //账户是否没有被锁定
        @TableField(exist = false)
        private boolean accountNonLocked = true;
    
        //密码是否没有过期
        @TableField(exist = false)
        private boolean credentialsNonExpired = true;
    
        //账户是否可用
        @TableField(exist = false)
        private boolean enabled = true;
    
        @TableField(exist = false)
        private List<RoleEntity> roles;
    
        // 返回用户的角色信息
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            List<SimpleGrantedAuthority> authorities = new ArrayList<>();
            for (RoleEntity role : getRoles()) {
                 // 注意这里的 角色 前缀
                authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleCode()));
            }
            return authorities;
        }
    
        @Override
        public String getPassword() {
            return this.userPass;
        }
    
        @Override
        public String getUsername() {
            return this.userName;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return accountNonExpired;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return accountNonLocked;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return credentialsNonExpired;
        }
    
        @Override
        public boolean isEnabled() {
            return enabled;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113

    5.3.service 查询

    
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    
    /**
     *
     */
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity>
    implements UserService, UserDetailsService {
    
        @Override
        public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
    
            UserEntity user = this.getOne(new LambdaQueryWrapper<UserEntity>().eq(UserEntity::getUsername, name));
            if (user == null) {
                throw new UsernameNotFoundException("用户不存在");
            }
            user.setRoles(this.getRolesByUserId(user.getUserId()));
            System.out.println("user = " + user);
            return user;
        }
    
        public List<RoleEntity> getRolesByUserId(Integer userId){
            // 通过 数据库 连表 , 根据 用户id 查询对应的 role 集合
            return this.baseMapper.selectRoleListByUserId(userId);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    5.4.security配置类

    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
    import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
    import org.springframework.security.authentication.*;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.NoOpPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.provisioning.InMemoryUserDetailsManager;
    
    import java.io.PrintWriter;
    
    /**
     * security 配置类
     */
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    
        @Autowired
        private UserServiceImpl userService;
    
        /**
         * 密码生成器
         * @return
         */
        @Bean
        PasswordEncoder passwordEncoder() {
            // 自带加密器
            BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
            return bCryptPasswordEncoder;
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
        }
    
        /**
         * 角色继承
         * @return
         */
        @Bean
        RoleHierarchy roleHierarchy() {
            RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
            hierarchy.setHierarchy("ROLE_admin > ROLE_user");
            return hierarchy;
        }
    
        /**
         * 配置忽略掉的 URL 地址,一般对于静态文件
         * @param web
         * @throws Exception
         */
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/js/**", "/css/**","/img/**","/font/**");
        }
    
    
    
        /**
         *  请求属性配置
         * @param http
         * @throws Exception
         */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/admin/**").hasRole("admin")
                    .antMatchers("/user/**").hasRole("user")
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .loginProcessingUrl("/doLogin")
                    .successHandler((request,response, authentication)->{
                        response.setContentType("application/json;charset=utf-8");
                        PrintWriter out = response.getWriter();
                        out.write(new ObjectMapper().writeValueAsString(authentication.getPrincipal()));
                        out.flush();
                        out.close();
                    })
                    .failureHandler((request,response, exception)->{
                        response.setContentType("application/json;charset=utf-8");
                        PrintWriter out = response.getWriter();
                        String msg = "";
                        if (exception instanceof LockedException) {
                            msg = "账户被锁定,请联系管理员!";
                        } else if (exception instanceof CredentialsExpiredException) {
                            msg = "密码过期,请联系管理员!";
                        } else if (exception instanceof AccountExpiredException) {
                            msg = "账户过期,请联系管理员!";
                        } else if (exception instanceof DisabledException) {
                            msg = "账户被禁用,请联系管理员!";
                        } else if (exception instanceof BadCredentialsException) {
                            msg = "用户名或者密码输入错误,请重新输入!";
                        }
                        out.write(new ObjectMapper().writeValueAsString(msg));
                        out.flush();
                        out.close();
                    })
                    .permitAll()
                    .and()
                    .logout()
                    .logoutUrl("/logout")
                    .logoutSuccessHandler((req, resp, authentication) -> {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter out = resp.getWriter();
                        out.write("注销成功");
                        out.flush();
                        out.close();
                    })
                    .and()
                    .csrf().disable()
                    .exceptionHandling()
                    .authenticationEntryPoint((req, resp, authException) -> {
                            resp.setContentType("application/json;charset=utf-8");
                            PrintWriter out = resp.getWriter();
                            out.write("尚未登录,请先登录");
                            out.flush();
                            out.close();
                        }
                    );
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
  • 相关阅读:
    shell脚本的 正则表达式
    Kubernetes技术与架构-Ingress
    docker安装常用软件
    【会员管理系统】篇一之项目预热
    Python基础导读:变量+条件语句+字符串操作
    打印机打印数量和碳粉监视器 2.2--PrintLimit Print Tracking
    linux0.12-10-5-rs_io.s
    基于SSM+MySQL的校园共享单车管理系统
    图解Nginx,系统架构演变 + Nginx反向代理与负载均衡
    Vue —— 进阶 vue-router 路由(零)(路由的概念、基本使用)
  • 原文地址:https://blog.csdn.net/yuanchun05/article/details/127871940