后台权限管理系统记录

1 项目搭建

1.1 新建项目

在这里插入图片描述
Next
在这里插入图片描述

1.2 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>testspringboot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>testspringboot</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

总有一些莫名其妙的配置问题,直接替换该pom,省事!

1.3 application.yml

采用的数据库连接池是默认的Hikari。
将application.properties改名为application.yml,添加配置项:

server:
  port: 8090

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/authority?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  type: com.zaxxer.hikari.HikariDataSource
  hikari:
    ## 最小空闲连接数量
    minimum-idle: 5
    ## 空闲连接存活最大时间,默认600000(10分钟)
    idle-timeout: 180000
    ## 连接池最大连接数,默认是10
    maximum-pool-size: 500
    ## 此属性控制从池返回的连接的默认自动提交行为,默认值:true
    auto-commit: true
    ## 连接池母子
    pool-name: MyHikariCP
    ## 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
    max-lifetime: 1800000
    ## 数据库连接超时时间,默认30秒,即30000
    connection-timeout: 30000
    connection-test-query: SELECT 1
  servlet: #修改前端上传文件大小配置
    multipart:
      max-file-size: 500MB
      max-request-size: 1024MB
  mvc: #访问静态资源
    static-path-pattern: /static/**
    web:
      resources:
        static-locations: classpath:/static,classpath:/public,classpath:/resources,classpath:/META-INF/resources

mybatis: #resources下新建的放置mapper文件的文件夹应与此处同名
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.lzh.authoritysystem.model

1.4 index.html

resources下新建templates文件夹,创建index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" type="text/css" href="../static/css/mycss.css">
</head>
<body>
<h1>Authority System Test</h1>
<div></div>
<img src="../static/img/login/bg.png">
<script src="../static/js/myjs.js"></script>
<script>
    console.log(name)
</script>
</body>
</html>

4 配置Swagger

  1. 添加依赖
        <!--swagger2-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
  1. 添加配置文件SwaggerConfig
package com.lzh.authoritysystem.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
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.Collections;
import java.util.List;

@Configuration
@EnableSwagger2
public class SwaggerConfig  implements WebMvcConfigurer{

    @Bean
    public Docket controllerApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(new ApiInfoBuilder()
                        .title("AuthoritySystem Swagger")
                        .description("众里寻她千百度,蓦然回首,那人却在灯火阑珊处。")
                        .contact(new Contact("lzh", null, null))
                        .version("版本号:1.0")
                        .build())
                .groupName("test") //配置分组
//                .globalOperationParameters(globalOperationParameters()) //配置文档参数
                .enable(true) //enable是否启动Swagger,如果为False,则Swagger不能再浏览器中访问
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.lzh.authoritysystem.controller")) //扫描接口
                .paths(PathSelectors.any())
                .build()
                //下面两行为header中的Authorization认证,如不需要可不添加
                // ApiKey的name需与SecurityReference的reference保持一致
                .securityContexts(securityContext())
                .securitySchemes(securitySchemes());
    }

    /**
     * 设置 token 的两种方式
     * 方式一:接口单独传token,只需设置globalOperationParameters,
     * 方式二:设置全局token,需要设置securitySchemes和securityContexts.
     *
     * @return
     */
    private List<Parameter> globalOperationParameters(){
        ParameterBuilder paramBuilder = new ParameterBuilder()
                .name("token")
                .description("token令牌")
                .modelRef(new ModelRef("string"))
                .parameterType("header")
                .required(false); // false表示header中的token参数可以为空
        List<Parameter> params = new ArrayList<>();// 创建参数集合
        params.add(paramBuilder.build());
        return params;
    }
    private List<ApiKey> securitySchemes() {
        //设置请求头信息
        List<ApiKey> result = new ArrayList<>();
        ApiKey apiKey = new ApiKey("token", "token", "header");
        result.add(apiKey);
        return result;
    }
    private List<SecurityContext> securityContext() {
        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
        authorizationScopes[0] = authorizationScope;
        List<SecurityReference> securityReferenceList = new ArrayList<>();
        securityReferenceList.add(new SecurityReference("token", authorizationScopes));

        SecurityContext securityContext = SecurityContext.builder()
                .securityReferences(securityReferenceList)
                //.forPaths(PathSelectors.regex("/*.*"))
                .build();
        List<SecurityContext> securityContextList = new ArrayList<>();
        securityContextList.add(securityContext);
        return securityContextList;
    }


    /**
     * @Author: lequal
     * @Description 添加放行资源,解决无法访问 swagger-ui.html
     * @Date 2022/7/28 0028 10:28
     * @Param [registry]
     * @return void
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
        registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
}

特别的:为方法增加token

@RestController
@RequestMapping("header")
@Api(tags = "为单个接口添加header")
public class HeaderController {
    @GetMapping("token")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", name = "token", required = true),
    })
    @ApiOperation("token")
    public R<Void> header() {
        return R.ok();
    }
}

这种方法的缺点就是需要手动在每个方法上添加@ApiImplicitParam注解来指定header参数

  1. 常用注解
  • Api:写在Controller接口类上,对该Controller做整体说明
  • ApiOperation:写在Controller中的方法(某一具体接口)上,对该接口做说明
  • ApiParam:写在Controller中方法的参数上,对该参数做说明
@Api(tags = "controller注释")
@RestController
public class TestController {

    @ApiOperation("方法注释")
    @GetMapping("/testGet")
    public Map testGet(@RequestParam @ApiParam("参数注释") String name, @RequestParam List list){
        Map m = new HashMap();
        m.put("name", name);
        return m;
    }

}
  1. 访问文档
    在本地主机启动项目后,访问以下地址即可查看:http://localhost:8090/swagger-ui.html

5 工具类—封装后台接口通用返回

统一的封装,让代码看起来更加高级,避免每个接口返回乱糟糟,不统一。

  1. 通用返回工具类
package com.lzh.authoritysystem.utils;

import java.io.Serializable;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

public class ResponseResult<T> extends LinkedHashMap<String, Object> implements Serializable {
    private static final long serialVersionUID = 1L;
    private static final int CODE_SUCCESS = 200;
    private static final int CODE_ERROR = 500;
    private static final String CODE_STR = "code";
    private static final String MSG_STR = "msg";
    private static final String DATA_STR = "data";
    private static final String SUCCESS_MSG_STR = "操作成功";
    private static final String ERROR_MSG_STR = "操作失败";
    private T data;

    public ResponseResult() {
    }

    public ResponseResult(Object data, int code, String msg) {
        this.setCode(code);
        this.setMsg(msg);
        this.setData(data);
    }
    public ResponseResult(int code, String msg, T data) {
        this.setCode(code);
        this.setMsg(msg);
        this.setData(data);
    }

    public ResponseResult(Map<String, ?> map) {
        this.setMap(map);
    }

    public Integer getCode() {
        return (Integer)this.get(CODE_STR);
    }

    public String getMsg() {
        return (String)this.get(MSG_STR);
    }

    public Object getData() {
        return this.get(DATA_STR);
    }

    public ResponseResult<?> setCode(int code) {
        this.put(CODE_STR, code);
        return this;
    }

    public ResponseResult<?> setMsg(String msg) {
        this.put(MSG_STR, msg);
        return this;
    }

    public ResponseResult<?> setData(Object data) {
        this.put(DATA_STR, data);
        return this;
    }

    public ResponseResult<?> set(String key, Object data) {
        this.put(key, data);
        return this;
    }


    public ResponseResult<?> setMap(Map<String, ?> map) {
        Iterator var2 = map.keySet().iterator();

        while(var2.hasNext()) {
            String key = (String)var2.next();
            this.put(key, map.get(key));
        }

        return this;
    }

    public static ResponseResult<?> success() {
        return new ResponseResult<>(CODE_SUCCESS, SUCCESS_MSG_STR, (Object)null);
    }

    public static ResponseResult<String> success(String msg) {
        return new ResponseResult<>(CODE_SUCCESS, SUCCESS_MSG_STR, msg);
    }
    public static <T> ResponseResult<T> success(T obj) {
        return new ResponseResult<>(CODE_SUCCESS, SUCCESS_MSG_STR, obj);
    }

    public static <T> ResponseResult<T> error() {
        return new ResponseResult(CODE_ERROR, ERROR_MSG_STR, (Object)null);
    }

    public static ResponseResult<String> error(String msg) {
        return new ResponseResult<>(CODE_ERROR, ERROR_MSG_STR, msg);
    }
}
  1. 全局异常捕获工具类

异常不需要自己在controller里判断返回,写个全局异常捕获,如果代码执行过程发生异常,就能自己按照我们这个格式抛出。

package com.lzh.authoritysystem.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandle {
    /**
     * 系统异常
     */
    @ExceptionHandler(Exception.class)
    public ResponseResult<String> handleException(Exception e, HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        log.error("请求地址'"+ requestURI +"',发生系统异常: " + e.getMessage());
        return ResponseResult.error("'" + requestURI + "' --- " + e.getMessage());
    }
}
  1. 使用方式

