<!--mp逆向工程 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3.1</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.31</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.7</version> </dependency>
package com.ds.book.mp; import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException; import com.baomidou.mybatisplus.core.toolkit.StringPool; import com.baomidou.mybatisplus.generator.AutoGenerator; import com.baomidou.mybatisplus.generator.InjectionConfig; import com.baomidou.mybatisplus.generator.config.*; import com.baomidou.mybatisplus.generator.config.po.TableInfo; import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; import java.util.List; import java.util.Scanner; public class CodeGenerator { /** * <p> * 读取控制台内容 * </p> */ public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("请输入" + tip + ":"); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotBlank(ipt)) { return ipt; } } throw new MybatisPlusException("请输入正确的" + tip + "!"); } public static void main(String[] args) { // 代码生成器 AutoGenerator mpg = new AutoGenerator(); // 全局配置 GlobalConfig gc = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); gc.setOutputDir(projectPath + "/src/main/java"); gc.setAuthor("java大师"); gc.setOpen(false); // gc.setSwagger2(true); 实体属性 Swagger2 注解 mpg.setGlobalConfig(gc); // 数据源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://175.24.198.63:3306/book?useSSL=false&characterEncoding=utf8&serverTimezone=GMT%2B8"); // dsc.setSchemaName("public"); dsc.setDriverName("com.mysql.cj.jdbc.Driver"); dsc.setUsername("root"); dsc.setPassword("root@1234!@#"); mpg.setDataSource(dsc); // 包配置 PackageConfig pc = new PackageConfig(); // pc.setModuleName(scanner("模块名")); pc.setParent("com.ds.book"); mpg.setPackageInfo(pc); // 自定义配置 InjectionConfig cfg = new InjectionConfig() { @Override public void initMap() { // to do nothing } }; // 如果模板引擎是 freemarker String templatePath = "/templates/mapper.xml.ftl"; // 如果模板引擎是 velocity // String templatePath = "/templates/mapper.xml.vm"; // 自定义输出配置 List<FileOutConfig> focList = new ArrayList<>(); // 自定义配置会被优先输出 focList.add(new FileOutConfig(templatePath) { @Override public String outputFile(TableInfo tableInfo) { // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!! return projectPath + "/src/main/resources/mapper/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML; } }); /* cfg.setFileCreate(new IFileCreate() { @Override public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) { // 判断自定义文件夹是否需要创建 checkDir("调用默认方法创建的目录,自定义目录用"); if (fileType == FileType.MAPPER) { // 已经生成 mapper 文件判断存在,不想重新生成返回 false return !new File(filePath).exists(); } // 允许生成模板文件 return true; } }); */ cfg.setFileOutConfigList(focList); mpg.setCfg(cfg); // 配置模板 TemplateConfig templateConfig = new TemplateConfig(); // 配置自定义输出模板 //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别 // templateConfig.setEntity("templates/entity2.java"); // templateConfig.setService(); // templateConfig.setController(); templateConfig.setXml(null); mpg.setTemplate(templateConfig); // 策略配置 StrategyConfig strategy = new StrategyConfig(); strategy.setNaming(NamingStrategy.underline_to_camel); strategy.setColumnNaming(NamingStrategy.underline_to_camel); strategy.setTablePrefix("t_"); // strategy.setInclude("t_user"); // strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!"); strategy.setEntityLombokModel(true); strategy.setRestControllerStyle(true); // 公共父类 // strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!"); // 写于父类中的公共字段 strategy.setSuperEntityColumns("id"); strategy.setInclude(scanner("表名,多个英文逗号分割").split(",")); strategy.setControllerMappingHyphenStyle(true); // strategy.setTablePrefix(pc.getModuleName() + "_"); mpg.setStrategy(strategy); mpg.setTemplateEngine(new FreemarkerTemplateEngine()); mpg.execute(); } }
运行CodeGenerator,生成业务实体类
请输入表名,多个英文逗号分割: t_user,t_menu,t_role,t_user_role,t_role_menu
1)
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
认证管理
流程图解读:
1、用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
2、然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证 。
3、认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。
4、SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它 的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为 DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至Authentication。
授权管理
访问资源(即授权管理),访问url时,会通过FilterSecurityInterceptor拦截器拦截,其中会调用SecurityMetadataSource的方法来获取被拦截url所需的全部权限,再调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的投票策略(有:一票决定,一票否定,少数服从多数等),如果权限足够,则决策通过,返回访问资源,请求放行,否则跳转到403页面、自定义页面。
package com.ds.book.entity; import com.baomidou.mybatisplus.annotation.TableName; import java.io.Serializable; import java.util.Collection; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; /** * <p> * * </p> * * @author java大师 * @since 2023-03-17 */ @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("t_user") public class User implements Serializable, UserDetails { private static final long serialVersionUID = 1L; private Integer id; /** * 登录名 */ private String name; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 是否有效:1-有效;0-无效 */ private String status; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles .stream() .map(role -> new SimpleGrantedAuthority(role.getRoleCode())) .collect(Collectors.toList()); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
登录成功后,将UserDetails的roles设置到用户中
package com.ds.book.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.ds.book.entity.User; import com.ds.book.mapper.UserMapper; import com.ds.book.service.IUserService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; 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; /** * <p> * 服务实现类 * </p> * * @author java大师 * @since 2023-03-17 */ @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService, UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User loginUser = userMapper.selectOne(new QueryWrapper<User>().eq("username", username)); if (loginUser == null){ throw new UsernameNotFoundException("用户名或密码错误"); } loginUser.setRoles(userMapper.getRolesByUserId(loginUser.getId())); return loginUser; } }
将我们自己的UserDetailService注入springsecurity
package com.ds.book.config; import com.ds.book.filter.JwtTokenFilter; import com.ds.book.service.impl.UserServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.ObjectPostProcessor; 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.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserServiceImpl userService; @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } //注入我们自己的UserDetailService @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(passwordEncoder()); } }
问题:前后端分离项目,通常不会使用springsecurity自带的登录界面,登录界面由前端完成,后台只需要提供响应的服务即可,且目前主流不会采用session去存取用户,后端会返回响应的token,前端访问的时候,会在headers里面带入token.
Jwt token由Header、Payload、Signature三部分组成,这三部分之间以小数点”.”连接,JWT token长这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.keH6T3x1z7mmhKL1T3r9sQdAxxdzB6siemGMr_6ZOwU
token解析后长这样: header部分,有令牌的类型(JWT)和签名算法名称(HS256): { "alg": "HS256", "typ": "JWT" } Payload部分,有效负载,这部分可以放任何你想放的数据:
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
Signature签名部分,由于这部分是使用header和payload部分计算的,所以还可以以此来验证payload部分有没有被篡改:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
123456 //这里是密钥,只要够复杂,一般不会被破解
)
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
package com.ds.book.tool; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; import java.util.Date; import java.util.UUID; /** * JWT工具类 */ public class JwtUtil { //有效期为 public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时 //设置秘钥明文 public static final String JWT_KEY = "dashii"; public static String getUUID(){ String token = UUID.randomUUID().toString().replaceAll("-", ""); return token; } /** * 生成jtw * @param subject token中要存放的数据(json格式) * @return */ public static String createJWT(String subject) { JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间 return builder.compact(); } /** * 生成jtw * @param subject token中要存放的数据(json格式) * @param ttlMillis token超时时间 * @return */ public static String createJWT(String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间 return builder.compact(); } private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; SecretKey secretKey = generalKey(); long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if(ttlMillis==null){ ttlMillis= JwtUtil.JWT_TTL; } long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); return Jwts.builder() .setId(uuid) //唯一的ID .setSubject(subject) // 主题 可以是JSON数据 .setIssuer("dashi") // 签发者 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥 .setExpiration(expDate); } /** * 创建token * @param id * @param subject * @param ttlMillis * @return */ public static String createJWT(String id, String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间 return builder.compact(); } public static void main(String[] args) throws Exception { String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg"; Claims claims = parseJWT(token); System.out.println(claims); } /** * 生成加密后的秘钥 secretKey * @return */ public static SecretKey generalKey() { byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } /** * 解析 * * @param jwt * @return * @throws Exception */ public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } }
package com.ds.book.filter; import com.ds.book.entity.User; import com.ds.book.mapper.UserMapper; import com.ds.book.service.IMenuService; import com.ds.book.service.IUserService; import com.ds.book.tool.JwtUtil; import io.jsonwebtoken.Claims; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class JwtTokenFilter extends OncePerRequestFilter { @Autowired private IUserService userService; @Autowired private UserMapper userMapper; @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { //1、获取token String token = httpServletRequest.getHeader("token"); if (StringUtils.isEmpty(token)){ filterChain.doFilter(httpServletRequest,httpServletResponse); return; } String userId; try { Claims claims = JwtUtil.parseJWT(token); userId = claims.getSubject(); } catch (Exception exception) { exception.printStackTrace(); throw new RuntimeException("token非法"); } User user = userService.getUserById(Integer.parseInt(userId)); user.setRoles(userMapper.getRolesByUserId(Integer.parseInt(userId))); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); filterChain.doFilter(httpServletRequest,httpServletResponse); } }
在springsecurity中,第一个经过的过滤器是UsernamePasswordAuthenticationFilter,所以前后端分离的项目,我们自己定义的过滤器要放在这个过滤器前面,具体配置如下
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/login").permitAll() .anyRequest().authenticated(); http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); http.cors(); }
1)主启动类上添加EnableGlobalMethodSecurity注解
@EnableGlobalMethodSecurity(prePostEnabled = true) @SpringBootApplication @MapperScan("com.ds.book.mapper") public class BookSysApplication { public static void main(String[] args) { SpringApplication.run(BookSysApplication.class,args); } }
2)Controller方法上添加@PreAuthorize注解
@RestController public class HelloController { @GetMapping("/hello") @PreAuthorize("hasRole('ROLE_ADMIN')") public String hello(){ return "hello"; } }
1)创建我们自己的FilterInvocationSecurityMetadataSource,实现getAttributes方法,获取请求url所需要的角色
@Component public class MySecurtiMetaDataSource implements FilterInvocationSecurityMetadataSource { @Autowired private IMenuService menuService; AntPathMatcher antPathMatcher = new AntPathMatcher(); //获取访问url需要的角色,例如:/sys/user需要ROLE_ADMIN角色,访问sys/user时获取到必须要有ROLE_ADMIN角色。返回 Collection<ConfigAttribute> @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { String requestURI = ((FilterInvocation) object).getRequest().getRequestURI(); //获取所有的菜单及角色 List<Menu> menus = menuService.getMenus(); for (Menu menu : menus) { if (antPathMatcher.match(menu.getUrl(),requestURI)){ String[] roles = menu.getRoles().stream().map(role -> role.getRoleCode()).toArray(String[]::new); return SecurityConfig.createList(roles); } } return null; } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return false; } }
2)创建我们自己的决策管理器AccessDecisionManager,实现decide方法,判断步骤1)中获取到的角色和我们目前登录的角色是否相同,相同则允许访问,不相同则不允许访问,
@Component public class MyAccessDecisionManager implements AccessDecisionManager { //1、认证通过后,会往authentication中填充用户信息 //2、拿authentication中的权限与上一步获取到的角色信息进行比对,比对成功后,允许访问 @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); for (ConfigAttribute configAttribute : configAttributes) { for (GrantedAuthority authority : authorities) { if (authority.getAuthority().equals(configAttribute.getAttribute())){ return; } } } throw new AccessDeniedException("权限不足,请联系管理员"); } @Override public boolean supports(ConfigAttribute attribute) { return false; } @Override public boolean supports(Class<?> clazz) { return false; } }
3)在SecurityConfig中,添加后置处理器(增强器),让springsecurity使用我们自己的datametasource和decisionMananger
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MySecurtiMetaDataSource mySecurtiMetaDataSource; @Autowired private MyAccessDecisionManager myAccessDecisionManager; @Autowired private MyAuthenticationEntryPoint myAuthenticationEntryPoint; @Autowired private MyAccessDeniedHandler myAccessDeniedHandler; @Autowired private UserServiceImpl userService; @Autowired private JwtTokenFilter jwtTokenFilter; @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/login").permitAll() .anyRequest().authenticated() //后置处理器,使用我们自己的FilterSecurityInterceptor拦截器配置 .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor> () { @Override public <O extends FilterSecurityInterceptor> O postProcess(O o) { o.setSecurityMetadataSource(mySecurtiMetaDataSource); o.setAccessDecisionManager(myAccessDecisionManager); return o; } }) .and() .headers().cacheControl(); http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); http.cors(); } }
1)前端渲染工具类
public class WebUtils { /** * 将字符串渲染到客户端 * * @param response 渲染对象 * @param string 待渲染的字符串 * @return null */ public static String renderString(HttpServletResponse response, String string) { try { response.setStatus(200); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.getWriter().print(string); } catch (IOException e) { e.printStackTrace(); } return null; } }
2)未登录异常处理,实现commence方法
@Component public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { Result result = new Result(401,"未登录,请先登录",null); String json = JSON.toJSONString(result); WebUtils.renderString(httpServletResponse,json); } }
3)授权失败异常处理,实现Handle方法
@Component public class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { Result result = new Result(403,"权限不足请联系管理员",null); String s = JSON.toJSONString(result); WebUtils.renderString(httpServletResponse,s); } }
1)添加pom.xml依赖
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.7.0</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.7.0</version> </dependency> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>2.0.7</version> </dependency>
2)创建swagger配置文件
package com.ds.book.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.*; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spi.service.contexts.SecurityContext; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; import java.util.ArrayList; import java.util.List; @Configuration @EnableSwagger2 public class Swagger2Config { @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .pathMapping("/") .apiInfo(apiInfo()) .select() //swagger要扫描的包路径 .apis(RequestHandlerSelectors.basePackage("com.ds.book.controller")) .paths(PathSelectors.any()) .build() .securityContexts(securityContexts()) .securitySchemes(securitySchemes()); } private ApiInfo apiInfo() { return new ApiInfoBuilder().title("图书管理系统接口文档") //作者、路径和邮箱 .contact(new Contact("java大师","http://localhost:8080/doc.html","fry000@qq.com")) .version("1.0").description("图书管理接口文档").build(); } private List<SecurityContext> securityContexts() { //设置需要登录认证的路径 List<SecurityContext> result = new ArrayList<>(); result.add(getContextByPath("/.*")); return result; } //通过pathRegex获取SecurityContext对象 private SecurityContext getContextByPath(String pathRegex) { return SecurityContext.builder() .securityReferences(defaultAuth()) .forPaths(PathSelectors.regex(pathRegex)) .build(); } //默认为全局的SecurityReference对象 private List<SecurityReference> defaultAuth() { List<SecurityReference> result = new ArrayList<>(); AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; authorizationScopes[0] = authorizationScope; result.add(new SecurityReference("Authorization", authorizationScopes)); return result; } private List<ApiKey> securitySchemes() { //设置请求头信息 List<ApiKey> result = new ArrayList<>(); //设置header中的token ApiKey apiKey = new ApiKey("token", "token", "header"); result.add(apiKey); return result; } }
3)修改SecurityConfig配置类,允许访问swagger的地址
//主要的配置文件,antMatchers匹配的路径,全部忽略,不进行JwtToken的认证 @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers( "/login", "/logout", "/css/**", "/js/**", "/index.html", "favicon.ico", "/doc.html", "/webjars/**", "/swagger-resources/**", "/v2/api-docs/**" ); }
4)编写LoginController接口,通过@Api和@ApiOperation注解使用swagger
package com.ds.book.controller; import com.ds.book.entity.Result; import com.ds.book.entity.User; import com.ds.book.service.IUserService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.SecurityConfig; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController @Api(tags = "登录") public class LoginController { @Autowired private IUserService userService; @ApiOperation("登录") @PostMapping("/login") public Result login(@RequestBody User user){ return userService.login(user); } }
5)输入地址 http://localhost:8080/doc.html,进入swagger
6)点击登录进入登录接口,点击调试,发送
测试成功!
注意:前后端分离项目,退出的时候,由前端清除浏览器请求header中的token和sessionStorage或者LocalStorage,后端只要返回一个退出成功的消息。
package com.ds.book.controller; import com.ds.book.entity.Result; import com.ds.book.entity.User; import com.ds.book.service.IUserService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.SecurityConfig; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import java.security.Principal; @RestController @Api(tags = "登录") public class LoginController { @Autowired private IUserService userService; @Autowired private UserDetailsService userDetailsService; @ApiOperation("登录") @PostMapping("/login") public Result login(@RequestBody User user){ return userService.login(user); } @ApiOperation("退出") @PostMapping("/logout") public Result logout(){ return Result.success("退出成功"); } @ApiOperation("获取当前登录用户信息") @GetMapping("/user/info") public User user(Principal principal){ if (principal == null){ return null; } String username = principal.getName(); User user = (User)userDetailsService.loadUserByUsername(username); user.setPassword(null); return user; } }
package com.ds.book.controller; import com.ds.book.entity.Menu; import com.ds.book.entity.Result; import com.ds.book.service.IMenuService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.models.auth.In; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.List; /** * <p> * 前端控制器 * </p> * * @author java大师 * @since 2023-03-09 */ @RestController @Api(tags = "菜单管理") public class MenuController { @Autowired private IMenuService menuService; @GetMapping("/menus") @ApiOperation("获取菜单树") public Result getMenus(){ List<Menu> allMenus = menuService.getMenuTree(); return Result.success("查询成功",allMenus); } @PostMapping("/menu/add") @ApiOperation("添加菜单") public Result addMenu(@RequestBody Menu menu){ return menuService.addMenu(menu); } @PostMapping("/menu/update") @ApiOperation("修改菜单") public Result updateMenu(@RequestBody Menu menu){ return menuService.updateMenu(menu); } @PostMapping("/menu/delete/{id}") @ApiOperation("删除菜单") public Result deleteMenu(@PathVariable Integer id){ return menuService.deleteMenu(id); } }
package com.ds.book.controller; import com.ds.book.entity.Result; import com.ds.book.entity.User; import com.ds.book.service.IUserService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.models.auth.In; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; import javax.jws.soap.SOAPBinding; import java.util.List; /** * <p> * 前端控制器 * </p> * * @author java大师 * @since 2023-03-09 */ @RestController @Api(tags = "用户管理") public class UserController { @Autowired private IUserService userService; @Autowired private PasswordEncoder passwordEncoder; @GetMapping("/users") @ApiOperation("查询用户列表") public Result getUsers(){ List<User> list = userService.getUsers(); if (list != null){ return Result.success("查询成功",list); } return Result.error("查询失败"); } @PostMapping("/user/add") @ApiOperation("添加用户") public Result addUser(@RequestBody User user){ user.setPassword(passwordEncoder.encode("123456")); return userService.addUser(user); } @PostMapping("/user/update") @ApiOperation("修改用户") public Result updateUser(@RequestBody User user){ return userService.updateUser(user); } @PostMapping("/user/chooseRole/{userId}/{roleId}") @ApiOperation("选择角色") public Result chooseRole(@PathVariable Integer userId,@PathVariable Integer roleId){ return userService.chooseRole(userId,roleId); } @PostMapping("/user/delete/{id}") @ApiOperation("删除用户") public Result deleteUser(@PathVariable Integer id){ return userService.deleteUser(id); } }
package com.ds.book.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.ds.book.entity.Menu; import com.ds.book.entity.Result; import com.ds.book.entity.Role; import com.ds.book.entity.RoleMenu; import com.ds.book.mapper.RoleMapper; import com.ds.book.mapper.RoleMenuMapper; import com.ds.book.service.IRoleService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; /** * <p> * 服务实现类 * </p> * * @author java大师 * @since 2023-03-09 */ @Service public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements IRoleService { @Autowired private RoleMapper roleMapper; @Autowired private RoleMenuMapper roleMenuMapper; private List<Menu> buildMenuTree(List<Menu> menus, Integer parentId) { List<Menu> treeMenus = new ArrayList<>(); for (Menu menu : menus) { if (parentId==0 ? menu.getParentId()==0 : parentId.equals(menu.getParentId())) { List<Menu> children = buildMenuTree(menus, menu.getId()); if (!children.isEmpty()) { menu.setChildren(children); } treeMenus.add(menu); } } return treeMenus; } @Override public List getRoles() { List<Role> roles = roleMapper.getRoles(); for (Role role : roles) { role.setMenus(buildMenuTree(role.getMenus(),0)); } return roles; } @Override public Result chooseMenus(Integer roleId, Integer[] menuIds) { try { roleMenuMapper.delete(new QueryWrapper<RoleMenu>().eq("role_id",roleId)); for (Integer menuId : menuIds) { RoleMenu roleMenu = new RoleMenu(); roleMenu.setRoleId(roleId); roleMenu.setMenuId(menuId); roleMenuMapper.insert(roleMenu); } return Result.success("添加成功"); } catch (Exception exception) { return Result.error("添加失败"); } } }
vue create vue-book
选择Vue2,运行完毕,出现以下画面
执行绿色的命令,出现下列界面代表脚手架创建项目成功
//命令行安装 npm i element-ui -S //main.js使用element-ui import Vue from 'vue'; import ElementUI from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'; import App from './App.vue'; Vue.use(ElementUI); new Vue({ el: '#app', render: h => h(App) });
2.1安装依赖
npm install vue-router@3
2.2创建路由文件
import Vue from 'vue' import VueRouter from "vue-router"; Vue.use(VueRouter) //配置localhost:8080/跳转为登录页 const routes =[ { path:'/', name:'Login', component:() => import('@/pages/Login.vue') } ] export default new VueRouter({ routes })
4.1安装json-server
npm install -g json-server
4.2创建mock文件夹,新建db.json
{ "posts": [ { "id": 1, "title": "json-server", "author": "typicode" } ], "users": [ { "id": 1, "username": "admin", "password": "123" } ], "login": { "code": 200, "message":"返回成功", "data": { "id": "1237361915165020161", "username": "admin", "phone": "111111111111", "nickName": "javads", "realName": "javads", "sex": 1, "deptId": "1237322421447561216", "deptName": "测试部门", "status": 1, "email": "xxxx@qq.com", "token":"ASDSADASDSW121DDSA", "menus": [ { "id": "1236916745927790564", "title": "系统管理", "icon": "el-icon-star-off", "path": "/sys", "name": "Sys", "children": [ { "id": "1236916745927790578", "title": "角色管理", "icon": "el-icon-s-promotion", "path": "/sys/roles", "name": "Roles", "children": [] }, { "id": "1236916745927790560", "title": "菜单管理", "icon": "el-icon-s-tools", "path": "/sys/menus", "name": "Menus", "children": [] }, { "id": "1236916745927790575", "title": "用户管理", "icon": "el-icon-s-custom", "path": "/sys/users", "name": "User", "children": [] } ], "spread": true, "checked": false }, { "id": "1236916745927790569", "title": "账号管理", "icon": "el-icon-s-data", "path": "/account", "name": "Account", "children": [] } ], "permissions": [ "sys:log:delete", "sys:user:add", "sys:role:update", "sys:dept:list" ] } }, "comments": [ { "id": 1, "body": "some comment", "postId": 1 } ], "profile": { "name": "typicode" } }
4.3修改vue.config.js,json-server的默认端口为3000,将代理服务器的的端口改成3000
const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true, lintOnSave:false, devServer:{ proxy:{ '/api':{ target:'http://localhost:3000', pathRewrite:{'^/api':''}, ws:true, //不写为true,websocket changeOrigin:true //不写为true } } } })
4.4修改package.json,在scripts添加以下代码
"mock": "json-server src/mock/db.json --port 3000 --middlewares src/mock/middlewares.js"
4.5 运行json-server,出现以下界面代表运行成功
json-server.cmd --watch db.jso
5.1配置axios请求拦截器,新建utils文件夹,新建api.js,输入以下内容
import router from '../router' import axios from 'axios' import {Message} from 'element-ui' import {Loading} from 'element-ui' axios.defaults.baseURL = '/api' //添加遮罩层代码 let loading; let loadingNum = 0; //弹出遮罩层 function showLoading(){ if (loadingNum ===0){ loading = Loading.service({ lock:true, text:'加载中,请稍后...', background:'rgba(255,255,255,0.5)' }) } loadingNum++; } //关闭遮罩层 function hiddenLoading(){ loadingNum--; if (loadingNum <=0){ loading.close(); } } /** * 添加响应拦截器,在浏览器每次发请求之前,token放入http消息头当中 */ axios.interceptors.request.use(config =>{ showLoading(); if(window.sessionStorage.getItem('token')){ config.headers.Authorization =window.sessionStorage.getItem('token') } console.log(config) return config },error => { console.log(error) }) /** * 添加响应拦截器 */ axios.interceptors.response.use(success => { hiddenLoading(); if (success.status && success.status == 200){ if (success.data.code == 500 || success.data.code == 401 || success.data.code == 403) { Message.error({ offset:200, message:success.data.message }) router.replace("/") } if (success.data.message){ Message.success({ offset:200, message:success.data.message }) } } return success.data },error => { hiddenLoading(); if (error.response.code == 504 || error.response.code == 404) { Message.error({ message: '服务器跑路了' }); } else if (error.response.status == 403) { Message.error({ message: '权限不足,请联系管理员' }); } else if (error.response.code == 401) { Message.error({ message: '尚未登录,请先登录' }) router.replace('/'); } else { if (error.response.data.message) { Message.error({ message: error.response.data.message }); } else { Message.error({ message: '未知错误' }); } } return; }) export default axios
5.2创建请求接口,新建http.js
import axios from './api' export const login = (param) =>{ return axios.get(`/posts`, {param}) } export const getUser = () =>{ return axios.get(`/users`, {}) }
6.1登录界面
<template> <div class="login-container"> <el-form ref="form" :model="form" label-width="100px" class="login-form"> <h1 style="margin-bottom: 20px;text-align: center">欢迎登录</h1> <el-form-item label="用户名"> <el-input v-model="form.username"></el-input> </el-form-item> <el-form-item label="密码"> <el-input type="password" v-model="form.password"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="onSubmit">登录</el-button> <el-button>取消</el-button> </el-form-item> </el-form> </div> </template> <script> import {initRoutes} from "@/utils/routesUtil"; import {login,getUser} from "@/utils/http"; export default { name:'Login', data() { return { form: { username: '', password: '', } } }, methods: { onSubmit() { login(this.form).then(res=>{ if(res){ //浏览器中存储token,以后每次调用后端接口,浏览器都会带入这个token window.sessionStorage.setItem("token",res.data.token) //初始化路由数据 let myRoutes = initRoutes(res.data.menus) //将路由进行替换并添加到router中 this.$router.options.routes = [myRoutes] this.$router.addRoute(myRoutes) this.$router.replace("/home") }else{ return false } }) }, } } </script> <style scoped> .login-form { border: 1px #DCDFE6 solid; border-radius: 4px; padding: 40px; margin: 110px 400px; box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04); width: 400px; } .login-container { /*background: url(../assets/image/login2.jpg) no-repeat;*/ height: 100%; width: 100%; overflow: hidden; background-size: cover; } </style>
6.2处理后台请求返回工具类
export const initTmpRoutes = (menus) => { let tmpRoutes = [] menus.forEach(menu => { let {id,title,icon,path,name,children} = menu if(children instanceof Array){ children = initTmpRoutes(children) } let tmpRoute = { path:path, meta:{icon:icon,title:title}, name:name, children:children, component:children.length?{render(c){return c('router-view')}}:()=>import(`@/pages${path}/${name}.vue`) } console.log('tmpRoute',tmpRoute.path) tmpRoutes.push(tmpRoute) }) return tmpRoutes } export const initRoutes = (menus)=>{ const homeRoute = { path:'/home', name:'Home', meta:{title:'首页',icon: 'el-icon-star-off'}, component:() => import('@/pages/Home.vue'), } homeRoute.children = initTmpRoutes(menus); console.log('homeRoute',homeRoute) return homeRoute; }
6.3首页、导航页和主页
home.vue
<template> <div class="box"> <el-container style="height: 100%;" direction="vertial"> <el-aside width="200px"> <Nav/> </el-aside> <el-container> <el-header class="homeHeader"> <el-dropdown class="userInfo" @command="handlecommand"> <span class="el-dropdown-link"> </span> <el-dropdown-menu slot="dropdown"> <el-dropdown-item command="userInfo">个人中心</el-dropdown-item> <el-dropdown-item command="setting">设置</el-dropdown-item> <el-dropdown-item command="logout">退出</el-dropdown-item> </el-dropdown-menu> </el-dropdown> </el-header> <el-main> <Main/> </el-main> <el-footer>底部</el-footer> </el-container> </el-container> </div> </template> <script> import Nav from "@/components/Nav"; import Main from "@/components/Main"; import RecursiveMenu from "@/components/RecursiveMenu"; export default{ data(){ return { user:JSON.parse(window.sessionStorage.getItem('user')) } }, components:{ Nav, RecursiveMenu, Main }, methods:{ handlecommand(command){ if(command=='logout'){ this.$confirm('确定退出?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(()=>{ logout(); window.sessionStorage.removeItem('user'); window.sessionStorage.removeItem('token'); this.$store.commit('initRoutes',[]); this.$router.replace('/'); }).catch(()=>{ }) } } }, } </script> <style> #app, html, body, .box, .el-container{ padding: 0px; margin: 0px; height: 100%; } .el-header, .el-footer { background-color: #B3C0D1; color: #333; text-align: right; line-height: 60px; } .el-aside { background-color: #545C64; color: #333; text-align: center; line-height: 300px; } .el-main { background-color: #E9EEF3; color: #333; display: flex; flex-direction: column; } body>.el-container { margin-bottom: 40px; } .homeHeader .userInfo{ cursor: pointer; } .el-dropdown-link img{ width: 36px; height: 36px; border-radius: 18px; } </style>
Nav.vue
<template> <el-menu router> <template v-for="item in routes"> <el-submenu v-if="item.children.length" :index="item.path"> <template slot="title">{{ item.meta.title }}</template> <recursive-menu :menu="item.children"></recursive-menu> </el-submenu> <el-menu-item v-else :index="item.path">{{ item.meta.title }}</el-menu-item> </template> </el-menu> </template> <script> import RecursiveMenu from "@/components/RecursiveMenu"; export default { name: 'Nav', components:{ RecursiveMenu }, computed:{ routes(){ console.log('Nav routes:',this.$router.options.routes.length) // return this.$router.options.routes[1].children; return this.$router.options.routes; } } } </script>
RecursiveMenu.vue
<template> <div> <el-menu router> <template v-for="item in menu"> <el-submenu v-if="item.children.length" :index="item.path"> <template slot="title">{{ item.meta.title }}</template> <recursive-menu :menu="item.children"></recursive-menu> </el-submenu> <el-menu-item v-else :index="item.path">{{ item.meta.title }}</el-menu-item> </template> </el-menu> </div> </template> <script> export default { name: 'RecursiveMenu', props: { menu: { type: Array, required: true }, }, components: { RecursiveMenu: () => import('./RecursiveMenu.vue') } } </script>
可以看到左边的菜单和路由已经展示在浏览器中
注意:这里有一个坑,页面刷新以后,路由中的数据就会丢失,系统菜单会不显示
原因:页面刷新后,页面会重新实例化路由数据,因为是动态路由,所以页面刷新后会将router置为router/index.js配置的原始路由数据,所以匹配路由地址的时候会报错。
解决方法
思路:因为目前login接口返回的时候,直接将菜单数据传回前端,所以我们需要将菜单缓存起来,因为每次页面刷新vuex数据都会重置,所以不适合存储在vuex中,可以将菜单数据存储在sessionStorage中,页面刷新在实例化vue的created生命周期函数之前初始化路由即可
步骤
1)安装vuex
npm install vuex@3
2)修改登录页Login.vue
<template> <div class="login-container"> <el-form ref="form" :model="form" label-width="100px" class="login-form"> <h1 style="margin-bottom: 20px;text-align: center">欢迎登录</h1> <el-form-item label="用户名"> <el-input v-model="form.username"></el-input> </el-form-item> <el-form-item label="密码"> <el-input type="password" v-model="form.password"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="onSubmit">登录</el-button> <el-button>取消</el-button> </el-form-item> </el-form> </div> </template> <script> import {initRoutes} from "@/utils/routesUtil"; import {login,getUser} from "@/utils/http"; export default { name:'Login', data() { return { form: { username: '', password: '', } } }, methods: { onSubmit() { login(this.form).then(res=>{ if(res){ //将token和menus保存在vuex中 this.$store.dispatch("UPDATETOKEN",res.data.token); this.$store.dispatch("UPDATEUSERDATA",res.data.menus) //登录的时候,初始化菜单放在vuex中,不在登录页进行处理 this.$store.commit('INITROUTES',res.data.menus) // 以下代码为注释 // let myRoutes = initRoutes(res.data.menus) // this.$router.options.routes = [myRoutes] // this.$router.addRoute(myRoutes) this.$router.replace("/home") }else{ return false } }) }, } } </script> <style scoped> .login-form { border: 1px #DCDFE6 solid; border-radius: 4px; padding: 40px; margin: 110px 400px; box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04); width: 400px; } .login-container { /*background: url(../assets/image/login2.jpg) no-repeat;*/ height: 100%; width: 100%; overflow: hidden; background-size: cover; } </style>
3)创建store文件夹,创建index.js
import Vuex from 'vuex' import Vue from "vue"; import {initRoutes} from "@/utils/routesUtil"; import Router from "@/router"; Vue.use(Vuex) const state = { token:window.sessionStorage.getItem('token')||'', userData:window.sessionStorage.getItem('userData')||{}, routes:{} } const mutations = { SETTOKEN(state,token){ window.sessionStorage.setItem('token',token) state.token = token }, SETUSERDATA(state,userData){ window.sessionStorage.setItem('userData',JSON.stringify(userData)) state.userData = userData }, INITROUTES(state,menus){ let myRoutes = initRoutes(menus) Router.options.routes = [myRoutes] Router.addRoute(myRoutes); state.routes = myRoutes } } const actions = { UPDATETOKEN(context,value){ context.commit('SETTOKEN',value) }, UPDATEUSERDATA(context,value){ context.commit('SETUSERDATA',value) } } const getters = { userinfo(state){ return state.userData }, menus(state){ return state.userData.menus }, routes(state){ return state.routes.filter(item => { return item.name==='Home' })[0].children } } export default new Vuex.Store({ state, mutations, actions, getters })
4)main.js修改
import Vue from 'vue' import App from './App.vue' import ElementUI from 'element-ui' import router from './router' import 'element-ui/lib/theme-chalk/index.css' import store from "@/store" Vue.config.productionTip = false Vue.use(ElementUI) //生成路由,由于没有获取菜单接口,所以直接从sessionStorage中直接去userData数据,进行路由的初始化 const init = async ()=>{ if (sessionStorage.getItem('token')){ if (store.state.routes){ await store.commit('INITROUTES',JSON.parse(sessionStorage.getItem('userData'))) } } } //此处await不可缺少,需要等待路由数据先生成,才能进行vue实例的创建,否则会报错 async function call(){ await init(); new Vue({ render: h => h(App), router, store }).$mount('#app') } call()
5)如果未登录,则跳转到login页处理,main.js添加如下内容
//路由导航守卫,每次路由地址改变前出发 router.beforeEach((to,from,next)=>{ if (sessionStorage.getItem('token')) { next(); } else { //如果是登录页面路径,就直接next() if (to.path === '/login') { next(); } else { if(to.path === '/home'){ next(); } next('/login'); } } })
安装e-icon-picker选择器