ResponseResult.success(o);
ResponseResult.error(“'” + requestURI + "’ — " + e.getMessage());

@RestController
public class TestController {

    @ApiOperation("方法注释")
    @GetMapping("/testGet")
    public ResponseResult testGet(@RequestParam @ApiParam("参数注释") String name, @RequestParam List list){
        if(name.equals("123")){
            Map m = new HashMap();
            m.put("name", name);
            return ResponseResult.success(m);
        }
        int a = 1 / 0;
        return null;
    }

}

6 用户认证与授权

6.1 Spring Security介绍

一般Web应用的需要进行认证和授权。

  • 认证(Authentication):验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
  • 授权(Authorization):经过认证后判断当前用户是否有权限进行某个操作

而认证和授权就是Spring Security作为安全框架的核心功能。

Spring Security登录认证主要涉及两个重要的接口UserDetailServiceUserDetails接口。UserDetailService接口主要定义了一个方法loadUserByUsername(String username)用于完成用户信息的查询,其中username就是登录时的登录名称,登录认证时,需要自定义一个实现类实现UserDetailService接口,完成数据库查询,该接口返回UserDetail
UserDetail主要用于封装认证成功时的用户信息,即UserDetailService返回的用户信息,可以用Spring自己的User对象,但是最好是实现UserDetail接口,自定义用户对象。

6.2 Spring Security认证步骤

  1. 自定UserDetails类:当实体对象字段不满足时需要自定义UserDetails,一般都要自定义UserDetails
  2. 自定义UserDetailsService类,主要用于从数据库查询用户信息。
  3. 创建登录认证成功处理器,认证成功后需要返回JSON数据,菜单权限等。
  4. 创建登录认证失败处理器,认证失败需要返回JSON数据,给前端判断。
  5. 创建匿名用户访问无权限资源时处理器,匿名用户访问时,需要提示JSON
  6. 创建认证过的用户访问无权限资源时的处理器,无权限访问时,需要提示JSON
  7. 配置Spring Security配置类,把上面自定义的处理器交给Spring Security

6.3 Spring Security认证实现

6.3.1 添加依赖

        <!--security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--token-->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.17</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

6.3.2 自定义UserDetails实现类

当实体对象字段不满足时Spring Security认证时,需要自定义UserDetails

  1. 将User类实现UserDetails接口。(本文用继承类LoginUser实现该接口)
  2. 将原有的isAccountNonExpired、isAccountNonLocked、isCredentialsNonExpired和isEnabled属性修改成boolean类型,同时添加authorities属性。

注意: 上述4个属性只能是非包装类的boolean类型属性,且默认值设置为true。

package com.lzh.authoritysystem.dto;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

import com.lzh.authoritysystem.model.Permission;
import com.lzh.authoritysystem.model.SysUser;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;

import com.fasterxml.jackson.annotation.JsonIgnore;

@Data
public class LoginUser extends SysUser implements UserDetails {

	private static final long serialVersionUID = -1379274258881257107L;

	private List<Permission> permissions;
	
	@Override
	@JsonIgnore
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return permissions.parallelStream().filter(p -> !StringUtils.isEmpty(p.getPermission()))
				.map(p -> new SimpleGrantedAuthority(p.getPermission())).collect(Collectors.toSet());
	}

	public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
		// do nothing
	}

	// 账户是否未过期
	@JsonIgnore
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	// 账户是否未锁定
	@JsonIgnore
	@Override
	public boolean isAccountNonLocked() {
		return getStatus() != Status.LOCKED;
	}

	// 密码是否未过期
	@JsonIgnore
	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	// 账户是否激活
	@JsonIgnore
	@Override
	public boolean isEnabled() {
		return true;
	}
	
}

6.3.3 编写UserService接口、实现类

和正常mybatis使用套路一样,首先要提前写好mapper接口和xml文件

UserService接口:

package com.lzh.authoritysystem.service;


import com.lzh.authoritysystem.model.SysUser;

public interface UserService {

	/**
	 * 根据用户名查询用户信息
	 * @param username
	 * @return
	 */
	SysUser getUser(String username);

}

UserService接口实现类:

package com.lzh.authoritysystem.service.impl;

import java.util.List;

import com.lzh.authoritysystem.dao.UserDao;
import com.lzh.authoritysystem.model.SysUser;
import com.lzh.authoritysystem.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


@Service
public class UserServiceImpl implements UserService {

	@Autowired
	private UserDao userDao;

	/**
	 * 根据用户名查询用户信息
	 * @param username
	 * @return
	 */
	@Override
	public SysUser getUser(String username) {
		return userDao.getUser(username);
	}

}

6.3.4 自定义UserDetailsService接口实现类

package com.lzh.authoritysystem.service.impl;

import java.util.List;

import com.lzh.authoritysystem.dao.PermissionDao;
import com.lzh.authoritysystem.dto.LoginUser;
import com.lzh.authoritysystem.model.Permission;
import com.lzh.authoritysystem.model.SysUser;
import com.lzh.authoritysystem.service.UserService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
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;



/**
 * spring security登陆处理<br>
 * <p>
 * 密码校验请看文档(02 框架及配置),第三章第4节
 *
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

	@Autowired
	private UserService userService;
	@Autowired
	private PermissionDao permissionDao;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		SysUser sysUser = userService.getUser(username);
		if (sysUser == null) {
			throw new AuthenticationCredentialsNotFoundException("用户名不存在");
		} else if (sysUser.getStatus() == SysUser.Status.LOCKED) {
			throw new LockedException("用户被锁定,请联系管理员");
		} else if (sysUser.getStatus() == SysUser.Status.DISABLED) {
			throw new DisabledException("用户已作废");
		}

		LoginUser loginUser = new LoginUser();
		BeanUtils.copyProperties(sysUser, loginUser);
		
		// 获取用户权限
		List<Permission> permissions = permissionDao.listByUserId(sysUser.getId());
		loginUser.setPermissions(permissions);

		return loginUser;
	}

}

6.3.5 编写自定义理器

package com.lzh.authoritysystem.config;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.lzh.authoritysystem.dto.LoginUser;
import com.lzh.authoritysystem.utils.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;


/**
 * spring security处理器
 *
 */
@Configuration
public class SecurityHandlerConfig {

	/**
	 * 登陆认证成功处理器
	 * 
	 * @return
	 */
	@Bean
	public AuthenticationSuccessHandler loginSuccessHandler() {
		return new AuthenticationSuccessHandler() {

			@Override
			public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
					Authentication authentication) throws IOException, ServletException {
				//设置响应的编码格式
				response.setContentType("application/json;charset=utf-8");
				//获取当前登录用户信息
				LoginUser loginUser = (LoginUser) authentication.getPrincipal();
				//将对象转为JSON格式,并消除循环引用
				String result = JSON.toJSONString(ResponseResult.success(loginUser), SerializerFeature.DisableCircularReferenceDetect);
				//获取输出流
				ServletOutputStream outputStream = response.getOutputStream();
				outputStream.write(result.getBytes(StandardCharsets.UTF_8));
				outputStream.flush();
				outputStream.close();
			}
		};
	}

	/**
	 * 登陆认证失败处理器
	 * 
	 * @return
	 */
	@Bean
	public AuthenticationFailureHandler loginFailureHandler() {
		return new AuthenticationFailureHandler() {

			@Override
			public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
					AuthenticationException exception) throws IOException, ServletException {
				//设置响应的编码格式
				response.setContentType("application/json;charset=utf-8");
				//判断异常类型
				String msg = null;
				if(exception instanceof BadCredentialsException) {
					msg = "密码错误,登录失败!";
				}else if(exception instanceof AccountExpiredException){
					msg = "账户过期,登录失败!";
				}else if(exception instanceof CredentialsExpiredException){
					msg = "密码过期,登录失败!";
				}else if(exception instanceof DisabledException){
					msg = "账户被禁用,登录失败!";
				}else if(exception instanceof LockedException){
					msg = "账户被锁,登录失败!";
				}else if(exception instanceof InternalAuthenticationServiceException){
					msg = "账户不存在,登录失败!";
				}else {
					msg = exception.getMessage();
				}
				//将对象转为JSON格式
				String result = JSON.toJSONString(ResponseResult.error(msg));
				//获取输出流
				ServletOutputStream outputStream = response.getOutputStream();
				outputStream.write(result.getBytes(StandardCharsets.UTF_8));
				outputStream.flush();
				outputStream.close();
			}
		};

	}

	/**
	 * 未登录,匿名无权限访问
	 * 
	 * @return
	 */
	@Bean
	public AuthenticationEntryPoint authenticationEntryPoint() {
		return new AuthenticationEntryPoint() {

			@Override
			public void commence(HttpServletRequest request, HttpServletResponse response,
					AuthenticationException authException) throws IOException, ServletException {
				//设置响应的编码格式
				response.setContentType("application/json;charset=utf-8");
				//将对象转为JSON格式,并消除循环引用
				String result = JSON.toJSONString(ResponseResult.error("请先登录"));
				//获取输出流
				ServletOutputStream outputStream = response.getOutputStream();
				outputStream.write(result.getBytes(StandardCharsets.UTF_8));
				outputStream.flush();
				outputStream.close();
			}
		};
	}

	/**
	 * 退出处理器
	 * 
	 * @return
	 */
	@Bean
	public LogoutSuccessHandler logoutSussHandler() {
		return new LogoutSuccessHandler() {

			@Override
			public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
					Authentication authentication) throws IOException, ServletException {
				//设置响应的编码格式
				response.setContentType("application/json;charset=utf-8");
				//将对象转为JSON格式,并消除循环引用
				String result = JSON.toJSONString(ResponseResult.success( "退出成功"));
				//获取输出流
				ServletOutputStream outputStream = response.getOutputStream();
				outputStream.write(result.getBytes(StandardCharsets.UTF_8));
				outputStream.flush();
				outputStream.close();
			}
		};

	}

}

6.3.6 编写Spring Security配置类

package com.lzh.authoritysystem.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;


/**
 * spring security配置
 * 
 */
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	//登陆成功
	@Autowired
	private AuthenticationSuccessHandler authenticationSuccessHandler;
	//登陆失败
	@Autowired
	private AuthenticationFailureHandler authenticationFailureHandler;
	@Autowired
	private LogoutSuccessHandler logoutSuccessHandler;
	@Autowired
	private AuthenticationEntryPoint authenticationEntryPoint;
	@Autowired
	private UserDetailsService userDetailsService;

	/**
	 * 注入加密类
	 * @return
	 */
	@Bean
	public BCryptPasswordEncoder bCryptPasswordEncoder() {
		return new BCryptPasswordEncoder();
	}

	/**
	 * 处理登录认证
	 * @param http
	 * @throws Exception
	 */
	@Override
	protected void configure(HttpSecurity http) throws Exception {

		// 登陆处理过程
		http.formLogin() //表单登录
				.loginProcessingUrl("/login") //登录请求的url地址
				.successHandler(authenticationSuccessHandler) //认证成功处理器
				.failureHandler(authenticationFailureHandler) //认证失败处理器
				.usernameParameter("username") //前端输入框name属性,默认即为username
				.passwordParameter("password") //前端输入框name属性,默认即为password
				.and()
				.csrf().disable()
				.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //不创建session
				.and()
				.authorizeRequests() //设置需要拦截的请求
				.antMatchers("/", "/*.html", "/favicon.ico", "/css/**", "/js/**", "/fonts/**", "/layui/**", "/img/**",
						"/v2/api-docs/**", "/swagger-resources/**", "/webjars/**", "/pages/**", "/druid/**",
						"/static/**").permitAll() //登录请求方向(不拦截)
				.anyRequest().authenticated() //其他一律请求都要进行身份认证
				.and()
				.exceptionHandling()
				.authenticationEntryPoint(authenticationEntryPoint) //匿名无权限访问
				.and()
				.cors(); //支持跨域
		http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
		// 解决不允许显示在iframe的问题
		http.headers().frameOptions().disable();
		http.headers().cacheControl();

	}

	/**
	 * 配置认证处理器
	 * @param auth
	 * @throws Exception
	 */
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
	}

}

6.3.7 替换数据库中用户密码

因为数据库中密码为加密存储,因此需要自定义生成密码替换库中密码

@SpringBootTest
class AuthoritySystemApplicationTests {

    @Autowired
    BCryptPasswordEncoder bCryptPasswordEncoder;

    @Test
    void testConnection() throws Exception {
        System.out.println(bCryptPasswordEncoder.encode("123456"));
    }
}

IDEA控制台即可生成加密后的密码,根据加密规则,同一密码多次加密后的结果各不相同。

6.3.8 测试登录认证接口

6.4 认证成功后生成并返回token

Token是服务器端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登陆后,服务器生成一个token并将其返回给客户端,客户端每次请求数据需携带token

6.4.1 在application.yml中新增配置

token:
  expireSeconds: 60 #token过期秒数
  jwtSecret: "(XIAO:)_$^11244^%$_(WEI:)_@@++--(LAO:)_++++_.sds_(SHI:)" #私钥

6.4.2 定义返回接口类LoginResult

package com.lzh.authoritysystem.model;

import com.lzh.authoritysystem.dto.LoginUser;
import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Date;

@Data
@AllArgsConstructor
public class LoginResult{

	private static final long serialVersionUID = 4566334160572911795L;

	/**
	 * LoginUser,登陆成功返回的UserDeatisls
	 */
	private LoginUser loginUser;
	/**
	 * token
	 */
	private String token;
	/**
	 * 过期时间
	 */
	private Date expireTime;

}

6.4.3 工具类JwtUtils

package com.lzh.authoritysystem.utils;


import com.lzh.authoritysystem.dto.LoginUser;
import com.lzh.authoritysystem.model.SysUser;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
@Data
public class JwtUtils {

    @Value("${token.expireSeconds}")
    private Integer expireSeconds;
    @Value("${token.jwtSecret}")
    private String jwtSecret;

    /**
     * 从数据声明生成令牌
     *
     * @param claims
     * @return
     */
    private String generateToken(Map<String, Object> claims){
        Date expriationDate = new Date(System.currentTimeMillis() + expireSeconds * 1000);
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(expriationDate)
                .signWith(SignatureAlgorithm.HS512, jwtSecret)
                .compact();
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token
     * @return
     */
    public Claims getClaimsFromToken(String token){
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody();
        }catch (Exception e){
            claims = null;
        }
        return claims;
    }

    /**
     * 通过用户名生成令牌
     *
     * @param UserDetails接口实现类 loginUser
     * @return
     */
    public String generateToken(LoginUser loginUser){
        Map<String, Object> claims = new HashMap<>(2);
        claims.put(Claims.SUBJECT, loginUser.getUsername());
        claims.put(Claims.ISSUED_AT, new Date());
        return generateToken(claims);
    }

    /**
     * 从令牌中获取用户名
     *
     * @param token
     * @return
     */
    public String getUsernameFromToken(String token){
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        }catch (Exception e){
            username = null;
        }
        return username;
    }

    /**
     * 判断token是否过期
     *
     * @param token
     * @return
     */
    public Boolean isTokenExpired(String token){
        Claims claims = getClaimsFromToken(token);
        Date expiration = claims.getExpiration();
        return expiration.before(new Date());
    }

    /**
     * 刷新token
     *
     * @param token
     * @return
     */
    public String refreshToken(String token){
        String refreshToken;
        try{
            Claims claims = getClaimsFromToken(token);
            claims.put(Claims.ISSUED_AT, new Date());
            refreshToken = generateToken(claims);
        }catch (Exception e){
            refreshToken = null;
        }
        return refreshToken;
    }

    /**
     * 验证token是否有效
     *
     * @param token
     * @param UserDetails接口实现类 loginUser
     * @return
     */
    public Boolean validateToken(String token, LoginUser loginUser){
        SysUser sysUser = (SysUser) loginUser;
        String username = getUsernameFromToken(token);
        try{
            return (username.equals(sysUser.getUsername())) && !isTokenExpired(token);
        }catch (Exception e){
            return false;
        }
    }

}

6.4.4 在登陆认证成功处理器中加入返回类LoginResult

	@Autowired
	private JwtUtils jwtUtils;

	/**
	 * 登陆认证成功处理器
	 * 
	 * @return
	 */
	@Bean
	public AuthenticationSuccessHandler loginSuccessHandler() {
		return new AuthenticationSuccessHandler() {

			@Override
			public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
					Authentication authentication) throws IOException, ServletException {
				//设置响应的编码格式
				response.setContentType("application/json;charset=utf-8");
				//获取当前登录用户信息
				LoginUser loginUser = (LoginUser) authentication.getPrincipal();

				//生成token
				String token = jwtUtils.generateToken(loginUser);
				//设置token的签名密钥及过期时间
				long expireTime = Jwts.parser()
						.setSigningKey(jwtUtils.getJwtSecret()) //设置签名密钥
						.parseClaimsJws(token.replace("jwt_", ""))
						.getBody().getExpiration().getTime(); //设置过期时间
				LoginResult loginResult = new LoginResult(loginUser, token, new Date(expireTime));

				//将对象转为JSON格式,并消除循环引用
				String result = JSON.toJSONString(ResponseResult.success(loginResult), SerializerFeature.DisableCircularReferenceDetect);
				//获取输出流
				ServletOutputStream outputStream = response.getOutputStream();
				outputStream.write(result.getBytes(StandardCharsets.UTF_8));
				outputStream.flush();
				outputStream.close();
			}
		};
	}

6.4.5 测试

6.5 token认证

本案例使用数据库存储token

在这里插入图片描述

6.5.1 新增TokenModel

package com.lzh.authoritysystem.model;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Date;

@Data
@AllArgsConstructor
public class TokenModel {

	private static final long serialVersionUID = 4566334160572911795L;

	/**
	 * token
	 */
	private String token;
	/**
	 * LoginUser的json串
	 */
	private String userinfo;
	/**
	 * 过期时间
	 */
	private Date expireTime;

	private Date createTime;
	private Date updateTime;


}

6.5.2 新增TokenDao

package com.lzh.authoritysystem.dao;

import com.lzh.authoritysystem.model.TokenModel;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;



@Mapper
public interface TokenDao {

	@Insert("insert into t_token(token, userinfo, expireTime, createTime, updateTime) values (#{token}, #{userinfo}, #{expireTime}, #{createTime}, #{updateTime})")
	int save(TokenModel model);

	@Select("select * from t_token t where t.token = #{token}")
	TokenModel getByToken(String token);

	@Select("select * from t_token t where t.userinfo = #{userinfo}")
	TokenModel getByUserinfo(String userinfo);

	@Update("update t_token t set t.token = #{token}, t.expireTime = #{expireTime}, t.updateTime = #{updateTime} where t.userinfo = #{userinfo}")
	int updateByUserinfo(TokenModel model);

	@Delete("delete from t_token where token = #{token}")
	int delete(String token);
}

6.5.3 在登陆认证成功处理器中存储token

/**
	 * 登陆认证成功处理器
	 * 
	 * @return
	 */
	@Bean
	public AuthenticationSuccessHandler loginSuccessHandler() {
		return new AuthenticationSuccessHandler() {

			@Override
			public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
					Authentication authentication) throws IOException, ServletException {
				//设置响应的编码格式
				response.setContentType("application/json;charset=utf-8");
				//获取当前登录用户信息
				LoginUser loginUser = (LoginUser) authentication.getPrincipal();

				//生成token
				String token = jwtUtils.generateToken(loginUser);
				//设置token的签名密钥及过期时间
				long expireTime = Jwts.parser()
						.setSigningKey(jwtUtils.getJwtSecret()) //设置签名密钥
						.parseClaimsJws(token.replace("jwt_", ""))
						.getBody().getExpiration().getTime(); //设置过期时间
				LoginResult loginResult = new LoginResult(loginUser, token, new Date(expireTime));

				//token表在数据库中存储的用户信息
				String userinfo = loginUser.getId() + "-" + loginUser.getUsername();
				//判断数据库中是否有该用户登录的token
				TokenModel tokenModel = new TokenModel(token, userinfo, new Date(expireTime),
						new Date(), new Date());
				TokenModel tokenModelInDB = tokenDao.getByUserinfo(userinfo);
				if(tokenModelInDB != null){
					//更新数据库中的token
					tokenDao.updateByUserinfo(tokenModel);
				}else{
					//将token存储到数据库中
					tokenDao.save(tokenModel);
				}


				//将对象转为JSON格式,并消除循环引用
				String result = JSON.toJSONString(ResponseResult.success(loginResult), SerializerFeature.DisableCircularReferenceDetect);
				//获取输出流
				ServletOutputStream outputStream = response.getOutputStream();
				outputStream.write(result.getBytes(StandardCharsets.UTF_8));
				outputStream.flush();
				outputStream.close();
			}
		};
	}

6.5.4 token过滤器

package com.lzh.authoritysystem.utils;

import java.io.IOException;
import java.util.Date;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.alibaba.fastjson.JSON;
import com.lzh.authoritysystem.dao.TokenDao;
import com.lzh.authoritysystem.dto.LoginUser;
import com.lzh.authoritysystem.model.TokenModel;
import io.jsonwebtoken.Jwts;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;


/**
 * Token过滤器
 *
 */
@Component
public class TokenFilter extends OncePerRequestFilter {

	public static final String TOKEN_KEY = "token";

	@Autowired
	private TokenDao tokenDao;
	@Autowired
	private UserDetailsService userDetailsService;

	@Autowired
	private JwtUtils jwtUtils;

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		//根据请求参数或者header获取token
		String token = getToken(request);
		if (StringUtils.isNotBlank(token)) {
			//根据token表获取数据库中存储的token
			TokenModel tokenModel = tokenDao.getByToken(token);
			if (tokenModel != null){
				String username = tokenModel.getUserinfo().split("-")[1];
				LoginUser loginUser = (LoginUser) userDetailsService.loadUserByUsername(username);
				//判断token是否有效
				if(jwtUtils.validateToken(token, loginUser)){
					UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser,
							null, loginUser.getAuthorities());
					SecurityContextHolder.getContext().setAuthentication(authentication);
				}else{
					//删除无效token
					tokenDao.delete(token);
				}
			}
		}

		filterChain.doFilter(request, response);
	}

	/**
	 * 根据参数或者header获取token
	 * 
	 * @param request
	 * @return
	 */
	public static String getToken(HttpServletRequest request) {
		String token = request.getParameter(TOKEN_KEY);
		if (StringUtils.isBlank(token)) {
			token = request.getHeader(TOKEN_KEY);
		}

		return token;
	}

}

6.5.5 在SecurityConfig中配置token过滤器

	@Autowired
	private TokenFilter tokenFilter;

	/**
	 * 处理登录认证
	 * @param http
	 * @throws Exception
	 */
	@Override
	protected void configure(HttpSecurity http) throws Exception {

		// 登陆处理过程---省略,具体看上文

		//配置token过滤器
		http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);

	}

可在postman中测试不传token或者token过期,将返回500.

6.6 配置登陆页面

6.6.1 新建login.html

resources下新建templates文件夹,创建login.html

注意

  1. 登录操作的Ajax请求url要和Security配置类中的loginProcessingUrl保持一致
  2. location.href跳转时要携带token
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>登录</title>
<link href="../static/css/login.css" type="text/css" rel="stylesheet">
</head>
<body>

	<div class="login">
		<div class="message">后台管理系统</div>
		<div id="darkbannerwrap"></div>

		<form id="login-form" method="post" onsubmit="return false;">
			<input id="username" name="username" placeholder="用户名" type="text" autocomplete="off">
			<hr class="hr15">
			<input id="password" name="password" placeholder="密码" type="password" autocomplete="off">
			<hr class="hr15">
			<button style="width: 100%;" type="submit" onclick="login(this)">登录</button>
			<hr class="hr20">
			<span id="info" style="color: red"></span>
		</form>


	</div>

</body>
<script src="../static/js/libs/jquery-2.1.1.min.js"></script>
<script type="text/javascript">
	function login(obj) {
		$(obj).attr("disabled", true);

		var username = $.trim($('#username').val());
		var password = $.trim($('#password').val());
		if (username == "" || password == "") {
			$("#info").html('用户名或者密码不能为空');
			$(obj).attr("disabled", false);
		} else {
			$.ajax({
				type : 'post',
				url : '/login',
				data : $("#login-form").serialize(),
				success : function(data) {
					console.log(data);
					if(data.code == 200){
						// localStorage.setItem("token", data.data.token);
						location.href = '/goIndex?token=' + data.data.token;
					}else{
						console.log(data);
					}
				},
				error : function(xhr, textStatus, errorThrown) {
					var msg = xhr.responseText;
					var response = JSON.parse(msg);
					$("#info").html(response.message);
					$(obj).attr("disabled", false);
				}
			});

		}
	}
</script>
</html>

6.6.2 新建页面导航配置页

package com.lzh.authoritysystem.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 *以下代码的作用,就是省略了写方法跳转页面
 */

@Configuration
public class IntercepterConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("login");
        registry.addViewController("/goLogin").setViewName("login");
        registry.addViewController("/goIndex").setViewName("index");

    }
}

前端知识点

1 html5实现电子签名

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>电子签字</title>
  </head>
  <body>
    <canvas></canvas>
    <div>
      <button onclick="save()">保存</button>
      <button onclick="cancel()">取消</button>
    </div>
  </body>
</html>
<script>
  let canvas = document.querySelector('canvas')
  let config = {
    width: 500, // 宽度
    height: 200, // 高度
    lineWidth: 5, // 线宽
    strokeStyle: 'red', // 线条颜色
    lineCap: 'round', // 设置线条两端圆角
    lineJoin: 'round', // 线条交汇处圆角
  }
  canvas.width = config.width
  canvas.height = config.height
  // 设置一个边框,方便我们查看及使用
  canvas.style.border = '1px solid #000'
  // 创建上下文
  let ctx = canvas.getContext('2d')
 
  // 设置填充背景色
  ctx.fillStyle = 'transparent'
  // 绘制填充矩形
  ctx.fillRect(
    0, // x 轴起始绘制位置
    0, // y 轴起始绘制位置
    config.width, // 宽度
    config.height // 高度
  );
  // 保存上次绘制的 坐标及偏移量
  let client = {
    offsetX: 0, // 偏移量
    offsetY: 0,
    endX: 0, // 坐标
    endY: 0
  }
  // 判断是否为移动端
  let mobileStatus = (/Mobile|Android|iPhone/i.test(navigator.userAgent))
 
  // 初始化
  let inits = event => {
    // 获取偏移量及坐标
    let {
      offsetX,
      offsetY,
      pageX,
      pageY
    } = mobileStatus ? event.changedTouches[0] : event
 
    // 修改上次的偏移量及坐标
    client.offsetX = offsetX
    client.offsetY = offsetY
    client.endX = pageX
    client.endY = pageY
 
    // 清除以上一次 beginPath 之后的所有路径,进行绘制
    ctx.beginPath()
 
    // 根据配置文件设置进行相应配置
    ctx.lineWidth = config.lineWidth
    ctx.strokeStyle = config.strokeStyle
    ctx.lineCap = config.lineCap
    ctx.lineJoin = config.lineJoin
 
    // 设置画线起始点位
    ctx.moveTo(client.endX, client.endY)
 
    // 监听 鼠标移动或手势移动
    window.addEventListener(mobileStatus ? "touchmove" : "mousemove", draw)
  }
  // 创建鼠标/手势按下监听器
  window.addEventListener(mobileStatus ? "touchstart" : "mousedown", inits)
  // 绘制
  let draw = event => {
    // 获取当前坐标点位
    let {
      pageX,
      pageY
    } = mobileStatus ? event.changedTouches[0] : event
    // 修改最后一次绘制的坐标点
    client.endX = pageX
    client.endY = pageY
 
    // 根据坐标点位移动添加线条
    ctx.lineTo(pageX, pageY)
 
    // 绘制
    ctx.stroke()
  }
  // 结束绘制
  let cloaseDraw = () => {
    // 结束绘制
    ctx.closePath()
    // 移除鼠标移动或手势移动监听器
    window.removeEventListener("mousemove", draw)
  }
  // 创建鼠标/手势 弹起/离开 监听器
  window.addEventListener(mobileStatus ? "touchend" : "mouseup", cloaseDraw)
 
  // 取消-清空画布
  let cancel = () => {
    // 清空当前画布上的所有绘制内容
    ctx.clearRect(0, 0, config.width, config.height)
  }
 
  // 保存-将画布内容保存为图片
  let save = () => {
    // 将canvas上的内容转成blob流
    canvas.toBlob(blob => {
      console.log(blob);
      // // 获取当前时间并转成字符串,用来当做文件名
	  let date = Date.now().toString()
      // // 创建一个 a 标签
      let a = document.createElement('a')
      // // 设置 a 标签的下载文件名
      a.download = `${date}.png`
      // // 设置 a 标签的跳转路径为 文件流地址
      a.href = URL.createObjectURL(blob)
      // // 手动触发 a 标签的点击事件
      a.click()
      // // 移除 a 标签
      a.remove()
    })
  }
</script>

保存:下载图片

  • 22
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值