Spring Security JWT redis 笔记 && java 技术栈汇总
创建项目
登录 https://start.spring.io/ 下载
journey
w%6*YT)4%bbrMK(Z2F
修改 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>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.journey</groupId>
<artifactId>zcadmin</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ZC-ADMIN 后台管理</name>
<description>Journey's project for Spring Boot</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<log4jdbc.version>1.16</log4jdbc.version>
<swagger.version>2.9.2</swagger.version>
<fastjson.version>1.2.70</fastjson.version>
<druid.version>1.2.6</druid.version>
<commons-pool2.version>2.5.0</commons-pool2.version>
<mapstruct.version>1.3.1.Final</mapstruct.version>
<redisson.version>3.16.0</redisson.version>
<lock4j.version>2.2.1</lock4j.version>
<poi.version>4.1.2</poi.version>
<hutool.version>5.7.4</hutool.version>
<mybatis-plus.version>3.4.3.1</mybatis-plus.version>
<easyexcel.version>2.2.10</easyexcel.version>
</properties>
<dependencies>
<!--Spring boot 核心-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--Spring boot Web容器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Spring boot 测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--Spring boot 安全框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- spring boot 缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--Spring boot Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>lock4j-redisson-spring-boot-starter</artifactId>
<version>${lock4j.version}</version>
</dependency>
<!--spring boot 集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>${commons-pool2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--监控sql日志-->
<dependency>
<groupId>org.bgee.log4jdbc-log4j2</groupId>
<artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>
<version>${log4jdbc.version}</version>
</dependency>
<!-- Swagger UI 相关 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
<exclusions>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
</exclusion>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.21</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.5.21</version>
</dependency>
<!--Mysql依赖包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- druid数据源驱动 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>1.7.2</version>
</dependency>
<!--lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- excel工具 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>xerces</groupId>
<artifactId>xercesImpl</artifactId>
<version>2.12.0</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- Java图形验证码 -->
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>
<!-- 解析客户端操作系统、浏览器信息 -->
<dependency>
<groupId>nl.basjes.parse.useragent</groupId>
<artifactId>yauaa</artifactId>
<version>5.23</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>${easyexcel.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.2</version>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
创建module,名称为zcadmin-common(公共模块)
修改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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>zcadmin</artifactId>
<groupId>com.journey</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>zcadmin-common</artifactId>
<name>公共模块</name>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.0.Final</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
创建module,名称为zcadmin-logging(日志模块)
修改pom.xml,依赖 zcadmin-common
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>zcadmin</artifactId>
<groupId>com.journey</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>zcadmin-logging</artifactId>
<name>日志模块</name>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.journey</groupId>
<artifactId>zcadmin-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
创建module,名称为zcadmin-security
修改pom.xml,依赖zcadmin-logging
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>zcadmin</artifactId>
<groupId>com.journey</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>zcadmin-security</artifactId>
<name>认证授权模块</name>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<jjwt.version>0.11.1</jjwt.version>
<!-- oshi监控需要指定jna版本, 问题详见 https://github.com/oshi/oshi/issues/1040 -->
<oshi.version>5.8.0</oshi.version>
<jna.version>5.8.0</jna.version>
</properties>
<dependencies>
<dependency>
<groupId>com.journey</groupId>
<artifactId>zcadmin-logging</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- Spring boot websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
</dependency>
<!-- quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
</dependency>
<!-- linux的管理 -->
<dependency>
<groupId>ch.ethz.ganymed</groupId>
<artifactId>ganymed-ssh2</artifactId>
<version>build210</version>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
<!-- 获取系统信息 -->
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>${oshi.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.2</version>
</plugin>
</plugins>
</build>
</project>
创建module,名称为zcadmin-system
修改 pom.xml,依赖zcadmin-security
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>zcadmin</artifactId>
<groupId>com.journey</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>zcadmin-system</artifactId>
<name>核心模块</name>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<jjwt.version>0.11.1</jjwt.version>
<!-- oshi监控需要指定jna版本, 问题详见 https://github.com/oshi/oshi/issues/1040 -->
<oshi.version>5.8.0</oshi.version>
<jna.version>5.8.0</jna.version>
</properties>
<dependencies>
<!-- Spring boot websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
</dependency>
<!-- quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
</dependency>
<!-- linux的管理 -->
<dependency>
<groupId>ch.ethz.ganymed</groupId>
<artifactId>ganymed-ssh2</artifactId>
<version>build210</version>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
<!-- 获取系统信息 -->
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>${oshi.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.2</version>
</plugin>
</plugins>
</build>
</project>
在zcadmin-security模块下分别创建config,controller,domain,security,service包。
在controller中创建 AuthorizationController 系统授权接口,主要用于授权、根据token获取用户详细信息。
创建验证码获取请求接口
/auth/code
使用匿名GET请求@AnonymousGetMapping
在zcadmin-common中创建annotation.rest包并且创建 AnonymousGetMapping Annotation
package com.journey.annotation.rest;
import com.journey.annotation.AnonymousAccess;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.lang.annotation.*;
@AnonymousAccess
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface AnonymousGetMapping {
/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class)
String name() default "";
/**
* Alias for {@link RequestMapping#value}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] value() default {};
/**
* Alias for {@link RequestMapping#path}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] path() default {};
/**
* Alias for {@link RequestMapping#params}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] params() default {};
/**
* Alias for {@link RequestMapping#headers}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] headers() default {};
/**
* Alias for {@link RequestMapping#consumes}.
*
* @since 4.3.5
*/
@AliasFor(annotation = RequestMapping.class)
String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] produces() default {};
}
创建 com.journey.annotation.AnonymousAccess
package com.journey.annotation;
import java.lang.annotation.*;
/**
* @author journey
* 用于标记匿名访问方法
*/
@Inherited
@Documented
@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AnonymousAccess {
}
- // 1. 判断是否启用验证码
- // 1.1 验证码信息
- // 2. 获取验证码
- // 3. 生成uuid
- // 4. 当验证码类型为 arithmetic时且长度 >= 2 时,captcha.text()的结果有几率为浮点型
- // 4.1. 解析生成后的验证码
- // 5. 将解析后的验证码value存入redis
- // 6. 返回uuid,验证码,enabled
用代码实现
public ResponseEntity<Object> getCode() {
// 1. 判断是否启用验证码
if (!loginProperties.getLoginCode().getEnabled()) {
// 1.1 验证码信息
Map<String, Object> imgResult = new HashMap<String, Object>(2) {{
put("enabled", 0);
}};
return ResponseEntity.ok(imgResult);
}
// 2. 获取验证码
Captcha captcha = loginProperties.getCaptcha();
// 3. 生成uuid
String uuid = securityProperties.getCodeKey() + IdUtil.simpleUUID();
// 4. 当验证码类型为 arithmetic时且长度 >= 2 时,captcha.text()的结果有几率为浮点型
String captchaValue = captcha.text();
if (captcha.getCharType() - 1 == LoginCodeEnum.arithmetic.ordinal() && captchaValue.contains(".")) {
// 4.1. 解析生成后的验证码
captchaValue = captchaValue.split("\\.")[0];
}
// 5. 将解析后的验证码value存入redis
redisUtils.set(uuid, captchaValue, loginProperties.getLoginCode().getExpiration(), TimeUnit.MINUTES);
// 6. 返回uuid,验证码,enabled
Map<String, Object> imgResult = new HashMap<String, Object>(2) {{
put("enabled", 1);
put("img", captcha.toBase64());
put("uuid", uuid);
}};
return ResponseEntity.ok(imgResult);
}
创建 loginProperties
package com.journey.config.bean;
import com.journey.exception.BadConfigurationException;
import com.journey.utils.StringUtils;
import com.wf.captcha.*;
import com.wf.captcha.base.Captcha;
import lombok.Data;
import java.awt.*;
import java.util.Objects;
/**
* 配置文件读取
*
* @author liaojinlong
* @date loginCode.length0loginCode.length0/6/10 17:loginCode.length6
*/
@Data
public class LoginProperties {
/**
* 账号单用户 登录
*/
private boolean singleLogin = false;
private LoginCode loginCode;
/**
* 用户登录信息缓存
*/
private boolean cacheEnable;
public boolean isSingleLogin() {
return singleLogin;
}
public boolean isCacheEnable() {
return cacheEnable;
}
/**
* 获取验证码生产类
*
* @return /
*/
public Captcha getCaptcha() {
if (Objects.isNull(loginCode)) {
loginCode = new LoginCode();
if (Objects.isNull(loginCode.getCodeType())) {
loginCode.setCodeType(LoginCodeEnum.arithmetic);
}
}
return switchCaptcha(loginCode);
}
/**
* 依据配置信息生产验证码
*
* @param loginCode 验证码配置信息
* @return /
*/
private Captcha switchCaptcha(LoginCode loginCode) {
Captcha captcha;
synchronized (this) {
switch (loginCode.getCodeType()) {
case arithmetic:
// 算术类型 https://gitee.com/whvse/EasyCaptcha
captcha = new FixedArithmeticCaptcha(loginCode.getWidth(), loginCode.getHeight());
// 几位数运算,默认是两位
captcha.setLen(loginCode.getLength());
break;
case chinese:
captcha = new ChineseCaptcha(loginCode.getWidth(), loginCode.getHeight());
captcha.setLen(loginCode.getLength());
break;
case chinese_gif:
captcha = new ChineseGifCaptcha(loginCode.getWidth(), loginCode.getHeight());
captcha.setLen(loginCode.getLength());
break;
case gif:
captcha = new GifCaptcha(loginCode.getWidth(), loginCode.getHeight());
captcha.setLen(loginCode.getLength());
break;
case spec:
captcha = new SpecCaptcha(loginCode.getWidth(), loginCode.getHeight());
captcha.setLen(loginCode.getLength());
break;
default:
throw new BadConfigurationException("验证码配置信息错误!正确配置查看 LoginCodeEnum ");
}
}
if(StringUtils.isNotBlank(loginCode.getFontName())){
captcha.setFont(new Font(loginCode.getFontName(), Font.PLAIN, loginCode.getFontSize()));
}
return captcha;
}
static class FixedArithmeticCaptcha extends ArithmeticCaptcha {
public FixedArithmeticCaptcha(int width, int height) {
super(width, height);
}
@Override
protected char[] alphas() {
// 生成随机数字和运算符
int n1 = num(1, 10), n2 = num(1, 10);
int opt = num(3);
// 计算结果
int res = new int[]{n1 + n2, n1 - n2, n1 * n2}[opt];
// 转换为字符运算符
char optChar = "+-x".charAt(opt);
this.setArithmeticString(String.format("%s%c%s=?", n1, optChar, n2));
this.chars = String.valueOf(res);
return chars.toCharArray();
}
}
}
创建 com.journey.exception.BadConfigurationException
package com.journey.exception;
/**
* 统一关于错误配置信息 异常
*
* @author: liaojinlong
* @date: 2020/6/10 18:06
*/
public class BadConfigurationException extends RuntimeException {
/**
* Constructs a new runtime exception with {@code null} as its
* detail message. The cause is not initialized, and may subsequently be
* initialized by a call to {@link #initCause}.
*/
public BadConfigurationException() {
super();
}
/**
* Constructs a new runtime exception with the specified detail message.
* The cause is not initialized, and may subsequently be initialized by a
* call to {@link #initCause}.
*
* @param message the detail message. The detail message is saved for
* later retrieval by the {@link #getMessage()} method.
*/
public BadConfigurationException(String message) {
super(message);
}
/**
* Constructs a new runtime exception with the specified detail message and
* cause. <p>Note that the detail message associated with
* {@code cause} is <i>not</i> automatically incorporated in
* this runtime exception's detail message.
*
* @param message the detail message (which is saved for later retrieval
* by the {@link #getMessage()} method).
* @param cause the cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A {@code null} value is
* permitted, and indicates that the cause is nonexistent or
* unknown.)
* @since 1.4
*/
public BadConfigurationException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructs a new runtime exception with the specified cause and a
* detail message of {@code (cause==null ? null : cause.toString())}
* (which typically contains the class and detail message of
* {@code cause}). This constructor is useful for runtime exceptions
* that are little more than wrappers for other throwables.
*
* @param cause the cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A {@code null} value is
* permitted, and indicates that the cause is nonexistent or
* unknown.)
* @since 1.4
*/
public BadConfigurationException(Throwable cause) {
super(cause);
}
/**
* Constructs a new runtime exception with the specified detail
* message, cause, suppression enabled or disabled, and writable
* stack trace enabled or disabled.
*
* @param message the detail message.
* @param cause the cause. (A {@code null} value is permitted,
* and indicates that the cause is nonexistent or unknown.)
* @param enableSuppression whether or not suppression is enabled
* or disabled
* @param writableStackTrace whether or not the stack trace should
* be writable
* @since 1.7
*/
protected BadConfigurationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
创建 com.journey.utils.StringUtils
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.journey.utils;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import nl.basjes.parse.useragent.UserAgent;
import nl.basjes.parse.useragent.UserAgentAnalyzer;
import org.lionsoul.ip2region.DataBlock;
import org.lionsoul.ip2region.DbConfig;
import org.lionsoul.ip2region.DbSearcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
/**
* @author Zheng Jie
* 字符串工具类, 继承org.apache.commons.lang3.StringUtils类
*/
public class StringUtils extends org.apache.commons.lang3.StringUtils {
private static final Logger log = LoggerFactory.getLogger(StringUtils.class);
private static boolean ipLocal = false;
private static File file = null;
private static DbConfig config;
private static final char SEPARATOR = '_';
private static final char YAML_SEPARATOR = '-';
private static final String UNKNOWN = "unknown";
private static final UserAgentAnalyzer userAgentAnalyzer = UserAgentAnalyzer
.newBuilder()
.hideMatcherLoadStats()
.withCache(10000)
.withField(UserAgent.AGENT_NAME_VERSION)
.build();
static {
SpringContextHolder.addCallBacks(() -> {
StringUtils.ipLocal = SpringContextHolder.getProperties("ip.local-parsing", false, Boolean.class);
if (ipLocal) {
/*
* 此文件为独享 ,不必关闭
*/
String path = "ip2region/ip2region.db";
String name = "ip2region.db";
try {
config = new DbConfig();
file = FileUtil.inputStreamToFile(new ClassPathResource(path).getInputStream(), name);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
});
}
/**
* Yaml key 驼峰命名法工具
*
* @return yamlToCamelCase(" hello-world ") == "helloWorld"
*/
public static String yamlToCamelCase(String s) {
if (s == null) {
return null;
}
if(StringUtils.containsAny(s, YAML_SEPARATOR)) {
s = s.toLowerCase();
StringBuilder sb = new StringBuilder(s.length());
boolean yamlUpperCase = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == YAML_SEPARATOR) {
yamlUpperCase = true;
} else if (yamlUpperCase) {
sb.append(Character.toUpperCase(c));
yamlUpperCase = false;
} else {
sb.append(c);
}
}
return sb.toString();
}
return s;
}
/**
* 驼峰命名法工具
*
* @return toCamelCase(" hello_world ") == "helloWorld"
* toCapitalizeCamelCase("hello_world") == "HelloWorld"
* toUnderScoreCase("helloWorld") = "hello_world"
*/
public static String toCamelCase(String s) {
if (s == null) {
return null;
}
s = s.toLowerCase();
StringBuilder sb = new StringBuilder(s.length());
boolean upperCase = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == SEPARATOR) {
upperCase = true;
} else if (upperCase) {
sb.append(Character.toUpperCase(c));
upperCase = false;
} else {
sb.append(c);
}
}
return sb.toString();
}
/**
* 驼峰命名法工具
*
* @return toCamelCase(" hello_world ") == "helloWorld"
* toCapitalizeCamelCase("hello_world") == "HelloWorld"
* toUnderScoreCase("helloWorld") = "hello_world"
*/
public static String toCapitalizeCamelCase(String s) {
if (s == null) {
return null;
}
s = toCamelCase(s);
return s.substring(0, 1).toUpperCase() + s.substring(1);
}
/**
* 驼峰命名法工具
*
* @return toCamelCase(" hello_world ") == "helloWorld"
* toCapitalizeCamelCase("hello_world") == "HelloWorld"
* toUnderScoreCase("helloWorld") = "hello_world"
*/
static String toUnderScoreCase(String s) {
if (s == null) {
return null;
}
StringBuilder sb = new StringBuilder();
boolean upperCase = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
boolean nextUpperCase = true;
if (i < (s.length() - 1)) {
nextUpperCase = Character.isUpperCase(s.charAt(i + 1));
}
if ((i > 0) && Character.isUpperCase(c)) {
if (!upperCase || !nextUpperCase) {
sb.append(SEPARATOR);
}
upperCase = true;
} else {
upperCase = false;
}
sb.append(Character.toLowerCase(c));
}
return sb.toString();
}
/**
* 获取ip地址
*/
public static String getIp(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
String comma = ",";
String localhost = "127.0.0.1";
if (ip.contains(comma)) {
ip = ip.split(",")[0];
}
if (localhost.equals(ip)) {
// 获取本机真正的ip地址
try {
ip = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
log.error(e.getMessage(), e);
}
}
return ip;
}
/**
* 根据ip获取详细地址
*/
public static String getCityInfo(String ip) {
if (ipLocal) {
return getLocalCityInfo(ip);
} else {
return getHttpCityInfo(ip);
}
}
/**
* 根据ip获取详细地址
*/
public static String getHttpCityInfo(String ip) {
String api = String.format(ElAdminConstant.Url.IP_URL, ip);
JSONObject object = JSONUtil.parseObj(HttpUtil.get(api));
return object.get("addr", String.class);
}
/**
* 根据ip获取详细地址
*/
public static String getLocalCityInfo(String ip) {
try {
DataBlock dataBlock = new DbSearcher(config, file.getPath())
.binarySearch(ip);
String region = dataBlock.getRegion();
String address = region.replace("0|", "");
char symbol = '|';
if (address.charAt(address.length() - 1) == symbol) {
address = address.substring(0, address.length() - 1);
}
return address.equals(ElAdminConstant.REGION) ? "内网IP" : address;
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return "";
}
public static String getBrowser(HttpServletRequest request) {
UserAgent.ImmutableUserAgent userAgent = userAgentAnalyzer.parse(request.getHeader("User-Agent"));
return userAgent.get(UserAgent.AGENT_NAME_VERSION).getValue();
}
/**
* 获得当天是周几
*/
public static String getWeekDay() {
String[] weekDays = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
Calendar cal = Calendar.getInstance();
cal.setTime(new Date());
int w = cal.get(Calendar.DAY_OF_WEEK) - 1;
if (w < 0) {
w = 0;
}
return weekDays[w];
}
/**
* 获取当前机器的IP
*
* @return /
*/
public static String getLocalIp() {
try {
InetAddress candidateAddress = null;
// 遍历所有的网络接口
for (Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); interfaces.hasMoreElements();) {
NetworkInterface anInterface = interfaces.nextElement();
// 在所有的接口下再遍历IP
for (Enumeration<InetAddress> inetAddresses = anInterface.getInetAddresses(); inetAddresses.hasMoreElements();) {
InetAddress inetAddr = inetAddresses.nextElement();
// 排除loopback类型地址
if (!inetAddr.isLoopbackAddress()) {
if (inetAddr.isSiteLocalAddress()) {
// 如果是site-local地址,就是它了
return inetAddr.getHostAddress();
} else if (candidateAddress == null) {
// site-local类型的地址未被发现,先记录候选地址
candidateAddress = inetAddr;
}
}
}
}
if (candidateAddress != null) {
return candidateAddress.getHostAddress();
}
// 如果没有发现 non-loopback地址.只能用最次选的方案
InetAddress jdkSuppliedAddress = InetAddress.getLocalHost();
if (jdkSuppliedAddress == null) {
return "";
}
return jdkSuppliedAddress.getHostAddress();
} catch (Exception e) {
return "";
}
}
}
创建 com.journey.utils.SpringContextHolder
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.journey.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.env.Environment;
import java.util.ArrayList;
import java.util.List;
/**
* @author Jie
* @date 2019-01-07
*/
@Slf4j
public class SpringContextHolder implements ApplicationContextAware, DisposableBean {
private static ApplicationContext applicationContext = null;
private static final List<CallBack> CALL_BACKS = new ArrayList<>();
private static boolean addCallback = true;
/**
* 针对 某些初始化方法,在SpringContextHolder 未初始化时 提交回调方法。
* 在SpringContextHolder 初始化后,进行回调使用
*
* @param callBack 回调函数
*/
public synchronized static void addCallBacks(CallBack callBack) {
if (addCallback) {
SpringContextHolder.CALL_BACKS.add(callBack);
} else {
log.warn("CallBack:{} 已无法添加!立即执行", callBack.getCallBackName());
callBack.executor();
}
}
/**
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) {
assertContextInjected();
return (T) applicationContext.getBean(name);
}
/**
* 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
public static <T> T getBean(Class<T> requiredType) {
assertContextInjected();
return applicationContext.getBean(requiredType);
}
/**
* 获取SpringBoot 配置信息
*
* @param property 属性key
* @param defaultValue 默认值
* @param requiredType 返回类型
* @return /
*/
public static <T> T getProperties(String property, T defaultValue, Class<T> requiredType) {
T result = defaultValue;
try {
result = getBean(Environment.class).getProperty(property, requiredType);
} catch (Exception ignored) {}
return result;
}
/**
* 获取SpringBoot 配置信息
*
* @param property 属性key
* @return /
*/
public static String getProperties(String property) {
return getProperties(property, null, String.class);
}
/**
* 获取SpringBoot 配置信息
*
* @param property 属性key
* @param requiredType 返回类型
* @return /
*/
public static <T> T getProperties(String property, Class<T> requiredType) {
return getProperties(property, null, requiredType);
}
/**
* 检查ApplicationContext不为空.
*/
private static void assertContextInjected() {
if (applicationContext == null) {
throw new IllegalStateException("applicaitonContext属性未注入, 请在applicationContext" +
".xml中定义SpringContextHolder或在SpringBoot启动类中注册SpringContextHolder.");
}
}
/**
* 清除SpringContextHolder中的ApplicationContext为Null.
*/
private static void clearHolder() {
log.debug("清除SpringContextHolder中的ApplicationContext:"
+ applicationContext);
applicationContext = null;
}
@Override
public void destroy() {
SpringContextHolder.clearHolder();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (SpringContextHolder.applicationContext != null) {
log.warn("SpringContextHolder中的ApplicationContext被覆盖, 原有ApplicationContext为:" + SpringContextHolder.applicationContext);
}
SpringContextHolder.applicationContext = applicationContext;
if (addCallback) {
for (CallBack callBack : SpringContextHolder.CALL_BACKS) {
callBack.executor();
}
CALL_BACKS.clear();
}
SpringContextHolder.addCallback = false;
}
}
注入
@Resource
private LoginProperties loginProperties;
登录授权 /auth/login
使用匿名POST请求@AnonymousPostMapping
参数
username // NotBlank
password // NotBlank
code
uuid
- // 1. 密码解密
- // 2. 开启验证码
- // 2.1 查询redis验证码
- // 2.2 清除redis验证码
- // 2.3 判断验证码是否存在是否过期
- // 2.4 判断验证码是否正确
- // 3. 通过UsernamePasswordAuthenticationToken将用户名和密码作为参数创建authenticationToken
- // 4. 执行登录认证过程
- // 5. 认证成功存储认证信息到上下文
- // 6. 生成令牌
- // 7. 获取JwtUserDto
- // 8. 保存在线信息
- // 9. 返回 token 与 用户信息
public ResponseEntity<Object> login(@Validated @RequestBody AuthUserDto authUserDto, HttpServletRequest httpServletRequest) throws Exception {
// 1. 密码解密
String password = RsaUtils.decryptByPrivateKey(RsaProperties.privateKey, authUserDto.getPassword());
// 2. 开启验证码
if(loginProperties.getLoginCode().getEnabled()) {
// 2.1 查询redis验证码
String code = (String) redisUtils.get(authUserDto.getUuid());
// 2.2 清除redis验证码
redisUtils.del(authUserDto.getUuid());
// 2.3 判断验证码是否存在是否过期
if(StringUtils.isBlank(code)) {
throw new BadRequestException("验证码不存在或已过期");
}
// 2.4 判断验证码是否正确
if(StringUtils.isBlank(authUserDto.getCode()) || !authUserDto.getCode().equalsIgnoreCase(code)){
throw new BadRequestException("验证码错误");
}
}
// 3. 通过UsernamePasswordAuthenticationToken将用户名和密码作为参数创建authenticationToken
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(authUserDto.getUsername(), password);
// 4. 执行登录认证过程
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
// 5. 认证成功存储认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
// 6. 生成令牌
String token = tokenProvider.createToken(authentication);
// 7. 获取JwtUserDto
final JwtUserDto jwtUserDto = (JwtUserDto) authentication.getPrincipal();
// 8. 保存在线信息
onlineUserService.save(jwtUserDto, token, httpServletRequest);
// 9. 返回 token 与 用户信息
Map<String, Object> authInfo = new HashMap<String, Object>(2) {{
put("token", securityProperties.getTokenStartWith() + token);
put("user", jwtUserDto);
}};
if (loginProperties.isSingleLogin()) {
// 踢掉之前已经登录的token
onlineUserService.checkLoginOnUser(authUserDto.getUsername(), token);
}
return ResponseEntity.ok(authInfo);
}
创建 com.journey.service.dto.AuthUserDto
package com.journey.service.dto;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
/**
* @author Zheng Jie
* @date 2018-11-30
*/
@Getter
@Setter
public class AuthUserDto {
@NotBlank
private String username;
@NotBlank
private String password;
private String code;
private String uuid = "";
}
创建 com.journey.utils.RsaUtils (工具类,公钥私钥生成,加解密)
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* @author https://www.cnblogs.com/nihaorz/p/10690643.html
* @description Rsa 工具类,公钥私钥生成,加解密
* @date 2020-05-18
**/
public class RsaUtils {
private static final String SRC = "123456";
public static void main(String[] args) throws Exception {
System.out.println("\n");
RsaKeyPair keyPair = generateKeyPair();
System.out.println("公钥:" + keyPair.getPublicKey());
System.out.println("私钥:" + keyPair.getPrivateKey());
System.out.println("\n");
test1(keyPair);
System.out.println("\n");
test2(keyPair);
System.out.println("\n");
}
/**
* 公钥加密私钥解密
*/
private static void test1(RsaKeyPair keyPair) throws Exception {
System.out.println("***************** 公钥加密私钥解密开始 *****************");
String text1 = encryptByPublicKey(keyPair.getPublicKey(), RsaUtils.SRC);
String text2 = decryptByPrivateKey(keyPair.getPrivateKey(), text1);
System.out.println("加密前:" + RsaUtils.SRC);
System.out.println("加密后:" + text1);
System.out.println("解密后:" + text2);
if (RsaUtils.SRC.equals(text2)) {
System.out.println("解密字符串和原始字符串一致,解密成功");
} else {
System.out.println("解密字符串和原始字符串不一致,解密失败");
}
System.out.println("***************** 公钥加密私钥解密结束 *****************");
}
/**
* 私钥加密公钥解密
* @throws Exception /
*/
private static void test2(RsaKeyPair keyPair) throws Exception {
System.out.println("***************** 私钥加密公钥解密开始 *****************");
String text1 = encryptByPrivateKey(keyPair.getPrivateKey(), RsaUtils.SRC);
String text2 = decryptByPublicKey(keyPair.getPublicKey(), text1);
System.out.println("加密前:" + RsaUtils.SRC);
System.out.println("加密后:" + text1);
System.out.println("解密后:" + text2);
if (RsaUtils.SRC.equals(text2)) {
System.out.println("解密字符串和原始字符串一致,解密成功");
} else {
System.out.println("解密字符串和原始字符串不一致,解密失败");
}
System.out.println("***************** 私钥加密公钥解密结束 *****************");
}
/**
* 公钥解密
*
* @param publicKeyText 公钥
* @param text 待解密的信息
* @return /
* @throws Exception /
*/
public static String decryptByPublicKey(String publicKeyText, String text) throws Exception {
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyText));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
byte[] result = doLongerCipherFinal(Cipher.DECRYPT_MODE, cipher, Base64.decodeBase64(text));
return new String(result);
}
/**
* 私钥加密
*
* @param privateKeyText 私钥
* @param text 待加密的信息
* @return /
* @throws Exception /
*/
public static String encryptByPrivateKey(String privateKeyText, String text) throws Exception {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyText));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] result = doLongerCipherFinal(Cipher.ENCRYPT_MODE, cipher, text.getBytes());
return Base64.encodeBase64String(result);
}
/**
* 私钥解密
*
* @param privateKeyText 私钥
* @param text 待解密的文本
* @return /
* @throws Exception /
*/
public static String decryptByPrivateKey(String privateKeyText, String text) throws Exception {
PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyText));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] result = doLongerCipherFinal(Cipher.DECRYPT_MODE, cipher, Base64.decodeBase64(text));
return new String(result);
}
/**
* 公钥加密
*
* @param publicKeyText 公钥
* @param text 待加密的文本
* @return /
*/
public static String encryptByPublicKey(String publicKeyText, String text) throws Exception {
X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyText));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec2);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] result = doLongerCipherFinal(Cipher.ENCRYPT_MODE, cipher, text.getBytes());
return Base64.encodeBase64String(result);
}
private static byte[] doLongerCipherFinal(int opMode,Cipher cipher, byte[] source) throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
if (opMode == Cipher.DECRYPT_MODE) {
out.write(cipher.doFinal(source));
} else {
int offset = 0;
int totalSize = source.length;
while (totalSize - offset > 0) {
int size = Math.min(cipher.getOutputSize(0) - 11, totalSize - offset);
out.write(cipher.doFinal(source, offset, size));
offset += size;
}
}
out.close();
return out.toByteArray();
}
/**
* 构建RSA密钥对
*
* @return /
* @throws NoSuchAlgorithmException /
*/
public static RsaKeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
String publicKeyString = Base64.encodeBase64String(rsaPublicKey.getEncoded());
String privateKeyString = Base64.encodeBase64String(rsaPrivateKey.getEncoded());
return new RsaKeyPair(publicKeyString, privateKeyString);
}
/**
* RSA密钥对对象
*/
public static class RsaKeyPair {
private final String publicKey;
private final String privateKey;
public RsaKeyPair(String publicKey, String privateKey) {
this.publicKey = publicKey;
this.privateKey = privateKey;
}
public String getPublicKey() {
return publicKey;
}
public String getPrivateKey() {
return privateKey;
}
}
}
创建 com.journey.config.RsaProperties
@Data
@Component
public class RsaProperties {
public static String privateKey;
@Value("${rsa.private_key}")
public void setPrivateKey(String privateKey) {
RsaProperties.privateKey = privateKey;
}
}
创建 com.journey.utils.RedisUtils
package com.journey.utils;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisConnectionUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* @ClassName RedisUtils
* @Author journey
* @Date 2021/11/15 21:38
* @Version 1.0.0
* @Description
**/
@Component
@SuppressWarnings({"unchecked", "all"})
public class RedisUtils {
private static final Logger log = LoggerFactory.getLogger(RedisUtils.class);
private RedisTemplate<Object, Object> redisTemplate;
@Value("${jwt.online-key}")
private String onlineKey;
public RedisUtils(RedisTemplate<Object, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
return true;
}
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @param timeUnit 单位
*/
public boolean expire(String key, long time, TimeUnit timeUnit) {
try {
if (time > 0) {
redisTemplate.expire(key, time, timeUnit);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
return true;
}
/**
* 根据 key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(Object key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 查找匹配key
*
* @param pattern key
* @return /
*/
public List<String> scan(String pattern) {
ScanOptions options = ScanOptions.scanOptions().match(pattern).build();
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection rc = Objects.requireNonNull(factory).getConnection();
Cursor<byte[]> cursor = rc.scan(options);
List<String> result = new ArrayList<>();
while (cursor.hasNext()) {
result.add(new String(cursor.next()));
}
try {
RedisConnectionUtils.releaseConnection(rc, factory);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return result;
}
/**
* 分页查询 key
*
* @param patternKey key
* @param page 页码
* @param size 每页数目
* @return /
*/
public List<String> findKeysForPage(String patternKey, int page, int size) {
ScanOptions options = ScanOptions.scanOptions().match(patternKey).build();
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection rc = Objects.requireNonNull(factory).getConnection();
Cursor<byte[]> cursor = rc.scan(options);
List<String> result = new ArrayList<>(size);
int tmpIndex = 0;
int fromIndex = page * size;
int toIndex = page * size + size;
while (cursor.hasNext()) {
if (tmpIndex >= fromIndex && tmpIndex < toIndex) {
result.add(new String(cursor.next()));
tmpIndex++;
continue;
}
// 获取到满足条件的数据后,就可以退出了
if (tmpIndex >= toIndex) {
break;
}
tmpIndex++;
cursor.next();
}
try {
RedisConnectionUtils.releaseConnection(rc, factory);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return result;
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
public void del(String... keys) {
if (keys != null && keys.length > 0) {
if (keys.length == 1) {
boolean result = redisTemplate.delete(keys[0]);
log.debug("--------------------------------------------");
log.debug(new StringBuilder("删除缓存:").append(keys[0]).append(",结果:").append(result).toString());
log.debug("--------------------------------------------");
} else {
Set<Object> keySet = new HashSet<>();
for (String key : keys) {
keySet.addAll(redisTemplate.keys(key));
}
long count = redisTemplate.delete(keySet);
log.debug("--------------------------------------------");
log.debug("成功删除缓存:" + keySet.toString());
log.debug("缓存删除数量:" + count + "个");
log.debug("--------------------------------------------");
}
}
}
// ============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 批量获取
*
* @param keys
* @return
*/
public List<Object> multiGet(List<String> keys) {
List list = redisTemplate.opsForValue().multiGet(Sets.newHashSet(keys));
List resultList = Lists.newArrayList();
Optional.ofNullable(list).ifPresent(e-> list.forEach(ele-> Optional.ofNullable(ele).ifPresent(resultList::add)));
return resultList;
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间
* @param timeUnit 类型
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time, TimeUnit timeUnit) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, timeUnit);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
// ================================Map=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time);
}
return count;
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return /
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
return redisTemplate.opsForList().remove(key, count, value);
} catch (Exception e) {
log.error(e.getMessage(), e);
return 0;
}
}
/**
* @param prefix 前缀
* @param ids id
*/
public void delByKeys(String prefix, Set<Long> ids) {
Set<Object> keys = new HashSet<>();
for (Long id : ids) {
keys.addAll(redisTemplate.keys(new StringBuffer(prefix).append(id).toString()));
}
long count = redisTemplate.delete(keys);
// 此处提示可自行删除
log.debug("--------------------------------------------");
log.debug("成功删除缓存:" + keys.toString());
log.debug("缓存删除数量:" + count + "个");
log.debug("--------------------------------------------");
}
}
创建 com.journey.security.TokenProvider
package com.journey.security;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import com.journey.config.bean.SecurityProperties;
import com.journey.utils.RedisUtils;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.security.Key;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* @ClassName TokenProvider
* @Author journey
* @Date 2021/11/15 21:46
* @Version 1.0.0
* @Description
**/
@Slf4j
@Component
public class TokenProvider implements InitializingBean {
public static final String AUTHORITIES_KEY = "user";
private final SecurityProperties properties;
private final RedisUtils redisUtils;
private JwtBuilder jwtBuilder;
private JwtParser jwtParser;
public TokenProvider(SecurityProperties properties, RedisUtils redisUtils) {
this.properties = properties;
this.redisUtils = redisUtils;
}
/**
* 创建Token 设置永不过期,
* Token 的时间有效性转到Redis 维护
*
* @param authentication /
* @return /
*/
public String createToken(Authentication authentication) {
/*
* 获取权限列表
*/
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
return jwtBuilder
// 加入ID确保生成的 Token 都不一致
.setId(IdUtil.simpleUUID())
.claim(AUTHORITIES_KEY, authorities)
.setSubject(authentication.getName())
.compact();
}
@Override
public void afterPropertiesSet() throws Exception {
byte[] keyBytes = Decoders.BASE64.decode(properties.getBase64Secret());
Key key = Keys.hmacShaKeyFor(keyBytes);
jwtParser = Jwts.parserBuilder()
.setSigningKey(key)
.build();
jwtBuilder = Jwts.builder()
.signWith(key, SignatureAlgorithm.HS512);
}
/**
* 依据Token 获取鉴权信息
*
* @param token /
* @return /
*/
Authentication getAuthentication(String token) {
Claims claims = getClaims(token);
// fix bug: 当前用户如果没有任何权限时,在输入用户名后,刷新验证码会抛IllegalArgumentException
Object authoritiesStr = claims.get(AUTHORITIES_KEY);
Collection<? extends GrantedAuthority> authorities =
ObjectUtil.isNotEmpty(authoritiesStr) ?
Arrays.stream(authoritiesStr.toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList()) : Collections.emptyList();
User principal = new User(claims.getSubject(), "******", authorities);
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
public Claims getClaims(String token) {
return jwtParser
.parseClaimsJws(token)
.getBody();
}
/**
* @param token 需要检查的token
*/
public void checkRenewal(String token) {
// 判断是否续期token,计算token的过期时间
long time = redisUtils.getExpire(properties.getOnlineKey() + token) * 1000;
Date expireDate = DateUtil.offset(new Date(), DateField.MILLISECOND, (int) time);
// 判断当前时间与过期时间的时间差
long differ = expireDate.getTime() - System.currentTimeMillis();
// 如果在续期检查的范围内,则续期
if (differ <= properties.getDetect()) {
long renew = time + properties.getRenew();
redisUtils.expire(properties.getOnlineKey() + token, renew, TimeUnit.MILLISECONDS);
}
}
public String getToken(HttpServletRequest request) {
final String requestHeader = request.getHeader(properties.getHeader());
if (requestHeader != null && requestHeader.startsWith(properties.getTokenStartWith())) {
return requestHeader.substring(7);
}
return null;
}
}
获取用户信息接口
/auth/info GET 请求
public ResponseEntity<Object> getUserInfo(){
return ResponseEntity.ok(SecurityUtils.getCurrentUser());
}
创建SecurityUtils
并且新增getCurrentUser方法
/**
* 获取当前登录用户
* @return UserDetails
*/
public static UserDetails getCurrentUser(){
UserDetailsService userDetailsService = SpringContextHolder.getBean(UserDetailsService.class);
return userDetailsService.loadUserByUsername(getCurrentUsername());
}
/**
* 获取系统用户名称
*
* @return 系统用户名称
*/
public static String getCurrentUsername() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new BadRequestException(HttpStatus.UNAUTHORIZED, "当前登录状态过期");
}
if (authentication.getPrincipal() instanceof UserDetails) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return userDetails.getUsername();
}
throw new BadRequestException(HttpStatus.UNAUTHORIZED, "找不到当前登录的信息");
退出登录接口
/auth/logout 匿名DELETE请求
public ResponseEntity<Object> logout(HttpServletRequest request) {
onlineUserService.logout(tokenProvider.getToken(request));
return new ResponseEntity<>(HttpStatus.OK);
}
AuthorizationController 完整代码
package com.journey.controller;
import cn.hutool.core.util.IdUtil;
import com.journey.annotation.rest.AnonymousDeleteMapping;
import com.journey.annotation.rest.AnonymousGetMapping;
import com.journey.annotation.rest.AnonymousPostMapping;
import com.journey.config.RsaProperties;
import com.journey.config.bean.LoginCodeEnum;
import com.journey.config.bean.LoginProperties;
import com.journey.config.bean.SecurityProperties;
import com.journey.exception.BadRequestException;
import com.journey.security.TokenProvider;
import com.journey.service.OnlineUserService;
import com.journey.service.dto.AuthUserDto;
import com.journey.service.dto.JwtUserDto;
import com.journey.utils.RedisUtils;
import com.journey.utils.RsaUtils;
import com.journey.utils.SecurityUtils;
import com.journey.utils.StringUtils;
import com.wf.captcha.base.Captcha;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @ClassName AuthorizationController
* @Author journey
* @Date 2021/11/15 20:50
* @Version 1.0.0
* @Description 授权、根据token获取用户详细信息
**/
@Slf4j
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
@Api(tags = "系统:系统授权接口")
public class AuthorizationController {
private final RedisUtils redisUtils;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final TokenProvider tokenProvider;
private final OnlineUserService onlineUserService;
private final SecurityProperties securityProperties;
@Resource
private LoginProperties loginProperties;
@ApiOperation("获取验证码")
@AnonymousGetMapping(value = "/code")
public ResponseEntity<Object> getCode() {
// 1. 判断是否启用验证码
if (!loginProperties.getLoginCode().getEnabled()) {
// 1.1 验证码信息
Map<String, Object> imgResult = new HashMap<String, Object>(2) {{
put("enabled", 0);
}};
return ResponseEntity.ok(imgResult);
}
// 2. 获取验证码
Captcha captcha = loginProperties.getCaptcha();
// 3. 生成uuid
String uuid = securityProperties.getCodeKey() + IdUtil.simpleUUID();
// 4. 当验证码类型为 arithmetic时且长度 >= 2 时,captcha.text()的结果有几率为浮点型
String captchaValue = captcha.text();
if (captcha.getCharType() - 1 == LoginCodeEnum.arithmetic.ordinal() && captchaValue.contains(".")) {
// 4.1. 解析生成后的验证码
captchaValue = captchaValue.split("\\.")[0];
}
// 5. 将解析后的验证码value存入redis
redisUtils.set(uuid, captchaValue, loginProperties.getLoginCode().getExpiration(), TimeUnit.MINUTES);
// 6. 返回uuid,验证码,enabled
Map<String, Object> imgResult = new HashMap<String, Object>(2) {{
put("enabled", 1);
put("img", captcha.toBase64());
put("uuid", uuid);
}};
return ResponseEntity.ok(imgResult);
}
@ApiOperation("登录授权")
@AnonymousPostMapping(value = "/login")
public ResponseEntity<Object> login(@Validated @RequestBody AuthUserDto authUserDto, HttpServletRequest httpServletRequest) throws Exception {
// 1. 密码解密
String password = RsaUtils.decryptByPrivateKey(RsaProperties.privateKey, authUserDto.getPassword());
// 2. 开启验证码
if(loginProperties.getLoginCode().getEnabled()) {
// 2.1 查询redis验证码
String code = (String) redisUtils.get(authUserDto.getUuid());
// 2.2 清除redis验证码
redisUtils.del(authUserDto.getUuid());
// 2.3 判断验证码是否存在是否过期
if(StringUtils.isBlank(code)) {
throw new BadRequestException("验证码不存在或已过期");
}
// 2.4 判断验证码是否正确
if(StringUtils.isBlank(authUserDto.getCode()) || !authUserDto.getCode().equalsIgnoreCase(code)){
throw new BadRequestException("验证码错误");
}
}
// 3. 通过UsernamePasswordAuthenticationToken将用户名和密码作为参数创建authenticationToken
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(authUserDto.getUsername(), password);
// 4. 执行登录认证过程
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
// 5. 认证成功存储认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
// 6. 生成令牌
String token = tokenProvider.createToken(authentication);
// 7. 获取JwtUserDto
final JwtUserDto jwtUserDto = (JwtUserDto) authentication.getPrincipal();
// 8. 保存在线信息
onlineUserService.save(jwtUserDto, token, httpServletRequest);
// 9. 返回 token 与 用户信息
Map<String, Object> authInfo = new HashMap<String, Object>(2) {{
put("token", securityProperties.getTokenStartWith() + token);
put("user", jwtUserDto);
}};
if (loginProperties.isSingleLogin()) {
// 踢掉之前已经登录的token
onlineUserService.checkLoginOnUser(authUserDto.getUsername(), token);
}
return ResponseEntity.ok(authInfo);
}
@ApiOperation("获取用户信息")
@GetMapping("/info")
public ResponseEntity<Object> getUserInfo(){
return ResponseEntity.ok(SecurityUtils.getCurrentUser());
}
@ApiOperation("退出登录")
@AnonymousDeleteMapping(value = "/logout")
public ResponseEntity<Object> logout(HttpServletRequest request) {
onlineUserService.logout(tokenProvider.getToken(request));
return new ResponseEntity<>(HttpStatus.OK);
}
}
SecurityConfig 创建并继承 WebSecurityConfigurerAdapter 设定访问权限和登录方式
@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
// 去除 ROLE_ 前缀
return new GrantedAuthorityDefaults("");
}
@Bean
public PasswordEncoder passwordEncoder() {
// 密码加密方式
return new BCryptPasswordEncoder();
}
重写configure()并传入HttpSecurity参数
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
...
}
通过applicationContext.getBean("requestMappingHandlerMapping") requestMappingHandlerMapping.getHandlerMethods()查看应用请求对应的ur和方法
(RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping")
requestMappingHandlerMapping.getHandlerMethods();
新增 getAnonymousUrl 找出匿名标记
private Map<String, Set<String>> getAnonymousUrl(Map<RequestMappingInfo, HandlerMethod> handlerMethodMap) {
Map<String, Set<String>> anonymousUrls = new HashMap<>(6);
Set<String> get = new HashSet<>();
Set<String> post = new HashSet<>();
Set<String> put = new HashSet<>();
Set<String> patch = new HashSet<>();
Set<String> delete = new HashSet<>();
Set<String> all = new HashSet<>();
for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) {
HandlerMethod handlerMethod = infoEntry.getValue();
AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);
if (null != anonymousAccess) {
List<RequestMethod> requestMethods = new ArrayList<>(infoEntry.getKey().getMethodsCondition().getMethods());
RequestMethodEnum request = RequestMethodEnum.find(requestMethods.size() == 0 ? RequestMethodEnum.ALL.getType() : requestMethods.get(0).name());
switch (Objects.requireNonNull(request)) {
case GET:
get.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case POST:
post.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case PUT:
put.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case PATCH:
patch.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case DELETE:
delete.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
default:
all.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
}
}
}
anonymousUrls.put(RequestMethodEnum.GET.getType(), get);
anonymousUrls.put(RequestMethodEnum.POST.getType(), post);
anonymousUrls.put(RequestMethodEnum.PUT.getType(), put);
anonymousUrls.put(RequestMethodEnum.PATCH.getType(), patch);
anonymousUrls.put(RequestMethodEnum.DELETE.getType(), delete);
anonymousUrls.put(RequestMethodEnum.ALL.getType(), all);
return anonymousUrls;
}
// 禁用 CSRF
.csrf().disable()
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
// 授权异常
.exceptionHandling()
//当用户尝试访问安全的REST资源而不提供任何凭据时,将调用此方法发送401 响应
.authenticationEntryPoint(authenticationErrorHandler)
//当用户在没有授权的情况下访问受保护的REST资源时,将调用此方法发送403 Forbidden响应
.accessDeniedHandler(jwtAccessDeniedHandler)
// 防止iframe 造成跨域
.and()
.headers()
.frameOptions()
.disable()// 不创建会话
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 静态资源等等
.antMatchers(
HttpMethod.GET,
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/webSocket/**"
).permitAll()
// swagger 文档
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/*/api-docs").permitAll()
// 文件
.antMatchers("/avatar/**").permitAll()
.antMatchers("/file/**").permitAll()
// 阿里巴巴 druid
.antMatchers("/druid/**").permitAll()
// 放行OPTIONS请求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 自定义匿名访问所有url放行:允许匿名和带Token访问,细腻化到每个 Request 类型
// GET
.antMatchers(HttpMethod.GET, anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0])).permitAll()
// POST
.antMatchers(HttpMethod.POST, anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0])).permitAll()
// PUT
.antMatchers(HttpMethod.PUT, anonymousUrls.get(RequestMethodEnum.PUT.getType()).toArray(new String[0])).permitAll()
// PATCH
.antMatchers(HttpMethod.PATCH, anonymousUrls.get(RequestMethodEnum.PATCH.getType()).toArray(new String[0])).permitAll()
// DELETE
.antMatchers(HttpMethod.DELETE, anonymousUrls.get(RequestMethodEnum.DELETE.getType()).toArray(new String[0])).permitAll()
// 所有类型的接口都放行
.antMatchers(anonymousUrls.get(RequestMethodEnum.ALL.getType()).toArray(new String[0])).permitAll()
// 所有请求都需要认证
.anyRequest().authenticated()
.and().apply(securityConfigurerAdapter());
private TokenConfigurer securityConfigurerAdapter() {
return new TokenConfigurer(tokenProvider, securityProperties, onlineUserService, userCacheClean);
}
完整代码
package com.journey.config;
import com.journey.annotation.AnonymousAccess;
import com.journey.config.bean.SecurityProperties;
import com.journey.security.JwtAccessDeniedHandler;
import com.journey.security.JwtAuthenticationEntryPoint;
import com.journey.security.TokenConfigurer;
import com.journey.security.TokenProvider;
import com.journey.service.OnlineUserService;
import com.journey.service.UserCacheClean;
import com.journey.utils.enums.RequestMethodEnum;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
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.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.util.*;
/**
* @ClassName SecurityConfig
* @Author journey
* @Date 2021/11/15 22:15
* @Version 1.0.0
* @Description
**/
@Configuration
@EnableWebSecurity // 开启Spring Security
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final ApplicationContext applicationContext;
private final CorsFilter corsFilter;
private final JwtAuthenticationEntryPoint authenticationErrorHandler;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
private final TokenProvider tokenProvider;
private final SecurityProperties securityProperties;
private final OnlineUserService onlineUserService;
private final UserCacheClean userCacheClean;
@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
// 去除 ROLE_ 前缀
return new GrantedAuthorityDefaults("");
}
@Bean
public PasswordEncoder passwordEncoder() {
// 密码加密方式
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// 搜索匿名标记 url: @AnonymousAccess
RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping");
Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
// 获取匿名标记
Map<String, Set<String>> anonymousUrls = getAnonymousUrl(handlerMethodMap);
httpSecurity
// 禁用 CSRF
.csrf().disable()
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
// 授权异常
.exceptionHandling()
//当用户尝试访问安全的REST资源而不提供任何凭据时,将调用此方法发送401 响应
.authenticationEntryPoint(authenticationErrorHandler)
//当用户在没有授权的情况下访问受保护的REST资源时,将调用此方法发送403 Forbidden响应
.accessDeniedHandler(jwtAccessDeniedHandler)
// 防止iframe 造成跨域
.and()
.headers()
.frameOptions()
.disable()// 不创建会话
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 静态资源等等
.antMatchers(
HttpMethod.GET,
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/webSocket/**"
).permitAll()
// swagger 文档
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/*/api-docs").permitAll()
// 文件
.antMatchers("/avatar/**").permitAll()
.antMatchers("/file/**").permitAll()
// 阿里巴巴 druid
.antMatchers("/druid/**").permitAll()
// 放行OPTIONS请求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 自定义匿名访问所有url放行:允许匿名和带Token访问,细腻化到每个 Request 类型
// GET
.antMatchers(HttpMethod.GET, anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0])).permitAll()
// POST
.antMatchers(HttpMethod.POST, anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0])).permitAll()
// PUT
.antMatchers(HttpMethod.PUT, anonymousUrls.get(RequestMethodEnum.PUT.getType()).toArray(new String[0])).permitAll()
// PATCH
.antMatchers(HttpMethod.PATCH, anonymousUrls.get(RequestMethodEnum.PATCH.getType()).toArray(new String[0])).permitAll()
// DELETE
.antMatchers(HttpMethod.DELETE, anonymousUrls.get(RequestMethodEnum.DELETE.getType()).toArray(new String[0])).permitAll()
// 所有类型的接口都放行
.antMatchers(anonymousUrls.get(RequestMethodEnum.ALL.getType()).toArray(new String[0])).permitAll()
// 所有请求都需要认证
.anyRequest().authenticated()
.and().apply(securityConfigurerAdapter());
}
private Map<String, Set<String>> getAnonymousUrl(Map<RequestMappingInfo, HandlerMethod> handlerMethodMap) {
Map<String, Set<String>> anonymousUrls = new HashMap<>(6);
Set<String> get = new HashSet<>();
Set<String> post = new HashSet<>();
Set<String> put = new HashSet<>();
Set<String> patch = new HashSet<>();
Set<String> delete = new HashSet<>();
Set<String> all = new HashSet<>();
for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) {
HandlerMethod handlerMethod = infoEntry.getValue();
AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);
if (null != anonymousAccess) {
List<RequestMethod> requestMethods = new ArrayList<>(infoEntry.getKey().getMethodsCondition().getMethods());
RequestMethodEnum request = RequestMethodEnum.find(requestMethods.size() == 0 ? RequestMethodEnum.ALL.getType() : requestMethods.get(0).name());
switch (Objects.requireNonNull(request)) {
case GET:
get.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case POST:
post.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case PUT:
put.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case PATCH:
patch.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
case DELETE:
delete.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
default:
all.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
break;
}
}
}
anonymousUrls.put(RequestMethodEnum.GET.getType(), get);
anonymousUrls.put(RequestMethodEnum.POST.getType(), post);
anonymousUrls.put(RequestMethodEnum.PUT.getType(), put);
anonymousUrls.put(RequestMethodEnum.PATCH.getType(), patch);
anonymousUrls.put(RequestMethodEnum.DELETE.getType(), delete);
anonymousUrls.put(RequestMethodEnum.ALL.getType(), all);
return anonymousUrls;
}
private TokenConfigurer securityConfigurerAdapter() {
return new TokenConfigurer(tokenProvider, securityProperties, onlineUserService, userCacheClean);
}
}
创建 TokenConfigurer 继承 SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>
@RequiredArgsConstructor
public class TokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final TokenProvider tokenProvider;
private final SecurityProperties properties;
private final OnlineUserService onlineUserService;
private final UserCacheClean userCacheClean;
@Override
public void configure(HttpSecurity http) {
TokenFilter customFilter = new TokenFilter(tokenProvider, properties, onlineUserService, userCacheClean);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
使用generator工具生成domain以及对应的mapper与 mapper.xml
修改User,添加@Data(Lombok)注解,@Builder(Lombok),@AllArgsConstructor(Lombok),@NoArgsConstructor(Lombok),@Accessors(chain = true)(Lombok),@EqualsAndHashCode(callSuper = false)(Lombok),@TableName("sys_user")(Mybatisplus)
Lombok官网使用手册 https://projectlombok.org/features/all
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@TableName("sys_user")
@Data注解在类上,会为类的所有属性自动生成setter/getter、equals、canEqual、hashCode、toString方法,如为final属性,则不会为该属性生成setter方法。
@Data相当于@Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode这5个注解的合集。
@Builder https://blog.csdn.net/baidu_35085676/article/details/89193416
不需些太多的set方法来定义属性内容
例如
@Builder
public class Card {
private int id;
private String name;
private boolean sex;
}
Card card = Card.builder().id(10).name("dasd").sex(true).build();
@AllArgsConstructor
全参构造器
@NoArgsConstructor
无参构造器
@Accessors用法 https://www.cnblogs.com/liaozhenghan/p/13329854.html
翻译是存取器。通过该注解可以控制getter和setter方法的形式。
fluent 若为true,则getter和setter方法的方法名都是属性名,且setter方法返回当前对象。
@Data
@Accessors(fluent = true)
class User {
private Integer id;
private String name;
// 生成的getter和setter方法如下,方法体略
public Integer id(){}
public User id(Integer id){}
public String name(){}
public User name(String name){}
}
chain 若为true,则setter方法返回当前对象
@Data
@Accessors(chain = true)
class User {
private Integer id;
private String name;
// 生成的setter方法如下,方法体略
public User setId(Integer id){}
public User setName(String name){}
}
prefix 若为true,则getter和setter方法会忽视属性名的指定前缀(遵守驼峰命名)
@Data
@Accessors(prefix = "f")
class User {
private Integer fId;
private String fName;
// 生成的getter和setter方法如下,方法体略
public Integer id(){}
public void id(Integer id){}
public String name(){}
public void name(String name){}
}
@EqualsAndHashCode
- https://blog.csdn.net/zhanlanmg/article/details/50392266
- https://projectlombok.org/features/EqualsAndHashCode
- 此注解会生成equals(Object other) 和 hashCode()方法。
- 它默认使用非静态,非瞬态的属性
- 可通过参数exclude排除一些属性
- 可通过参数of指定仅使用哪些属性
- 它默认仅使用该类中定义的属性且不调用父类的方法
- 可通过callSuper=true解决上一点问题。让其生成的方法中调用父类的方法。
@NonNull
该注解用在属性或构造器上,Lombok会生成一个非空的声明,可用于校验参数,能帮助避免空指针。
Mybatis-Plus官网 https://mp.baomidou.com/
@TableName
属性 | 类型 | 必须指定 | 默认值 | 描述 |
---|---|---|---|---|
value | String | 否 | "" | 表名 |
schema | String | 否 | "" | schema |
keepGlobalPrefix | boolean | 否 | false | 是否保持使用全局的 tablePrefix 的值(如果设置了全局 tablePrefix 且自行设置了 value 的值) |
resultMap | String 否 | "" | xml | 中 |
autoResultMap | boolean | 否 | false | 是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建并注入) |
excludeProperty | String[] | 否 | {} | 需要排除的属性名(@since 3.3.1) |
@TableId 主键注解
属性 | 类型 | 必须指定 | 默认值 | 描述 |
---|---|---|---|---|
value | String | 否 | "" | 主键字段名 |
type | Enum | 否 | IdType.NONE | 主键类型 |
IdType:
值 | 描述 |
---|---|
AUTO | 数据库ID自增 |
NONE | 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT) |
INPUT | insert前自行set主键值 |
ASSIGN_ID | 分配ID(主键类型为Number(Long和Integer)或String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法) |
ASSIGN_UUID | 分配UUID,主键类型为String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认default方法) |
ID_WORKER | 分布式全局唯一ID 长整型类型(please use ASSIGN_ID) |
UUID | 32位UUID字符串(please use ASSIGN_UUID) |
ID_WORKER_STR | 分布式全局唯一ID 字符串类型(please use ASSIGN_ID) |
swagger 注解 https://blog.csdn.net/xiaojin21cen/article/details/78654652
@ApiModelProperty:用在JavaBean类的属性上面,说明属性的含义
完成的User代码
package com.journey.domain;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.journey.base.CommonEntity;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Date;
/**
* @author jinjin
* @date 2020-09-25
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@TableName("sys_user")
public class User extends CommonEntity<User> implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "ID")
@TableId(value = "user_id", type= IdType.AUTO)
@NotNull(groups = Update.class)
private Long id;
@ApiModelProperty(value = "部门名称")
private Long deptId;
@ApiModelProperty(value = "用户名")
@NotBlank
private String username;
@ApiModelProperty(value = "昵称")
private String nickName;
@ApiModelProperty(value = "性别")
private String gender;
@ApiModelProperty(value = "手机号码")
@NotBlank
private String phone;
@ApiModelProperty(value = "邮箱")
@NotBlank
private String email;
@ApiModelProperty(value = "头像地址")
private String avatarName;
@ApiModelProperty(value = "头像真实路径")
private String avatarPath;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "是否为admin账号")
private Boolean isAdmin;
@ApiModelProperty(value = "状态:1启用、0禁用")
private Boolean enabled;
@ApiModelProperty(value = "修改密码的时间")
private Date pwdResetTime;
public <T> void copyFrom(T source){
BeanUtil.copyProperties(source,this, CopyOptions.create().setIgnoreNullValue(true));
}
}
编辑 UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.journey.service.mapper.UserMapper">
<!-- 通用查询结果列 -->
<!--
<sql id="Base_Column_List">
user_id, dept_id, username, nick_name, gender, phone, email, avatar_name, avatar_path, password, is_admin, enabled, create_by, update_by, pwd_reset_time, create_time, update_time
</sql>
-->
</mapper>
编辑对应的 UserMapper.java
@Repository 与 @Mapper的区别 https://blog.csdn.net/qq_44421399/article/details/109825479
如果没有上述注解就会 ‘xxx.xxx.mapper.XxxxxMapper’ that could not be found 报错
@Repository 是 Spring 的注解,用于声明一个 Bean。不单独使用。spring中在mapper接口上写一个@Repository注解,当使用@MapperScannerConfigurer这个接口将被扫描。
@Repository
public interface UserMapper extends CommonMapper<User> {
...
}
Mybatis:
https://juejin.cn/post/6844903888856809479
这里使用Mybatis注解的方式操作数据库数据(MyBatis Dynamic SQL)
https://juejin.cn/post/6914073265140924429
https://mybatis.org/mybatis-dynamic-sql/docs/introduction.html
例如:
public interface UserMapperI {
@Insert("inset into users(name,age) values(#{name},#{age})")
public int add(User user);
@Delete("delete from users where id=#{id}")
public int delete(int id);
@Update("update users set name=#{name},age=#{age} where id=#{id}")
public int update(User user);
@Select("select * from users")
public List<User> getAll();
@Select("select * from users where id=#{id}")
public User getById(int id);
}
UserMapper 完整代码
@Repository
public interface UserMapper extends CommonMapper<User> {
/**
* ${ew.customSqlSegment}” (自定义sql段),wrapper不能为null
*/
@Results({
@Result(column = "dept_id", property = "dept", one = @One(select = "com.journey.service.mapper.DeptMapper.selectLink")),
@Result(column = "job_id", property = "job", one = @One(select = "com.journey.service.mapper.JobMapper.selectLink")), })
@Select("select u.user_id as id, u.* from sys_user u ${ew.customSqlSegment}")
User selectLink(@Param(Constants.WRAPPER) Wrapper<User> query);
/**
* 根据角色查询用户
*
* @param roleId /
* @return /
*/
@Select("SELECT u.user_id as id, u.* FROM sys_user u, sys_users_roles r WHERE" + " u.user_id = r.user_id AND r.role_id = #{roleId}")
List<User> findByRoleId(@Param("roleId") Long roleId);
/**
* 根据角色中的部门查询
*
* @param roleId /
* @return /
*/
@Select("SELECT u.user_id as id, u.* FROM sys_user u, sys_users_roles r, sys_roles_depts d WHERE "
+ "u.user_id = r.user_id AND r.role_id = d.role_id AND r.role_id = #{roleId} group by u.user_id")
List<User> findByDeptRoleId(@Param("roleId") Long roleId);
/**
* 根据菜单查询
*
* @param menuId 菜单ID
* @return /
*/
@Select("SELECT u.user_id as id, u.* FROM sys_user u, sys_users_roles ur, sys_roles_menus rm WHERE "
+ "u.user_id = ur.user_id AND ur.role_id = rm.role_id AND rm.menu_id = #{menuId} group by u.user_id")
List<User> findByMenuId(@Param("menuId") Long menuId);
/**
* 根据岗位查询
*
* @param ids /
* @return /
*/
@Select("<script>SELECT count(1) FROM sys_user u, sys_users_jobs j WHERE u.user_id = j.user_id AND j.job_id IN "
+ "<foreach item='item' index='index' collection='ids' open='(' separator=',' close=')'> #{item} </foreach>"
+ "</script>")
int countByJobs(@Param("ids") Set<Long> ids);
/**
* 根据部门查询
*
* @param deptIds /
* @return /
*/
@Select("<script>SELECT count(1) FROM sys_user u WHERE u.dept_id IN "
+ "<foreach item='item' index='index' collection='deptIds' open='(' separator=',' close=')'> #{item} </foreach>"
+ "</script>")
int countByDepts(@Param("deptIds") Set<Long> deptIds);
/**
* 根据角色查询
*
* @return /
*/
@Select("<script>SELECT count(1) FROM sys_user u, sys_users_roles r WHERE "
+ "u.user_id = r.user_id AND r.role_id in "
+ "<foreach item='item' index='index' collection='ids' open='(' separator=',' close=')'> #{item} </foreach>"
+ "</script>")
int countByRoles(@Param("ids") Set<Long> ids);
/**
* 根据角色中的部门查询
* @param deptId /
* @return /
*/
@Select("SELECT u.user_id as id, u.* FROM sys_user u, sys_users_roles r, sys_roles_depts d WHERE "
+ "u.user_id = r.user_id AND r.role_id = d.role_id AND u.dept_id = #{deptId} group by u.user_id")
List<User> findByRoleDeptId(@Param("deptId")Long deptId);
}
创建 com.journey.service.UserService 接口并集成 CommonService
创建 com.journey.base.CommonService 接口继承 IService
public interface CommonService<T> extends IService<T> {
}
创建 CommonService 接口实现类 CommonServiceImpl, CommonServiceImpl 实现 CommonServic 接口并继承 com.baomidou.mybatisplus.extension.service.impl.ServiceImpl 。
public abstract class CommonServiceImpl<M extends BaseMapper<T>, T> extends ServiceImpl<M, T> implements CommonService<T> {
}
编写 UserService 接口
public interface UserService extends CommonService<User> {
/**
* 查询数据分页
* @param query 条件
* @param pageable 分页参数
* @return PageInfo<UserDto>
*/
PageInfo<UserDto> queryAll(UserQueryParam query, Pageable pageable);
/**
* 查询所有数据不分页
* @param query 条件参数
* @return List<UserDto>
*/
List<UserDto> queryAll(UserQueryParam query);
User getById(Long id);
UserDto findById(Long id);
/**
* 根据用户名查询
* @param userName /
* @return /
*/
User getByUsername(String userName);
UserDto findByName(String userName);
/**
* 插入一条新数据。
*/
boolean save(UserDto resources);
boolean updateById(UserDto resources) throws Exception;
boolean removeById(Long id);
boolean removeByIds(Set<Long> ids);
/**
* 修改密码
* @param username 用户名
* @param encryptPassword 密码
*/
void updatePass(String username, String encryptPassword);
/**
* 修改头像
* @param file 文件
* @return /
*/
Map<String, String> updateAvatar(MultipartFile file);
/**
* 修改邮箱
* @param username 用户名
* @param email 邮箱
*/
void updateEmail(String username, String email);
/**
* 用户自助修改资料
* @param resources /
*/
void updateCenter(User resources);
/**
* 导出数据
* @param all 待导出的数据
* @param response /
* @throws IOException /
*/
void download(List<UserDto> all, HttpServletResponse response) throws IOException;
}
编辑 UserServiceImpl 接口实现类(UserServiceImpl 实现类必须有 @Service 注解)
@Service
@AllArgsConstructor
@CacheConfig(cacheNames = "user")
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class UserServiceImpl extends CommonServiceImpl<UserMapper, User> implements UserService {
private final FileProperties properties;
private final RedisUtils redisUtils;
private final UserCacheClean userCacheClean;
private final OnlineUserService onlineUserService;
private final UserMapper userMapper;
private final DeptService deptService;
private final UsersRolesService usersRolesService;
private final UsersJobsService usersJobsService;
private final UsersRolesMapper usersRolesMapper;
private final UsersJobsMapper usersJobsMapper;
@Override
//@Cacheable
public PageInfo<UserDto> queryAll(UserQueryParam query, Pageable pageable) {
// 1. 创建分页查询 IPage 对象 page
IPage<User> page = PageUtil.toMybatisPage(pageable);
// 2. 获取 pageData
IPage<User> pageData = userMapper.selectPage(page, QueryHelpMybatisPlus.getPredicate(query));
// 3. 将获取的 pageData 数据进行转换
List<UserDto> userDtos = ConvertUtil.convertList(pageData.getRecords(), UserDto.class);
// 4. 判断 pageData total 是否大于0
if (pageData.getTotal() > 0) {
Map<Long, DeptDto> deptMap = deptService.queryAll().parallelStream()
.collect(Collectors.toMap(DeptDto::getId, Function.identity(), (x,y) -> x));
Map<Long, Set<UsersRoles>> usersRolesMap = usersRolesService.lambdaQuery()
.in(UsersRoles::getUserId, userDtos.stream().map(UserDto::getId).collect(Collectors.toSet()))
.list()
.stream()
.collect(Collectors.groupingBy(UsersRoles::getUserId, Collectors.toSet()));
Map<Long, List<UsersJobs>> usersJobsMap = usersJobsService.lambdaQuery()
.in(UsersJobs::getUserId, userDtos.stream().map(UserDto::getId).collect(Collectors.toList()))
.list()
.stream()
.collect(Collectors.groupingBy(UsersJobs::getUserId));
// 5. 遍历 userDtos
userDtos.forEach(user -> {
// 5.1. 通过用户数据deptId 获取 deptMap,并将获取的数据进行转换后 set 到 UserDto 对象 user 中。
user.setDept(ConvertUtil.convert(deptMap.get(user.getDeptId()), DeptSmallDto.class));
// 5.2. 判断 usersRolesMap 是否存在用户 id
if (usersRolesMap.containsKey(user.getId())) {
// 5.2.1. 从 usersRolesMap 获取指定用户id对应的数据,并将数据 set 到 user 对象中。
user.setRoles(usersRolesMap.get(user.getId()).stream().map(ur -> {
RoleSmallDto role = new RoleSmallDto();
role.setId(ur.getRoleId());
return role;
}).collect(Collectors.toSet()));
}
// 5.3 判断 usersJobsMap 是否存在用户 id
if (usersJobsMap.containsKey(user.getId())) {
// 5.3.1. 从 usersJobsMap 获取指定用户id对应的数据,并将数据 set 到 user 对象中。
user.setJobs(usersJobsMap.get(user.getId()).stream().map(uj -> {
JobSmallDto job = new JobSmallDto();
job.setId(uj.getJobId());
return job;
}).collect(Collectors.toSet()));
}
});
}
return new PageInfo<>(pageData.getTotal(), userDtos);
}
@Override
//@Cacheable
public List<UserDto> queryAll(UserQueryParam query){
return ConvertUtil.convertList(userMapper.selectList(QueryHelpMybatisPlus.getPredicate(query)), UserDto.class);
}
@Override
public User getById(Long id) {
return userMapper.selectById(id);
}
@Override
@Cacheable(key = "'id:' + #p0")
public UserDto findById(Long id) {
return ConvertUtil.convert(getById(id), UserDto.class);
}
@Override
public User getByUsername(String userName) {
User user = lambdaQuery().eq(User::getUsername, userName).one();
/*if (user == null) {
throw new EntityNotFoundException(User.class, "username", userName);
}*/
return user;
}
@Override
public UserDto findByName(String userName) {
UserDto dto = ConvertUtil.convert(getByUsername(userName), UserDto.class);
if (dto == null) {
return dto;
}
dto.setDept(new DeptSmallDto(dto.getDeptId(), deptService.findById(dto.getDeptId()).getName()));
//dto.setRoles();
//dto.setJobs();
return dto;
}
private User getByEmail(String email) {
return lambdaQuery().eq(User::getEmail, email).one();
}
private User getByPhone(String phone) {
return lambdaQuery().eq(User::getPhone, phone).one();
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean save(UserDto resources) {
User user = getByUsername(resources.getUsername());
if (user != null) {
throw new EntityExistException(User.class, "username", user.getUsername());
}
user = getByEmail(resources.getEmail());
if (user != null) {
throw new EntityExistException(User.class, "email", resources.getEmail());
}
user = getByPhone(resources.getPhone());
if (user != null) {
throw new EntityExistException(User.class, "phone", resources.getPhone());
}
user = ConvertUtil.convert(resources, User.class);
if (resources.getDept() != null) {
user.setDeptId(resources.getDept().getId());
}
int ret = userMapper.insert(user);
final Long userId = user.getId();
if (CollectionUtils.isNotEmpty(resources.getRoles())) {
resources.getRoles().forEach(role -> {
UsersRoles ur = new UsersRoles();
ur.setUserId(userId);
ur.setRoleId(role.getId());
usersRolesMapper.insert(ur);
});
}
if (CollectionUtils.isNotEmpty(resources.getJobs())) {
resources.getJobs().forEach(job -> {
UsersJobs uj = new UsersJobs();
uj.setUserId(userId);
uj.setJobId(job.getId());
usersJobsMapper.insert(uj);
});
}
return ret > 0;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateById(UserDto resources) throws Exception {
User user = getById(resources.getId());
User user1 = getByUsername(user.getUsername());
User user2 = getByEmail(user.getEmail());
User user3 = getByPhone(user.getPhone());
if (user1 != null && !user.getId().equals(user1.getId())) {
throw new EntityExistException(User.class, "username", user.getUsername());
}
if (user2 != null && !user.getId().equals(user2.getId())) {
throw new EntityExistException(User.class, "email", user.getEmail());
}
if (user3 != null && !user.getId().equals(user3.getId())) {
throw new EntityExistException(User.class, "phone", user.getPhone());
}
//usersRolesService.getUsersRoleList(resources.getId());
// 如果用户的角色改变
//if (!resources.getRoles().equals(xxxx.getRoles())) {
redisUtils.del(CacheKey.DATA_USER + resources.getId());
redisUtils.del(CacheKey.MENU_USER + resources.getId());
redisUtils.del(CacheKey.ROLE_AUTH + resources.getId());
//}
// 如果用户名称修改
if(!resources.getUsername().equals(user.getUsername())){
throw new BadRequestException("不能修改用户名");
}
// 如果用户被禁用,则清除用户登录信息
if(!resources.getEnabled()){
onlineUserService.kickOutForUsername(resources.getUsername());
}
if (CollectionUtils.isNotEmpty(resources.getRoles())) {
usersRolesService.removeByUserId(resources.getId());
resources.getRoles().stream().forEach(role -> {
UsersRoles ur = new UsersRoles();
ur.setUserId(resources.getId());
ur.setRoleId(role.getId());
usersRolesMapper.insert(ur);
});
}
if (CollectionUtils.isNotEmpty(resources.getJobs())) {
usersJobsService.removeByUserId(resources.getId());
resources.getJobs().stream().forEach(job -> {
UsersJobs uj = new UsersJobs();
uj.setUserId(resources.getId());
uj.setJobId(job.getId());
usersJobsMapper.insert(uj);
});
}
user.setUsername(resources.getUsername());
user.setEmail(resources.getEmail());
user.setEnabled(resources.getEnabled());
if (resources.getDept() != null) {
user.setDeptId(resources.getDept().getId());
} else {
user.setDeptId(null);
}
user.setPhone(resources.getPhone());
user.setNickName(resources.getNickName());
user.setGender(resources.getGender());
delCaches(user.getId(), user.getUsername());
return userMapper.updateById(user) > 0;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updatePass(String username, String encryptPassword) {
User user = new User();
user.setPassword(encryptPassword);
user.setPwdResetTime(new Date());
lambdaUpdate().eq(User::getUsername, username).update(user);
flushCache(username);
}
@Override
public Map<String, String> updateAvatar(MultipartFile multipartFile) {
// 文件大小验证
FileUtil.checkSize(properties.getAvatarMaxSize(), multipartFile.getSize());
// 验证文件上传的格式
String image = "gif jpg png jpeg";
String fileType = FileUtil.getExtensionName(multipartFile.getOriginalFilename());
if(fileType != null && !image.contains(fileType)){
throw new BadRequestException("文件格式错误!, 仅支持 " + image +" 格式");
}
User user = getByUsername(SecurityUtils.getCurrentUsername());
String oldPath = user.getAvatarPath();
File file = FileUtil.upload(multipartFile, properties.getPath().getAvatar());
user.setAvatarName(file.getName());
user.setAvatarPath(Objects.requireNonNull(file).getPath());
userMapper.updateById(user);
if (StrUtil.isNotBlank(oldPath)) {
FileUtil.del(oldPath);
}
flushCache(user.getUsername());
return new HashMap<String, String>() {
{
put("avatar", file.getName());
}
};
}
@Override
public void updateEmail(String username, String email) {
User user = getByUsername(username);
User user2 = getByEmail(email);
if (ObjectUtil.notEqual(user.getId(), user2.getId())) {
throw new EntityExistException(User.class, "email", email);
}
User userUpdate = new User();
userUpdate.setEmail(email);
lambdaUpdate().eq(User::getUsername, username).update(userUpdate);
}
@Override
public void updateCenter(User resources) {
User user2 = getByPhone(resources.getPhone());
if (ObjectUtil.notEqual(resources.getId(), user2.getId())) {
throw new EntityExistException(User.class, "phone", resources.getPhone());
}
User userUpdate = new User();
userUpdate.setPhone(resources.getPhone());
userUpdate.setGender(resources.getGender());
userUpdate.setNickName(resources.getNickName());
lambdaUpdate().eq(User::getId, resources.getId()).update(userUpdate);
redisUtils.del("user::username:" + resources.getUsername());
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean removeByIds(Set<Long> ids){
for (Long id: ids) {
User user = getById(id);
delCaches(user.getId(), user.getUsername());
usersRolesService.removeByUserId(id);
usersJobsService.removeByUserId(id);
}
return userMapper.deleteBatchIds(ids) > 0;
}
@Override
@Transactional
public boolean removeById(Long id){
Set<Long> ids = new HashSet<>(1);
ids.add(id);
return this.removeByIds(ids);
}
@Override
public void download(List<UserDto> all, HttpServletResponse response) throws IOException {
List<Map<String, Object>> list = new ArrayList<>();
for (UserDto user : all) {
Map<String,Object> map = new LinkedHashMap<>();
map.put("部门名称", user.getDeptId());
map.put("用户名", user.getUsername());
map.put("昵称", user.getNickName());
map.put("性别", user.getGender());
map.put("手机号码", user.getPhone());
map.put("邮箱", user.getEmail());
map.put("头像地址", user.getAvatarName());
map.put("头像真实路径", user.getAvatarPath());
map.put("密码", user.getPassword());
map.put("是否为admin账号", user.getIsAdmin());
map.put("状态:1启用、0禁用", user.getEnabled());
map.put("创建者", user.getCreateBy());
map.put("更新着", user.getUpdateBy());
map.put("修改密码的时间", user.getPwdResetTime());
map.put("创建日期", user.getCreateTime());
map.put("更新时间", user.getUpdateTime());
list.add(map);
}
FileUtil.downloadExcel(list, response);
}
/**
* 清理缓存
*
* @param id /
*/
public void delCaches(Long id, String username) {
redisUtils.del(CacheKey.USER_ID + id);
flushCache(username);
}
/**
* 清理 登陆时 用户缓存信息
*
* @param username /
*/
private void flushCache(String username) {
userCacheClean.cleanUserCache(username);
}
}
https://docs.oracle.com/javase/8/docs/api/
java 8
用户 Controller 类:UserController
package com.journey.controller;
import cn.hutool.core.collection.CollectionUtil;
import com.journey.annotation.Log;
import com.journey.domain.User;
import com.journey.exception.BadRequestException;
import com.journey.service.DataService;
import com.journey.service.DeptService;
import com.journey.service.RoleService;
import com.journey.service.UserService;
import com.journey.service.dto.RoleSmallDto;
import com.journey.service.dto.UserDto;
import com.journey.service.dto.UserQueryParam;
import com.journey.utils.PageUtil;
import com.journey.utils.SecurityUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* @ClassName UserController
* @Author journey
* @Date 2021/11/17 22:52
* @Version 1.0.0
* @Description 用户管理Controller
**/
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Api(tags = "系统:用户管理")
public class UserController {
private final DeptService deptService;
private final DataService dataService;
private final UserService userService;
private final PasswordEncoder passwordEncoder;
private final RoleService roleService;
@ApiOperation("导出用户数据")
@GetMapping(value = "/download")
@PreAuthorize("@el.check('user:list')")
public void download(HttpServletResponse response, UserQueryParam criteria) throws IOException {
userService.download(userService.queryAll(criteria), response);
}
@ApiOperation("查询用户")
@GetMapping
@PreAuthorize("@el.check('user:list')")
public ResponseEntity<Object> query(UserQueryParam criteria, Pageable pageable){
// 如果用户部门ID不为空
if(!ObjectUtils.isEmpty(criteria.getDeptId())) {
// 添加部门ID
criteria.getDeptIds().add(criteria.getDeptId());
// 添加子部门ID
criteria.getDeptIds().addAll(deptService.getDeptChildren(criteria.getDeptId(),
deptService.findByPid(criteria.getDeptId())));
}
// 数据权限
List<Long> dataScopes = dataService.getDeptIds(userService.findByName(SecurityUtils.getCurrentUsername()));
// criteria.getDeptIds() 不为空并且数据权限不为空则取交集
if (!CollectionUtils.isEmpty(criteria.getDeptIds()) && !CollectionUtils.isEmpty(dataScopes)){
// 取交集
criteria.getDeptIds().retainAll(dataScopes);
if(!CollectionUtil.isEmpty(criteria.getDeptIds())){
return new ResponseEntity<>(userService.queryAll(criteria,pageable), HttpStatus.OK);
}
} else {
// 否则取并集
criteria.getDeptIds().addAll(dataScopes);
return new ResponseEntity<>(userService.queryAll(criteria,pageable),HttpStatus.OK);
}
return new ResponseEntity<>(PageUtil.toPage(null,0),HttpStatus.OK);
}
@Log("新增用户")
@ApiOperation("新增用户")
@PostMapping
@PreAuthorize("@el.check('user:add')")
public ResponseEntity<Object> create(@Validated @RequestBody UserDto resources){
checkLevel(resources);
// 默认密码 123456
resources.setPassword(passwordEncoder.encode("123456"));
userService.save(resources);
return new ResponseEntity<>(HttpStatus.CREATED);
}
@Log("修改用户")
@ApiOperation("修改用户")
@PutMapping
@PreAuthorize("@el.check('user:edit')")
public ResponseEntity<Object> update(@Validated(User.Update.class) @RequestBody UserDto resources) throws Exception {
checkLevel(resources);
userService.updateById(resources);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
@Log("修改用户:个人中心")
@ApiOperation("修改用户:个人中心")
@PutMapping(value = "center")
public ResponseEntity<Object> center(@Validated(User.Update.class) @RequestBody User resources){
if(!resources.getId().equals(SecurityUtils.getCurrentUserId())){
throw new BadRequestException("不能修改他人资料");
}
userService.updateCenter(resources);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
/**
* 如果当前用户的角色级别低于创建用户的角色级别,则抛出权限不足的错误
* @param resources /
*/
private void checkLevel(UserDto resources) {
Integer currentLevel = Collections.min(roleService.findByUsersId(SecurityUtils.getCurrentUserId()).stream().map(RoleSmallDto::getLevel).collect(Collectors.toList()));
Integer optLevel = roleService.findByRoles(resources.getRoles().stream().map(RoleSmallDto::getId).collect(Collectors.toSet()));
if (currentLevel > optLevel) {
throw new BadRequestException("角色权限不足");
}
}
}
新增部门service接口类 DeptService:DeptService
package com.journey.service;
import com.journey.base.CommonService;
import com.journey.domain.Dept;
import com.journey.service.dto.DeptDto;
import com.journey.service.dto.DeptQueryParam;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Set;
/**
* @author jinjin
* @date 2020-09-25
*/
public interface DeptService extends CommonService<Dept> {
/**
* 查询所有数据不分页
* @param query 条件参数
* @return List<DeptDto>
*/
List<DeptDto> queryAll(DeptQueryParam query, Boolean isQuery);
List<DeptDto> queryAll();
List<Dept> findByPid(long pid);
Set<DeptDto> findByRoleId(Long roleId);
/**
* 获取待删除的部门
* @param deptList /
* @param deptIds /
* @return /
*/
Set<Long> getDeleteDepts(List<Dept> deptList, Set<Long> deptIds);
/**
* 根据ID获取同级与上级数据
* @param deptDto /
* @param depts /
* @return /
*/
List<DeptDto> getSuperior(DeptDto deptDto, List<Dept> depts);
/**
* 构建树形数据
* @param deptDtos /
* @return /
*/
Object buildTree(List<DeptDto> deptDtos);
/**
* 获取
* @param deptId
* @param deptList
* @return
*/
List<Long> getDeptChildren(Long deptId, List<Dept> deptList);
/**
* 验证是否被角色或用户关联
* @param deptIds /
*/
void verification(Set<Long> deptIds);
Dept getById(Long id);
DeptDto findById(Long id);
/**
* 插入一条新数据。
*/
@Override
boolean save(Dept resources);
@Override
boolean updateById(Dept resources);
boolean removeById(Long id);
boolean removeByIds(Set<Long> ids);
/**
* 导出数据
* @param all 待导出的数据
* @param response /
* @throws IOException /
*/
void download(List<DeptDto> all, HttpServletResponse response) throws IOException;
}
创建部门接口实现类DeptServiceImpl
package com.journey.service.impl;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.journey.base.QueryHelpMybatisPlus;
import com.journey.base.impl.CommonServiceImpl;
import com.journey.domain.Dept;
import com.journey.domain.User;
import com.journey.exception.BadRequestException;
import com.journey.service.DeptService;
import com.journey.service.RolesDeptsService;
import com.journey.service.dto.DeptDto;
import com.journey.service.dto.DeptQueryParam;
import com.journey.service.mapper.DeptMapper;
import com.journey.service.mapper.RoleMapper;
import com.journey.service.mapper.UserMapper;
import com.journey.utils.*;
import com.journey.utils.enums.DataScopeEnum;
import lombok.AllArgsConstructor;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;
// 默认不使用缓存
//import org.springframework.cache.annotation.CacheConfig;
//import org.springframework.cache.annotation.CacheEvict;
//import org.springframework.cache.annotation.Cacheable;
/**
* @author jinjin
* @date 2020-09-25
*/
@Service
@AllArgsConstructor
@CacheConfig(cacheNames = "dept")
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class DeptServiceImpl extends CommonServiceImpl<DeptMapper, Dept> implements DeptService {
private final UserMapper userMapper;
private final RoleMapper roleMapper;
private final DeptMapper deptMapper;
private final RedisUtils redisUtils;
private final RolesDeptsService rolesDeptsService;
@Override
//@Cacheable
public List<DeptDto> queryAll(DeptQueryParam query, Boolean isQuery){
if (isQuery) {
query.setPidIsNull(true);
}
boolean notEmpty = StrUtil.isNotEmpty(query.getName()) || null != query.getPid()
|| CollectionUtils.isNotEmpty(query.getCreateTime());
if (isQuery && notEmpty) {
query.setPidIsNull(null);
}
return ConvertUtil.convertList(deptMapper.selectList(QueryHelpMybatisPlus.getPredicate(query)), DeptDto.class);
}
public List<DeptDto> queryAll2(DeptQueryParam query, Boolean isQuery) throws Exception {
//Sort sort = new Sort(Sort.Direction.ASC, "deptSort");
String dataScopeType = SecurityUtils.getDataScopeType();
if (isQuery) {
if (dataScopeType.equals(DataScopeEnum.ALL.getValue())) {
query.setPidIsNull(true);
}
List<Field> fields = QueryHelpMybatisPlus.getAllFields(query.getClass(), new ArrayList<>());
List<String> fieldNames = new ArrayList<String>() {{
add("pidIsNull");
add("enabled");
}};
for (Field field : fields) {
//设置对象的访问权限,保证对private的属性的访问
field.setAccessible(true);
Object val = field.get(query);
if (fieldNames.contains(field.getName())) {
continue;
}
if (ObjectUtil.isNotNull(val)) {
query.setPidIsNull(null);
break;
}
}
}
QueryWrapper<Dept> wrapper = QueryHelpMybatisPlus.getPredicate(query);
wrapper.lambda().orderByAsc(Dept::getDeptSort);
List<DeptDto> list = ConvertUtil.convertList(deptMapper.selectList(wrapper), DeptDto.class);//deptMapper.toDto(deptRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root, criteria, criteriaBuilder), sort));
// 如果为空,就代表为自定义权限或者本级权限,就需要去重,不理解可以注释掉,看查询结果
if (StringUtils.isBlank(dataScopeType)) {
return deduplication(list);
}
return list;
}
private List<DeptDto> deduplication(List<DeptDto> list) {
List<DeptDto> deptDtos = new ArrayList<>();
for (DeptDto deptDto : list) {
boolean flag = true;
for (DeptDto dto : list) {
if (deptDto.getPid().equals(dto.getId())) {
flag = false;
break;
}
}
if (flag){
deptDtos.add(deptDto);
}
}
return deptDtos;
}
@Override
// @Cacheable
public List<DeptDto> queryAll() {
return ConvertUtil.convertList(deptMapper.selectList(Wrappers.emptyWrapper()), DeptDto.class);
}
@Override
public Dept getById(Long id) {
return deptMapper.selectById(id);
}
@Override
public DeptDto findById(Long id) {
return ConvertUtil.convert(getById(id), DeptDto.class);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean save(Dept resources) {
int ret = deptMapper.insert(resources);
resources.setSubCount(0);
if (resources.getPid() != null) {
redisUtils.del(CacheKey.DEPT_PID + resources.getPid());
updateSubCnt(resources.getPid());
// 清理缓存
delCaches(resources.getId(), null, resources.getPid());
}
return ret > 0;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateById(Dept resources){
// 旧的部门
Long pidOld = getById(resources.getId()).getPid();
if (resources.getPid() != null && resources.getId().equals(resources.getPid())) {
throw new BadRequestException("上级不能为自己");
}
int ret = deptMapper.updateById(resources);
updateSubCnt(pidOld);
updateSubCnt(resources.getPid());
// 清理缓存
delCaches(resources.getId(), pidOld, resources.getPid());
return ret > 0;
}
private void updateSubCnt(Long deptId) {
if (deptId == null) {
return;
}
Dept parent = getById(deptId);
int count = lambdaQuery().eq(Dept::getPid, deptId).count();
Dept dept = new Dept();
dept.setSubCount(count);
if (parent != null) {
dept.setPid(parent.getPid());
}
lambdaUpdate().eq(Dept::getId, deptId)
.update(dept);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean removeByIds(Set<Long> ids){
for (Long id: ids) {
Dept dept = getById(id);
// 清理缓存
delCaches(id, dept.getPid(), null);
updateSubCnt(dept.getPid());
rolesDeptsService.removeByDeptId(id);
}
return deptMapper.deleteBatchIds(ids) > 0;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean removeById(Long id){
Set<Long> ids = new HashSet<>(1);
ids.add(id);
return removeByIds(ids);
}
// @Cacheable(key = "'pid:' + #p0")
@Override
public List<Dept> findByPid(long pid) {
return lambdaQuery().eq(Dept::getPid, pid).list();
}
@Override
public Set<DeptDto> findByRoleId(Long roleId) {
return ConvertUtil.convertSet(deptMapper.selectByRoleId(roleId), DeptDto.class);
}
@Override
public List<DeptDto> getSuperior(DeptDto deptDto, List<Dept> depts) {
if (deptDto.getPid() == null) {
depts.addAll(lambdaQuery().isNull(Dept::getPid).list());
return ConvertUtil.convertList(depts, DeptDto.class);
}
depts.addAll(lambdaQuery().eq(Dept::getPid, deptDto.getPid()).list());
return getSuperior(ConvertUtil.convert(getById(deptDto.getPid()), DeptDto.class), depts);
}
@Override
public Map<String, Object> buildTree(List<DeptDto> deptDtos) {
Set<DeptDto> trees = new LinkedHashSet<>();
Set<DeptDto> depts = new LinkedHashSet<>();
List<String> deptNames = deptDtos.stream().map(DeptDto::getName).collect(Collectors.toList());
boolean isChild;
for (DeptDto deptDTO : deptDtos) {
isChild = false;
if (deptDTO.getPid() == null) {
trees.add(deptDTO);
}
for (DeptDto it : deptDtos) {
if (it.getPid() != null && it.getPid().equals(deptDTO.getId())) {
isChild = true;
if (deptDTO.getChildren() == null) {
deptDTO.setChildren(new ArrayList<>());
}
deptDTO.getChildren().add(it);
}
}
if (isChild) {
depts.add(deptDTO);
} else {
Dept dept = null;
if (null != deptDTO.getPid()) {
dept = this.getById(deptDTO.getPid());
}
if (null != dept && !deptNames.contains(dept.getName())) {
depts.add(deptDTO);
}
}
}
if (CollectionUtils.isEmpty(trees)) {
trees = depts;
}
Map<String, Object> map = new HashMap<>(2);
map.put("totalElements", deptDtos.size());
map.put("content", CollectionUtils.isEmpty(trees) ? deptDtos : trees);
return map;
}
@Override
public Set<Long> getDeleteDepts(List<Dept> deptList, Set<Long> deptIds) {
for (Dept dept : deptList) {
deptIds.add(dept.getId());
List<Dept> depts = findByPid(dept.getId());
if (depts != null && depts.size() != 0) {
getDeleteDepts(depts, deptIds);
}
}
return deptIds;
}
@Override
public List<Long> getDeptChildren(Long deptId, List<Dept> deptList) {
List<Long> list = new ArrayList<>();
deptList.forEach(dept -> {
if (dept != null && dept.getEnabled()) {
List<Dept> depts = lambdaQuery().eq(Dept::getPid, dept.getId()).list();
if (depts.size() != 0) {
list.addAll(getDeptChildren(dept.getId(), depts));
}
list.add(dept.getId());
}
});
return list;
}
@Override
public void verification(Set<Long> deptIds) {
if (userMapper.countByDepts(deptIds) > 0) {
throw new BadRequestException("所选部门存在用户关联,请解除后再试!");
}
if (roleMapper.countByDepts(deptIds) > 0) {
throw new BadRequestException("所选部门存在角色关联,请解除后再试!");
}
}
@Override
public void download(List<DeptDto> all, HttpServletResponse response) throws IOException {
List<Map<String, Object>> list = new ArrayList<>();
for (DeptDto dept : all) {
Map<String,Object> map = new LinkedHashMap<>();
map.put("上级部门", dept.getPid());
map.put("子部门数目", dept.getSubCount());
map.put("名称", dept.getName());
map.put("排序", dept.getDeptSort());
map.put("状态", dept.getEnabled());
map.put("创建者", dept.getCreateBy());
map.put("更新者", dept.getUpdateBy());
map.put("创建日期", dept.getCreateTime());
map.put("更新时间", dept.getUpdateTime());
list.add(map);
}
FileUtil.downloadExcel(list, response);
}
/**
* 清理缓存
*
* @param id /
*/
public void delCaches(Long id, Long pidOld, Long pidNew) {
List<User> users = userMapper.findByRoleDeptId(id);
// 删除数据权限
redisUtils.delByKeys(CacheKey.DATA_USER, users.stream().map(User::getId).collect(Collectors.toSet()));
redisUtils.del(CacheKey.DEPT_ID + id);
redisUtils.del(CacheKey.DEPT_PID + (pidOld == null ? 0 : pidOld));
redisUtils.del(CacheKey.DEPT_PID + (pidNew == null ? 0 : pidNew));
}
}
vue 复制内容
Vue.component() 使用
Vue Table 滚动条加载数据
UI
import { initData, download } from '@/api/data'
import { parseTime, downloadFile } from '@/utils/index'
import Vue from 'vue'
/**
* CRUD配置
* @author moxun
* @param {*} options <br>
* @return crud instance.
* @example
* 要使用多crud时,请在关联crud的组件处使用crud-tag进行标记,如:<jobForm :job-status="dict.job_status" crud-tag="job" />
*/
function CRUD(options) {
const defaultOptions = {
tag: 'default',
// id字段名
idField: 'id',
// 标题
title: '',
// 请求数据的url
url: '',
// 表格数据
data: [],
// 选择项
selections: [],
// 待查询的对象
query: {},
// 查询数据的参数
params: {},
// Form 表单
form: {},
// 重置表单
defaultForm: () => {},
// 排序规则,默认 id 降序, 支持多字段排序 ['id,desc', 'createTime,asc']
sort: ['id,desc'],
// 等待时间
time: 50,
// CRUD Method
crudMethod: {
add: (form) => {},
del: (id) => {},
edit: (form) => {},
get: (id) => {}
},
// 主页操作栏显示哪些按钮
optShow: {
add: true,
edit: true,
del: true,
download: true,
reset: true
},
// 自定义一些扩展属性
props: {},
// 在主页准备
queryOnPresenterCreated: true,
// 调试开关
debug: false
}
options = mergeOptions(defaultOptions, options)
const data = {
...options,
// 记录数据状态
dataStatus: {},
status: {
add: CRUD.STATUS.NORMAL,
edit: CRUD.STATUS.NORMAL,
// 添加或编辑状态
get cu() {
if (this.add === CRUD.STATUS.NORMAL && this.edit === CRUD.STATUS.NORMAL) {
return CRUD.STATUS.NORMAL
} else if (this.add === CRUD.STATUS.PREPARED || this.edit === CRUD.STATUS.PREPARED) {
return CRUD.STATUS.PREPARED
} else if (this.add === CRUD.STATUS.PROCESSING || this.edit === CRUD.STATUS.PROCESSING) {
return CRUD.STATUS.PROCESSING
}
throw new Error('wrong crud\'s cu status')
},
// 标题
get title() {
return this.add > CRUD.STATUS.NORMAL ? `新增${crud.title}` : this.edit > CRUD.STATUS.NORMAL ? `编辑${crud.title}` : crud.title
}
},
msg: {
submit: '提交成功',
add: '新增成功',
edit: '编辑成功',
del: '删除成功'
},
page: {
// 页码
page: 0,
// 每页数据条数
size: 10,
// 总数据条数
total: 0
},
// 整体loading
loading: false,
// 导出的 Loading
downloadLoading: false,
// 删除的 Loading
delAllLoading: false
}
const methods = {
/**
* 通用的提示
*/
submitSuccessNotify() {
crud.notify(crud.msg.submit, CRUD.NOTIFICATION_TYPE.SUCCESS)
},
addSuccessNotify() {
crud.notify(crud.msg.add, CRUD.NOTIFICATION_TYPE.SUCCESS)
},
editSuccessNotify() {
crud.notify(crud.msg.edit, CRUD.NOTIFICATION_TYPE.SUCCESS)
},
delSuccessNotify() {
crud.notify(crud.msg.del, CRUD.NOTIFICATION_TYPE.SUCCESS)
},
// 搜索
toQuery() {
crud.page.page = 1
crud.refresh()
},
// 刷新
refresh() {
if (!callVmHook(crud, CRUD.HOOK.beforeRefresh)) {
return
}
return new Promise((resolve, reject) => {
crud.loading = true
// 请求数据
initData(crud.url, crud.getQueryParams()).then(data => {
const table = crud.getTable()
if (table && table.lazy) { // 懒加载子节点数据,清掉已加载的数据
table.store.states.treeData = {}
table.store.states.lazyTreeNodeMap = {}
}
crud.page.total = data.totalElements
crud.data = data.content
crud.resetDataStatus()
// time 毫秒后显示表格
setTimeout(() => {
crud.loading = false
callVmHook(crud, CRUD.HOOK.afterRefresh)
}, crud.time)
resolve(data)
}).catch(err => {
crud.loading = false
reject(err)
})
})
},
/**
* 启动添加
*/
toAdd() {
// 刷新
crud.resetForm()
//
if (!(callVmHook(crud, CRUD.HOOK.beforeToAdd, crud.form) && callVmHook(crud, CRUD.HOOK.beforeToCU, crud.form))) {
return
}
crud.status.add = CRUD.STATUS.PREPARED
callVmHook(crud, CRUD.HOOK.afterToAdd, crud.form)
callVmHook(crud, CRUD.HOOK.afterToCU, crud.form)
},
/**
* 启动编辑
* @param {*} data 数据项
*/
toEdit(data) {
crud.resetForm(JSON.parse(JSON.stringify(data)))
if (!(callVmHook(crud, CRUD.HOOK.beforeToEdit, crud.form) && callVmHook(crud, CRUD.HOOK.beforeToCU, crud.form))) {
return
}
crud.status.edit = CRUD.STATUS.PREPARED
crud.getDataStatus(crud.getDataId(data)).edit = CRUD.STATUS.PREPARED
callVmHook(crud, CRUD.HOOK.afterToEdit, crud.form)
callVmHook(crud, CRUD.HOOK.afterToCU, crud.form)
},
/**
* 启动删除
* @param {*} data 数据项
*/
toDelete(data) {
crud.getDataStatus(crud.getDataId(data)).delete = CRUD.STATUS.PREPARED
},
/**
* 取消删除
* @param {*} data 数据项
*/
cancelDelete(data) {
if (!callVmHook(crud, CRUD.HOOK.beforeDeleteCancel, data)) {
return
}
crud.getDataStatus(crud.getDataId(data)).delete = CRUD.STATUS.NORMAL
callVmHook(crud, CRUD.HOOK.afterDeleteCancel, data)
},
/**
* 取消新增/编辑
*/
cancelCU() {
const addStatus = crud.status.add
const editStatus = crud.status.edit
if (addStatus === CRUD.STATUS.PREPARED) {
if (!callVmHook(crud, CRUD.HOOK.beforeAddCancel, crud.form)) {
return
}
crud.status.add = CRUD.STATUS.NORMAL
}
if (editStatus === CRUD.STATUS.PREPARED) {
if (!callVmHook(crud, CRUD.HOOK.beforeEditCancel, crud.form)) {
return
}
crud.status.edit = CRUD.STATUS.NORMAL
crud.getDataStatus(crud.getDataId(crud.form)).edit = CRUD.STATUS.NORMAL
}
crud.resetForm()
if (addStatus === CRUD.STATUS.PREPARED) {
callVmHook(crud, CRUD.HOOK.afterAddCancel, crud.form)
}
if (editStatus === CRUD.STATUS.PREPARED) {
callVmHook(crud, CRUD.HOOK.afterEditCancel, crud.form)
}
// 清除表单验证
if (crud.findVM('form').$refs['form']) {
crud.findVM('form').$refs['form'].clearValidate()
}
},
/**
* 提交新增/编辑
*/
submitCU() {
if (!callVmHook(crud, CRUD.HOOK.beforeValidateCU)) {
return
}
crud.findVM('form').$refs['form'].validate(valid => {
if (!valid) {
return
}
if (!callVmHook(crud, CRUD.HOOK.afterValidateCU)) {
return
}
if (crud.status.add === CRUD.STATUS.PREPARED) {
crud.doAdd()
} else if (crud.status.edit === CRUD.STATUS.PREPARED) {
crud.doEdit()
}
})
},
/**
* 执行添加
*/
doAdd() {
if (!callVmHook(crud, CRUD.HOOK.beforeSubmit)) {
return
}
crud.status.add = CRUD.STATUS.PROCESSING
crud.crudMethod.add(crud.form).then(() => {
crud.status.add = CRUD.STATUS.NORMAL
crud.resetForm()
crud.addSuccessNotify()
callVmHook(crud, CRUD.HOOK.afterSubmit)
crud.toQuery()
}).catch(() => {
crud.status.add = CRUD.STATUS.PREPARED
callVmHook(crud, CRUD.HOOK.afterAddError)
})
},
/**
* 执行编辑
*/
doEdit() {
if (!callVmHook(crud, CRUD.HOOK.beforeSubmit)) {
return
}
crud.status.edit = CRUD.STATUS.PROCESSING
crud.crudMethod.edit(crud.form).then(() => {
crud.status.edit = CRUD.STATUS.NORMAL
crud.getDataStatus(crud.getDataId(crud.form)).edit = CRUD.STATUS.NORMAL
crud.editSuccessNotify()
crud.resetForm()
callVmHook(crud, CRUD.HOOK.afterSubmit)
crud.refresh()
}).catch(() => {
crud.status.edit = CRUD.STATUS.PREPARED
callVmHook(crud, CRUD.HOOK.afterEditError)
})
},
/**
* 执行删除
* @param {*} data 数据项
*/
doDelete(data) {
let delAll = false
let dataStatus
const ids = []
if (data instanceof Array) {
delAll = true
data.forEach(val => {
ids.push(this.getDataId(val))
})
} else {
ids.push(this.getDataId(data))
dataStatus = crud.getDataStatus(this.getDataId(data))
}
if (!callVmHook(crud, CRUD.HOOK.beforeDelete, data)) {
return
}
if (!delAll) {
dataStatus.delete = CRUD.STATUS.PROCESSING
}
return crud.crudMethod.del(ids).then(() => {
if (delAll) {
crud.delAllLoading = false
} else dataStatus.delete = CRUD.STATUS.PREPARED
crud.dleChangePage(1)
crud.delSuccessNotify()
callVmHook(crud, CRUD.HOOK.afterDelete, data)
crud.refresh()
}).catch(() => {
if (delAll) {
crud.delAllLoading = false
} else dataStatus.delete = CRUD.STATUS.PREPARED
})
},
/**
* 通用导出
*/
doExport() {
crud.downloadLoading = true
download(crud.url + '/download', crud.getQueryParams()).then(result => {
downloadFile(result, crud.title + '数据', 'xlsx')
crud.downloadLoading = false
}).catch(() => {
crud.downloadLoading = false
})
},
/**
* 获取查询参数
*/
getQueryParams: function() {
// 清除参数无值的情况
Object.keys(crud.query).length !== 0 && Object.keys(crud.query).forEach(item => {
if (crud.query[item] === null || crud.query[item] === '') crud.query[item] = undefined
})
Object.keys(crud.params).length !== 0 && Object.keys(crud.params).forEach(item => {
if (crud.params[item] === null || crud.params[item] === '') crud.params[item] = undefined
})
return {
page: crud.page.page - 1,
size: crud.page.size,
sort: crud.sort,
...crud.query,
...crud.params
}
},
// 当前页改变
pageChangeHandler(e) {
crud.page.page = e
crud.refresh()
},
// 每页条数改变
sizeChangeHandler(e) {
crud.page.size = e
crud.page.page = 1
crud.refresh()
},
// 预防删除第二页最后一条数据时,或者多选删除第二页的数据时,页码错误导致请求无数据
dleChangePage(size) {
if (crud.data.length === size && crud.page.page !== 1) {
crud.page.page -= 1
}
},
// 选择改变
selectionChangeHandler(val) {
crud.selections = val
},
/**
* 重置查询参数
* @param {Boolean} toQuery 重置后进行查询操作
*/
resetQuery(toQuery = true) {
const defaultQuery = JSON.parse(JSON.stringify(crud.defaultQuery))
const query = crud.query
Object.keys(query).forEach(key => {
query[key] = defaultQuery[key]
})
// 重置参数
this.params = {}
if (toQuery) {
crud.toQuery()
}
},
/**
* 重置表单
* @param {Array} data 数据
*/
resetForm(data) {
const form = data || (typeof crud.defaultForm === 'object' ? JSON.parse(JSON.stringify(crud.defaultForm)) : crud.defaultForm.apply(crud.findVM('form')))
const crudFrom = crud.form
for (const key in form) {
if (crudFrom.hasOwnProperty(key)) {
crudFrom[key] = form[key]
} else {
Vue.set(crudFrom, key, form[key])
}
}
// add by ghl 2020-10-04 页面重复添加信息时,下拉框的校验会存在,需要找工取消
if (crud.findVM('form').$refs['form']) {
crud.findVM('form').$refs['form'].clearValidate()
}
},
/**
* 重置数据状态
*/
resetDataStatus() {
const dataStatus = {}
function resetStatus(datas) {
datas.forEach(e => {
dataStatus[crud.getDataId(e)] = {
delete: 0,
edit: 0
}
if (e.children) {
resetStatus(e.children)
}
})
}
resetStatus(crud.data)
crud.dataStatus = dataStatus
},
/**
* 获取数据状态
* @param {Number | String} id 数据项id
*/
getDataStatus(id) {
return crud.dataStatus[id]
},
/**
* 用于树形表格多选, 选中所有
* @param selection
*/
selectAllChange(selection) {
// 如果选中的数目与请求到的数目相同就选中子节点,否则就清空选中
if (selection && selection.length === crud.data.length) {
selection.forEach(val => {
crud.selectChange(selection, val)
})
} else {
crud.getTable().clearSelection()
}
},
/**
* 用于树形表格多选,单选的封装
* @param selection
* @param row
*/
selectChange(selection, row) {
// 如果selection中存在row代表是选中,否则是取消选中
if (selection.find(val => { return crud.getDataId(val) === crud.getDataId(row) })) {
if (row.children) {
row.children.forEach(val => {
crud.getTable().toggleRowSelection(val, true)
selection.push(val)
if (val.children) {
crud.selectChange(selection, val)
}
})
}
} else {
crud.toggleRowSelection(selection, row)
}
},
/**
* 切换选中状态
* @param selection
* @param data
*/
toggleRowSelection(selection, data) {
if (data.children) {
data.children.forEach(val => {
crud.getTable().toggleRowSelection(val, false)
if (val.children) {
crud.toggleRowSelection(selection, val)
}
})
}
},
findVM(type) {
return crud.vms.find(vm => vm && vm.type === type).vm
},
notify(title, type = CRUD.NOTIFICATION_TYPE.INFO) {
crud.vms[0].vm.$notify({
title,
type,
duration: 2500
})
},
updateProp(name, value) {
Vue.set(crud.props, name, value)
},
getDataId(data) {
return data[this.idField]
},
getTable() {
return this.findVM('presenter').$refs.table
},
attchTable() {
const table = this.getTable()
this.updateProp('table', table)
const that = this
table.$on('expand-change', (row, expanded) => {
if (!expanded) {
return
}
const lazyTreeNodeMap = table.store.states.lazyTreeNodeMap
row.children = lazyTreeNodeMap[crud.getDataId(row)]
if (row.children) {
row.children.forEach(ele => {
const id = crud.getDataId(ele)
if (that.dataStatus[id] === undefined) {
that.dataStatus[id] = {
delete: 0,
edit: 0
}
}
})
}
})
}
}
const crud = Object.assign({}, data)
// 可观测化
Vue.observable(crud)
// 附加方法
Object.assign(crud, methods)
// 记录初始默认的查询参数,后续重置查询时使用
Object.assign(crud, {
defaultQuery: JSON.parse(JSON.stringify(data.query)),
// 预留4位存储:组件 主页、头部、分页、表单,调试查看也方便找
vms: Array(4),
/**
* 注册组件实例
* @param {String} type 类型
* @param {*} vm 组件实例
* @param {Number} index 该参数内部使用
*/
registerVM(type, vm, index = -1) {
const vmObj = {
type,
vm: vm
}
if (index < 0) {
this.vms.push(vmObj)
return
}
if (index < 4) { // 内置预留vm数
this.vms[index] = vmObj
return
}
this.vms.length = Math.max(this.vms.length, index)
this.vms.splice(index, 1, vmObj)
},
/**
* 取消注册组件实例
* @param {*} vm 组件实例
*/
unregisterVM(type, vm) {
for (let i = this.vms.length - 1; i >= 0; i--) {
if (this.vms[i] === undefined) {
continue
}
if (this.vms[i].type === type && this.vms[i].vm === vm) {
if (i < 4) { // 内置预留vm数
this.vms[i] = undefined
} else {
this.vms.splice(i, 1)
}
break
}
}
}
})
// 冻结处理,需要扩展数据的话,使用crud.updateProp(name, value),以crud.props.name形式访问,这个是响应式的,可以做数据绑定
Object.freeze(crud)
return crud
}
// hook VM
function callVmHook(crud, hook) {
if (crud.debug) {
console.log('callVmHook: ' + hook)
}
const tagHook = crud.tag ? hook + '$' + crud.tag : null
let ret = true
const nargs = [crud]
for (let i = 2; i < arguments.length; ++i) {
nargs.push(arguments[i])
}
// 有些组件扮演了多个角色,调用钩子时,需要去重
const vmSet = new Set()
crud.vms.forEach(vm => vm && vmSet.add(vm.vm))
vmSet.forEach(vm => {
if (vm[hook]) {
ret = vm[hook].apply(vm, nargs) !== false && ret
}
if (tagHook && vm[tagHook]) {
ret = vm[tagHook].apply(vm, nargs) !== false && ret
}
})
return ret
}
function mergeOptions(src, opts) {
const optsRet = {
...src
}
for (const key in src) {
if (opts.hasOwnProperty(key)) {
optsRet[key] = opts[key]
}
}
return optsRet
}
/**
* 查找crud
* @param {*} vm
* @param {string} tag
*/
function lookupCrud(vm, tag) {
tag = tag || vm.$attrs['crud-tag'] || 'default'
// function lookupCrud(vm, tag) {
if (vm.$crud) {
const ret = vm.$crud[tag]
if (ret) {
return ret
}
}
return vm.$parent ? lookupCrud(vm.$parent, tag) : undefined
}
/**
* crud主页
*/
function presenter(crud) {
if (crud) {
console.warn('[CRUD warn]: ' + 'please use $options.cruds() { return CRUD(...) or [CRUD(...), ...] }')
}
return {
data() {
// 在data中返回crud,是为了将crud与当前实例关联,组件观测crud相关属性变化
return {
crud: this.crud
}
},
beforeCreate() {
this.$crud = this.$crud || {}
let cruds = this.$options.cruds instanceof Function ? this.$options.cruds() : crud
if (!(cruds instanceof Array)) {
cruds = [cruds]
}
cruds.forEach(ele => {
if (this.$crud[ele.tag]) {
console.error('[CRUD error]: ' + 'crud with tag [' + ele.tag + ' is already exist')
}
this.$crud[ele.tag] = ele
ele.registerVM('presenter', this, 0)
})
this.crud = this.$crud['defalut'] || cruds[0]
},
methods: {
parseTime
},
created() {
for (const k in this.$crud) {
if (this.$crud[k].queryOnPresenterCreated) {
this.$crud[k].toQuery()
}
}
},
destroyed() {
for (const k in this.$crud) {
this.$crud[k].unregisterVM('presenter', this)
}
},
mounted() {
// 如果table未实例化(例如使用了v-if),请稍后在适当时机crud.attchTable刷新table信息
if (this.$refs.table !== undefined) {
this.crud.attchTable()
}
}
}
}
/**
* 头部
*/
function header() {
return {
data() {
return {
crud: this.crud,
query: this.crud.query
}
},
beforeCreate() {
this.crud = lookupCrud(this)
this.crud.registerVM('header', this, 1)
},
destroyed() {
this.crud.unregisterVM('header', this)
}
}
}
/**
* 分页
*/
function pagination() {
return {
data() {
return {
crud: this.crud,
page: this.crud.page
}
},
beforeCreate() {
this.crud = lookupCrud(this)
this.crud.registerVM('pagination', this, 2)
},
destroyed() {
this.crud.unregisterVM('pagination', this)
}
}
}
/**
* 表单
*/
function form(defaultForm) {
return {
data() {
return {
crud: this.crud,
form: this.crud.form
}
},
beforeCreate() {
this.crud = lookupCrud(this)
this.crud.registerVM('form', this, 3)
},
created() {
this.crud.defaultForm = defaultForm
this.crud.resetForm()
},
destroyed() {
this.crud.unregisterVM('form', this)
}
}
}
/**
* crud
*/
function crud(options = {}) {
const defaultOptions = {
type: undefined
}
options = mergeOptions(defaultOptions, options)
return {
data() {
return {
crud: this.crud
}
},
beforeCreate() {
this.crud = lookupCrud(this)
this.crud.registerVM(options.type, this)
},
destroyed() {
this.crud.unregisterVM(options.type, this)
}
}
}
/**
* CRUD钩子
*/
CRUD.HOOK = {
/** 刷新 - 之前 */
beforeRefresh: 'beforeCrudRefresh',
/** 刷新 - 之后 */
afterRefresh: 'afterCrudRefresh',
/** 删除 - 之前 */
beforeDelete: 'beforeCrudDelete',
/** 删除 - 之后 */
afterDelete: 'afterCrudDelete',
/** 删除取消 - 之前 */
beforeDeleteCancel: 'beforeCrudDeleteCancel',
/** 删除取消 - 之后 */
afterDeleteCancel: 'afterCrudDeleteCancel',
/** 新建 - 之前 */
beforeToAdd: 'beforeCrudToAdd',
/** 新建 - 之后 */
afterToAdd: 'afterCrudToAdd',
/** 编辑 - 之前 */
beforeToEdit: 'beforeCrudToEdit',
/** 编辑 - 之后 */
afterToEdit: 'afterCrudToEdit',
/** 开始 "新建/编辑" - 之前 */
beforeToCU: 'beforeCrudToCU',
/** 开始 "新建/编辑" - 之后 */
afterToCU: 'afterCrudToCU',
/** "新建/编辑" 验证 - 之前 */
beforeValidateCU: 'beforeCrudValidateCU',
/** "新建/编辑" 验证 - 之后 */
afterValidateCU: 'afterCrudValidateCU',
/** 添加取消 - 之前 */
beforeAddCancel: 'beforeCrudAddCancel',
/** 添加取消 - 之后 */
afterAddCancel: 'afterCrudAddCancel',
/** 编辑取消 - 之前 */
beforeEditCancel: 'beforeCrudEditCancel',
/** 编辑取消 - 之后 */
afterEditCancel: 'afterCrudEditCancel',
/** 提交 - 之前 */
beforeSubmit: 'beforeCrudSubmitCU',
/** 提交 - 之后 */
afterSubmit: 'afterCrudSubmitCU',
afterAddError: 'afterCrudAddError',
afterEditError: 'afterCrudEditError'
}
/**
* CRUD状态
*/
CRUD.STATUS = {
NORMAL: 0,
PREPARED: 1,
PROCESSING: 2
}
/**
* CRUD通知类型
*/
CRUD.NOTIFICATION_TYPE = {
SUCCESS: 'success',
WARNING: 'warning',
INFO: 'info',
ERROR: 'error'
}
export default CRUD
export {
presenter,
header,
form,
pagination,
crud
}
创建
CRUD.operation.vue
<template>
<div class="crud-opts">
<span class="crud-opts-left">
<!--左侧插槽-->
<slot name="left" />
<el-button
v-if="crud.optShow.add"
v-permission="permission.add"
class="filter-item"
size="mini"
type="primary"
icon="el-icon-plus"
@click="crud.toAdd"
>
新增
</el-button>
<el-button
v-if="crud.optShow.edit"
v-permission="permission.edit"
class="filter-item"
size="mini"
type="success"
icon="el-icon-edit"
:disabled="crud.selections.length !== 1"
@click="crud.toEdit(crud.selections[0])"
>
修改
</el-button>
<el-button
v-if="crud.optShow.del"
slot="reference"
v-permission="permission.del"
class="filter-item"
type="danger"
icon="el-icon-delete"
size="mini"
:loading="crud.delAllLoading"
:disabled="crud.selections.length === 0"
@click="toDelete(crud.selections)"
>
删除
</el-button>
<el-button
v-if="crud.optShow.download"
:loading="crud.downloadLoading"
:disabled="!crud.data.length"
class="filter-item"
size="mini"
type="warning"
icon="el-icon-download"
@click="crud.doExport"
>导出</el-button>
<!--右侧-->
<slot name="right" />
</span>
<el-button-group class="crud-opts-right">
<el-button
size="mini"
plain
type="info"
icon="el-icon-search"
@click="toggleSearch()"
/>
<el-button
size="mini"
icon="el-icon-refresh"
@click="crud.refresh()"
/>
<el-popover
placement="bottom-end"
width="150"
trigger="click"
>
<el-button
slot="reference"
size="mini"
icon="el-icon-s-grid"
>
<i
class="fa fa-caret-down"
aria-hidden="true"
/>
</el-button>
<el-checkbox
v-model="allColumnsSelected"
:indeterminate="allColumnsSelectedIndeterminate"
@change="handleCheckAllChange"
>
全选
</el-checkbox>
<el-checkbox
v-for="item in tableColumns"
:key="item.property"
v-model="item.visible"
@change="handleCheckedTableColumnsChange(item)"
>
{{ item.label }}
</el-checkbox>
</el-popover>
</el-button-group>
</div>
</template>
<script>
import CRUD, { crud } from '@crud/crud'
function sortWithRef(src, ref) {
const result = Object.assign([], ref)
let cursor = -1
src.forEach(e => {
const idx = result.indexOf(e)
if (idx === -1) {
cursor += 1
result.splice(cursor, 0, e)
} else {
cursor = idx
}
})
return result
}
export default {
mixins: [crud()],
props: {
permission: {
type: Object,
default: () => { return {} }
},
hiddenColumns: {
type: Array,
default: () => { return [] }
},
ignoreColumns: {
type: Array,
default: () => { return [] }
}
},
data() {
return {
tableColumns: [],
allColumnsSelected: true,
allColumnsSelectedIndeterminate: false,
tableUnwatcher: null,
// 忽略下次表格列变动
ignoreNextTableColumnsChange: false
}
},
watch: {
'crud.props.table'() {
this.updateTableColumns()
this.tableColumns.forEach(column => {
if (this.hiddenColumns.indexOf(column.property) !== -1) {
column.visible = false
this.updateColumnVisible(column)
}
})
},
'crud.props.table.store.states.columns'() {
this.updateTableColumns()
}
},
created() {
this.crud.updateProp('searchToggle', true)
},
methods: {
updateTableColumns() {
const table = this.crud.getTable()
if (!table) {
this.tableColumns = []
return
}
let cols = null
const columnFilter = e => e && e.type === 'default' && e.property && this.ignoreColumns.indexOf(e.property) === -1
const refCols = table.columns.filter(columnFilter)
if (this.ignoreNextTableColumnsChange) {
this.ignoreNextTableColumnsChange = false
return
}
this.ignoreNextTableColumnsChange = false
const columns = []
const fullTableColumns = table.$children.map(e => e.columnConfig).filter(columnFilter)
cols = sortWithRef(fullTableColumns, refCols)
cols.forEach(config => {
const column = {
property: config.property,
label: config.label,
visible: refCols.indexOf(config) !== -1
}
columns.push(column)
})
this.tableColumns = columns
},
toDelete(datas) {
this.$confirm(`确认删除选中的${datas.length}条数据?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.crud.delAllLoading = true
this.crud.doDelete(datas)
}).catch(() => {
})
},
handleCheckAllChange(val) {
if (val === false) {
this.allColumnsSelected = true
return
}
this.tableColumns.forEach(column => {
if (!column.visible) {
column.visible = true
this.updateColumnVisible(column)
}
})
this.allColumnsSelected = val
this.allColumnsSelectedIndeterminate = false
},
handleCheckedTableColumnsChange(item) {
let totalCount = 0
let selectedCount = 0
this.tableColumns.forEach(column => {
++totalCount
selectedCount += column.visible ? 1 : 0
})
if (selectedCount === 0) {
this.crud.notify('请至少选择一列', CRUD.NOTIFICATION_TYPE.WARNING)
this.$nextTick(function() {
item.visible = true
})
return
}
this.allColumnsSelected = selectedCount === totalCount
this.allColumnsSelectedIndeterminate = selectedCount !== totalCount && selectedCount !== 0
this.updateColumnVisible(item)
},
updateColumnVisible(item) {
const table = this.crud.props.table
const vm = table.$children.find(e => e.prop === item.property)
const columnConfig = vm.columnConfig
if (item.visible) {
// 找出合适的插入点
const columnIndex = this.tableColumns.indexOf(item)
vm.owner.store.commit('insertColumn', columnConfig, columnIndex + 1, null)
} else {
vm.owner.store.commit('removeColumn', columnConfig, null)
}
this.ignoreNextTableColumnsChange = true
},
toggleSearch() {
this.crud.props.searchToggle = !this.crud.props.searchToggle
}
}
}
</script>
<style>
.crud-opts {
padding: 4px 0;
display: -webkit-flex;
display: flex;
align-items: center;
}
.crud-opts .crud-opts-right {
margin-left: auto;
}
.crud-opts .crud-opts-right span {
float: left;
}
</style>
创建Pagination.vue
<!--分页-->
<template>
<el-pagination
:page-size.sync="page.size"
:total="page.total"
:current-page.sync="page.page"
style="margin-top: 8px;"
layout="total, prev, pager, next, sizes"
@size-change="crud.sizeChangeHandler($event)"
@current-change="crud.pageChangeHandler"
/>
</template>
<script>
import { pagination } from '@crud/crud'
export default {
mixins: [pagination()]
}
</script>
创建RR.operation.vue
<!--搜索与重置-->
<template>
<span>
<el-button class="filter-item" size="mini" type="success" icon="el-icon-search" @click="crud.toQuery">搜索</el-button>
<el-button v-if="crud.optShow.reset" class="filter-item" size="mini" type="warning" icon="el-icon-refresh-left" @click="crud.resetQuery()">重置</el-button>
</span>
</template>
<script>
import { crud } from '@crud/crud'
export default {
mixins: [crud()],
props: {
itemClass: {
type: String,
required: false,
default: ''
}
}
}
</script>
创建 UD.operation.vue
<template>
<div>
<el-button v-permission="permission.edit" :loading="crud.status.cu === 2" :disabled="disabledEdit" size="mini" type="primary" icon="el-icon-edit" @click="crud.toEdit(data)" />
<el-popover v-model="pop" v-permission="permission.del" placement="top" width="180" trigger="manual" @show="onPopoverShow" @hide="onPopoverHide">
<p>{{ msg }}</p>
<div style="text-align: right; margin: 0">
<el-button size="mini" type="text" @click="doCancel">取消</el-button>
<el-button :loading="crud.dataStatus[crud.getDataId(data)].delete === 2" type="primary" size="mini" @click="crud.doDelete(data)">确定</el-button>
</div>
<el-button slot="reference" :disabled="disabledDle" type="danger" icon="el-icon-delete" size="mini" @click="toDelete" />
</el-popover>
</div>
</template>
<script>
import CRUD, { crud } from '@crud/crud'
export default {
mixins: [crud()],
props: {
data: {
type: Object,
required: true
},
permission: {
type: Object,
required: true
},
disabledEdit: {
type: Boolean,
default: false
},
disabledDle: {
type: Boolean,
default: false
},
msg: {
type: String,
default: '确定删除本条数据吗?'
}
},
data() {
return {
pop: false
}
},
methods: {
doCancel() {
this.pop = false
this.crud.cancelDelete(this.data)
},
toDelete() {
this.pop = true
},
[CRUD.HOOK.afterDelete](crud, data) {
if (data === this.data) {
this.pop = false
}
},
onPopoverShow() {
setTimeout(() => {
document.addEventListener('click', this.handleDocumentClick)
}, 0)
},
onPopoverHide() {
document.removeEventListener('click', this.handleDocumentClick)
},
handleDocumentClick(event) {
this.pop = false
}
}
}
</script>
项目之外的扩展知识
教程
Java
- JavaGuide :【Java 学习+面试指南】 一份涵盖大部分 Java 程序员所需要掌握的核心知识。
- advanced-java :互联网 Java 工程师进阶知识完全扫盲:涵盖高并发、分布式、高可用、微服务、海量数据处理等领域知识。
- miaosha : 秒杀系统设计与实现.互联网工程师进阶与分析。
- architect-awesome :后端架构师技术图谱。
- toBeTopJavaer :Java 工程师成神之路 。
- technology-talk : 汇总 java 生态圈常用技术框架、开源中间件,系统架构、数据库、大公司架构案例、常用三方类库、项目管理、线上问题排查、个人成长、思考等知识
- tutorials:该项目是一系列小而专注的教程 - 每个教程都涵盖 Java 生态系统中单一且定义明确的开发领域。 当然,它们的重点是 Spring Framework - Spring,Spring Boot 和 Spring Securiyt。 除了 Spring 之外,还有以下技术:核心 Java,Jackson,HttpClient,Guava。
- JCSprout :处于萌芽阶段的 Java 核心知识库。
- fullstack-tutorial :后台技术栈/架构师之路/全栈开发社区,春招/秋招/校招/面试。
- JavaFamily :【互联网一线大厂面试+学习指南】进阶知识完全扫盲。
- JGrowing :Java is Growing up but not only Java。Java 成长路线,但学到不仅仅是 Java。
- bestJavaer : 这是一个成为更好的Java程序员的系列教程。
- interview_internal_reference :2019 年最新总结,阿里,腾讯,百度,美团,头条等技术面试题目,以及答案,专家出题人分析汇总。
- effective-java-3rd-chinese:Effective Java 中文版(第 3 版),Java 四大名著之一,本书一共包含 90 个条目,每个条目讨论 Java 程序设计中的一条规则。这些规则反映了最有经验的优秀程序员在实践中常用的一些有益的做法。
- OnJava8:《On Java 8》中文版,又名《Java 编程思想》第 5 版, Java 四大名著之一。
- java-design-patterns : Design patterns implemented in Java。
数据结构/算法
- LeetCodeAnimation :Demonstrate all the questions on LeetCode in the form of animation.(用动画的形式呈现解 LeetCode 题目的思路)。
- TheAlgorithms-Java :All Algorithms implemented in Java。
- leetcode :多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解。
- LeetCode-Solution-in-Good-Style :这个项目是作者在学习《算法与数据结构》的时候,在 LeetCode(力扣) 上做的练习,刷题以 Java 语言为主。作者在刷题的时候,非常考虑代码质量,他的很多问题的回答都被 Leetcode 官方精选,值得推荐!
- Algorithms-in-4-Steps :四步从 0 到 1 系统学习算法和数据结构。
- algorithm-base :专门为刚开始刷题的同学准备的算法基地,没有最细只有更细,立志用动画将晦涩难懂的算法说的通俗易懂!
计算机基础
- CS-Notes :技术面试必备基础知识、Leetcode 题解、后端面试、Java 面试、春招、秋招、操作系统、计算机网络、系统设计。
- Waking-Up :计算机基础(计算机网络/操作系统/数据库/Git...)面试问题全面总结,包含详细的 follow-up question 以及答案;全部采用【问题+追问+答案】的形式,即拿即用,直击互联网大厂面试 🚀;可用于模拟面试、面试前复习、短期内快速备战面试...
SpringBoot
- springboot-guide :SpringBoot 核心知识点总结。 基于 Spring Boot 2.19+。
- SpringAll :循序渐进,学习 Spring Boot、Spring Boot & Shiro、Spring Cloud、Spring Security & Spring Security OAuth2,博客 Spring 系列源码。
- Springboot-Notebook :一系列以 Spring Boot 为基础开发框架,整合 Redis 、 Rabbitmq 、ES 、MongoDB 、Spring Cloud、Kafka、Skywalking 等互联网主流技术,实现各种常见功能点的综合性案例。
- springboot-learning-example :Spring Boot 实践学习案例,是 Spring Boot 初学者及核心技术巩固的最佳实践。
- spring-boot-demo :spring boot demo 是一个用来深度学习并实战 spring boot 的项目,目前总共包含 63 个集成 demo,已经完成 52 个。
- SpringBoot-Labs :Spring Boot 系列教程。
相关文章:Github 点赞接近 100k 的 SpringBoot 学习教程+实战推荐!牛批!
SpringCloud
- SpringCloudLearning : 方志朋的《史上最简单的 Spring Cloud 教程源码》。
- SpringCloud-Learning : Spring Cloud 基础教程,持续连载更新中。
- spring-cloud : 《Spring Cloud 微服务-全栈技术与案例解析》和《Spring Cloud 微服务 入门 实战与进阶》配套源码。
- spring-cloud-examples :Spring Cloud 学习案例,服务发现、服务治理、链路追踪、服务监控等 (基本没更新了,Spring Cloud 比较老了)。
- SpringCloud :基于 SpringCloud2.1 的微服务开发脚手架,整合了 spring-security-oauth2、nacos、feign、sentinel、springcloud-gateway 等。服务治理方面引入 elasticsearch、skywalking、springboot-admin、zipkin 等,让项目开发快速进入业务开发,而不需过多时间花费在架构搭建上。
相关文章:Github 点赞接近 70k 的 Spring Cloud 学习教程+实战项目推荐!牛批!
大数据
- BigData-Notes :大数据入门指南 ⭐️。
- flink-learning :含 Flink 入门、概念、原理、实战、性能调优、源码解析等内容。
商城
- mall :mall 项目是一套电商系统,包括前台商城系统及后台管理系统,基于 SpringBoot+MyBatis 实现。
- mall-swarm : mall-swarm 是一套微服务商城系统,采用了 Spring Cloud Greenwich、Spring Boot 2、MyBatis、Docker、Elasticsearch 等核心技术,同时提供了基于 Vue 的管理后台方便快速搭建系统。
- onemall :mall 商城,基于微服务的思想,构建在 B2C 电商场景下的项目实战。核心技术栈,是 Spring Boot + Dubbo 。未来,会重构成 Spring Cloud Alibaba 。
- litemall : 又一个小商城。litemall = Spring Boot 后端 + Vue 管理员前端 + 微信小程序用户前端 + Vue 用户移动端。
- xmall :基于 SOA 架构的分布式电商购物商城 前后端分离 前台商城:Vue 全家桶 后台管理系统:Spring/Dubbo/SSM/Elasticsearch/Redis/MySQL/ActiveMQ/Shiro/Zookeeper 等
- newbee-mall :newbee-mall 项目(新蜂商城)是一套电商系统,包括 newbee-mall 商城系统及 newbee-mall-admin 商城后台管理系统,基于 Spring Boot 2.X 及相关技术栈开发。
博客/论坛/考试系统
- vhr :微人事是一个前后端分离的人力资源管理系统,项目采用 SpringBoot+Vue 开发。
- favorites-web :云收藏 Spring Boot 2.X 开源项目。云收藏是一个使用 Spring Boot 构建的开源网站,可以让用户在线随时随地收藏的一个网站,在网站上分类整理收藏的网站或者文章。
- community :开源论坛、问答系统,现有功能提问、回复、通知、最新、最热、消除零回复功能。功能持续更新中…… 技术栈 Spring、Spring Boot、MyBatis、MySQL/H2、Bootstrap。
- VBlog :V 部落,Vue+SpringBoot 实现的多用户博客管理平台!
- My-Blog : My Blog 是由 SpringBoot + Mybatis + Thymeleaf 等技术实现的 Java 博客系统,页面美观、功能齐全、部署简单及完善的代码,一定会给使用者无与伦比的体验。
- uexam :一个非常不错的考试系统!考试系统应用场景还挺多的,不论是对于在校大学生还是已经工作的小伙伴,并且,类似的私活也有很多。相关阅读:《好一个 Spring Boot 开源在线考试系统!解决了我的燃眉之急》 。
- PassJava-Platform :一个基于微服务(SpringBoot、Spring Cloud)的面试刷题系统!相关阅读:《一个基于 Spring Cloud 的面试刷题系统。面试、毕设、项目经验一网打尽》。
相关文章:
权限管理系统
注:权限管理系统在企业级的项目中一般都是非常重要的,如果你需求去实际了解一个不错的权限系统是如何设计的话,推荐你可以参考下面这些开源项目。
- Spring-Cloud-Admin :Cloud-Admin 是国内首个基于 Spring Cloud 微服务化开发平台,具有统一授权、认证后台管理系统,其中包含具备用户管理、资源权限管理、网关 API 管理等多个模块,支持多业务系统并行开发,可以作为后端服务的开发脚手架。代码简洁,架构清晰,适合学习和直接项目中使用。核心技术采用 Spring Boot2 以及 Spring Cloud Gateway 相关核心组件,前端采用 vue-element-admin 组件。
- pig:(gitee)基于 Spring Boot 2.2、 Spring Cloud Hoxton & Alibaba、 OAuth2 的 RBAC 权限管理系统。
- FEBS-Shiro :Spring Boot 2.1.3,Shiro1.4.0 & Layui 2.5.4 权限管理系统。
- eladmin : 项目基于 Spring Boot 2.1.0 、 Jpa、 Spring Security、redis、Vue 的前后端分离的后台管理系统,项目采用分模块开发方式, 权限控制采用 RBAC,支持数据字典与数据权限管理,支持一键生成前后端代码,支持动态路由。
- SpringBoot-Shiro-Vue :提供一套基于 Spring Boot-Shiro-Vue 的权限管理思路.前后端都加以控制,做到按钮/接口级别的权限。
快速开发脚手架
- RuoYi :RuoYi 一款基于基于 SpringBoot 的权限管理系统 易读易懂、界面简洁美观,直接运行即可用 。
- ruoyi-vue-pro : 基于 SpringBoot,Spring Security,JWT,Vue & Element 的前后端分离权限管理系统。
- Guns : 我在上大学的时候就了解和接触过了这个项目,当时我还是一个 Spring 入门不太久的小菜鸟。一晃,不经意间已经过去快 3 年了。Guns 功能齐全 ,采用主流框架 Spring Boot2.0+开发,并且支持 Spring Cloud Alibaba 微服务)。 适合企业后台管理网站的快速开发场景,不论是对于单体和微服务都有支持。
- JHipster :开源应用程序平台,可在几秒钟内创建 Spring Boot + Angular / React 项目!
- JeecgBoot :一款基于代码生成器的 JAVA 快速开发平台,开源界“小普元”超越传统商业企业级开发平台!
- zuihou-admin-cloud :基于
SpringCloud(Hoxton.SR7)
+SpringBoot(2.2.9.RELEASE)
的 SaaS 型微服务快速开发平台,具备用户管理、资源权限管理、网关统一鉴权、Xss 防跨站攻击、自动代码生成、多存储系统、分布式事务、分布式定时任务等多个模块,支持多业务系统并行开发, 支持多服务并行开发,可以作为后端服务的开发脚手架。 - Erupt : 使用 Java 注解,快速开发 Admin 管理后台。零前端代码、零 CURD、不生成任何代码、自动建表、注解式 API,支持所有主流数据库,支持自定义页面,支持多数据源,提供二十几类业务组件,十几种展示形式,支持逻辑删除,动态定时任务,前端后端分离等。 核心技术:Spring Boot、JPA、Reflect、TypeScript、NG-ZORRO 等。
- SmartAdmin : 互联网企业级的通用型中后台解决方案!使用最前沿的前后台技术栈 SpringBoot 和 Vue,前后端分离!代码质量非常高!
- BallCat :一个功能完善的快速开发脚手架!除了最基本的权限管理,定时任务功能之外,还额外支持 XSS 过滤,SQL 防注入、数据脱敏等多种功能
相关文章 :
造轮子
- guide-rpc-framework :一款基于 Netty+Kyro+Zookeeper 实现的自定义 RPC 框架-附详细实现过程和相关教程。
- mini-spring :简化版的 Spring 框架,能帮助你快速熟悉 Spring 源码和掌握 Spring 的核心原理。代码极度简化,保留了 Spring 的核心功能,如 IoC 和 AOP、资源加载器等核心功能。
- itstack-demo-jvm :通过 Java 代码来实现 JVM 的基础功能(搜索解析 class 文件、字节码命令、运行时数据区等。相关阅读:《zachaxy 的手写 JVM 系列》
系统设计
基础框架
- Spring Boot :Spring Boot 可以轻松创建独立的生产级基于 Spring 的应用程序,内置 web 服务器让你可以像运行普通 Java 程序一样运行项 目。另外,大部分 Spring Boot 项目只需要少量的配置即可,这有别于 Spring 的重配置。
- SOFABoot :SOFABoot 基于 Spring Boot ,不过在其基础上增加了 Readiness Check,类隔离,日志空间隔离等等能力。 配套提供的还有:SOFARPC(RPC 框架)、SOFABolt(基于 Netty 的远程通信框架)、SOFARegistry(注册中心)...详情请参考:SOFAStack 。
- Spring Batch : Spring Batch是一个轻量级但功能又十分全面的批处理框架,主要用于批处理场景比如从数据库、文件或队列中读取大量记录。不过,需要注意的是:Spring Batch 不是调度框架。商业和开源领域都有许多优秀的企业调度框架比如 Quartz、XXL-JOB、Elastic-Job。它旨在与调度程序一起工作,而不是取代调度程序。更多介绍请参考 Spring Batch 官方文档,入门教程可以参考 Spring Batch从入门到实战。
数据库
连接池
- Druid : 阿里巴巴数据库事业部出品,为监控而生的数据库连接池。
- HikariCP : 一个可靠的高性能 JDBC 连接池。Springboot 2.0 选择 HikariCP 作为默认数据库连接池。
缓存
- Redisson : Redisson是架设在Redis基础上的一个 Java 驻内存数据网格(In-Memory Data Grid),支持超过 30 个对象和服务:
Set
,SortedSet
,Map
,List
,Queue
,Deque
......。更多介绍请看:《Redisson 项目介绍》
框架
- MyBatis-Plus : MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
数据同步
- Canal [kə'næl] : Canal 译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。
- DataX :DataX 是阿里巴巴集团内被广泛使用的离线数据同步工具/平台,实现包括 MySQL、Oracle、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(OTS)、MaxCompute(ODPS)、DRDS 等各种异构数据源之间高效的数据同步功能。
其他:Flinkx (基于 Flink 的分布式数据同步工具)。
微服务&分布式
API 网关
微服务下一个系统被拆分为多个服务,但是像 安全认证,流量控制,日志,监控等功能是每个服务都需要的,没有网关的话,我们就需要在每个服务中单独实现,这使得我们做了很多重复的事情并且没有一个全局的视图来统一管理这些功能。
综上:一般情况下,网关一般都会提供请求转发、安全认证(身份/权限认证)、流量控制、负载均衡、容灾、日志、监控这些功能。
上面介绍了这么多功能实际上网关主要做了一件事情:请求过滤 。权限校验、流量控制这些都可以通过过滤器实现,请求转也是通过过滤器实现的。
- Kong :Kong 是一个云原生、快速的、可伸缩的分布式微服务抽象层(也称为 API 网关、API 中间件或在某些情况下称为服务网格)。2015 年作为开源项目发布,其核心价值是高性能和可扩展性。
- Soul :高性能、基于 webflux 的反应式 Java API 网关
- Spring Cloud Gateway : 基于 Spring Framework 5.x 和 Spring Boot 2.x 构建的高性能网关。
- Zuul : Zuul 是一个 L7 应用程序网关,它提供了动态路由,监视,弹性,安全性等功能。
配置中心
微服务下,业务的发展一般会导致服务数量的增加,进而导致程序配置(服务地址、数据库参数等等)增多。
传统的配置文件的方式已经无法满足当前需求,主要有两点原因:一是安全性得不到保障(配置放在代码库中容易泄露);二是时效性不行 (修改配置需要重启服务才能生效)。
- Apollo :Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。
- Nacos :Nacos 是 Spring Cloud Alibaba 提供的服务注册发现组件,类似于 Consul、Eureka。并且,提供了分布式配置管理功能。
- Spring Cloud Config : Spring Cloud Config 是 Spring Cloud 家族中最早的配置中心,虽然后来又发布了 Consul 可以代替配置中心功能,但是 Config 依然适用于 Spring Cloud 项目,通过简单的配置即可实现功能。
- Consul :Consul 是 HashiCorp 公司推出的开源软件,提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网格,总之 Consul 提供了一种完整的服务网格解决方案。
相关阅读:微服务架构中配置中心的选择:Apollo VS Spring Cloud Config
链路追踪
目前分布式链路追踪系统基本都是根据谷歌的《Dapper 大规模分布式系统的跟踪系统》这篇论文发展而来,主流的有 Pinpoint,Skywalking ,CAT(当然也有其他的例如 Zipkin,Jaeger 等产品,不过总体来说不如前面选取的 3 个完成度高)等。
- Skywalking : 针对分布式系统的应用性能监控,尤其是针对微服务、云原生和面向容器的分布式系统架构。
- Zipkin :Zipkin 是一个分布式跟踪系统。它有助于收集解决服务体系结构中的延迟问题所需的时序数据。功能包括该数据的收集和查找。
- CAT : CAT 作为服务端项目基础组件,提供了 Java, C/C++, Node.js, Python, Go 等多语言客户端,已经在美团点评的基础架构中间件框架(MVC 框架,RPC 框架,数据库框架,缓存框架等,消息队列,配置系统等)深度集成,为美团点评各业务线提供系统丰富的性能指标、健康状况、实时告警等。
相关阅读: Skywalking 官网对于主流开源链路追踪系统的对比
高并发
消息队列
我们可以把消息队列比作是一个存放消息的容器,当我们需要使用消息的时候可以取出消息供自己使用。
使用消息队列主要有两点好处:一是通过异步处理提高系统性能(削峰、减少响应所需时间);二是降低系统耦合性。
分布式队列 :
- RocketMQ :阿里巴巴开源的一款高性能、高吞吐量的分布式消息中间件。
- Kafaka: Kafka 是一种分布式的,基于发布 / 订阅的消息系统。关于它的入门可以查看:Kafka 入门看这一篇就够了
- RabbitMQ :由 erlang 开发的基于 AMQP(Advanced Message Queue 高级消息队列协议)协议实现的消息队列。
内存队列 :
- Disruptor : Disruptor 是英国外汇交易公司 LMAX 开发的一个高性能队列,研发的初衷是解决内存队列的延迟问题(在性能测试中发现竟然与 I/O 操作处于同样的数量级)。相关阅读 :
数据源&读写分离&分库分表
数据库的数据量大了之后就要考虑读写分离、分库分表,但是一定要尽量能避免分库分表就避免,因为会带来很多其他问题。
- ShardingSphere :ShardingSphere 是一套开源的分布式数据库中间件解决方案组成的生态圈,它由 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar(计划中)这 3 款相互独立的产品组成。 他们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如 Java 同构、异构语言、云原生等各种多样化的应用场景。中文文档:https://shardingsphere.apache.org/document/current/cn/overview/ 。
- MyCat : MyCat 是数据库分库分表的中间件,MyCat 使用最多的两个功能是:读写分离和分库分表。MyCat 是一些社区爱好者在阿里 Cobar 的基础上进行二次开发,解决了 Cobar 当时存 在的一些问题,并且加入了许多新的功能在其中。
- dynamic-datasource-spring-boot-starter :dynamic-datasource-spring-boot-starter 是一个基于 springboot 的快速集成多数据源的启动器。如果说你有配置多数据源、读写分离等需求的话,可以了解一下这个项目。
相关阅读:数据库中间件详解(精品长文)
任务调度/定时任务
任务调度/定时任务在系统中应用太广泛了。 为什么需要任务调度/定时任务呢?
- 时间驱动处理场景:整点发送优惠券,每天更新收益,每天刷新标签数据和人群数据。
- 批量处理数据: 按月批量统计报表数据,批量更新短信状态,实时性要求不高。
- 步执行解耦: 活动状态刷新,异步执行离线查询,与内部逻辑解耦。
这类框架也比较多,下面就带大家看看!
- Quartz :一个很火的开源任务调度框架,Java 定时任务领域的老大哥或者说参考标准, 很多其他任务调度框架都是基于
quartz
开发的,比如当当网的elastic-job
就是基于quartz
二次开发之后的分布式调度解决方案 - XXL-JOB :XXL-JOB 是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
- Elastic-Job :Elastic-Job 是当当网开源的一个基于 Quartz 和 Zookeeper 的分布式调度解决方案,由两个相互独立的子项目 Elastic-Job-Lite 和 Elastic-Job-Cloud 组成,一般我们只要使用 Elastic-Job-Lite 就好。
- EasyScheduler ( 更名为 DolphinScheduler,已经成为 Apache 孵化器项目): Easy Scheduler 是一个分布式工作流任务调度系统,主要解决“复杂任务依赖但无法直接监控任务健康状态”的问题。Easy Scheduler 以 DAG 方式组装任务,可以实时监控任务的运行状态。同时,它支持重试,重新运行等操作... 。
- PowerJob :新一代分布式任务调度与计算框架,支持 CRON、API、固定频率、固定延迟等调度策略,提供工作流来编排任务解决依赖关系,使用简单,功能强大,文档齐全,欢迎各位接入使用!http://www.powerjob.tech/ 。
相关阅读:Spring Job、Quartz、XXL-Job 对比+全解析
搜索引擎
数据库对于全文检索不太友好,这种事情最好交给搜索引擎来做,比如电商系统的商品搜索往往都是基于搜索引擎来做的。
Elasticsearch 在搜索引擎数据库领域排名绝对第一,内核基于 Lucene 构建,支持全文搜索是职责所在,提供了丰富友好的 API。
Elasticsearch 除了做搜索引擎,还常常被拿来做日志检索(著名的 ELK 三件套,讲的就是 Elasticsearch,Logstash,Kibana,专门针对日志采集、存储、查询设计的产品组合,相关阅读:什么是 ELK Stack?)。
Solr 比较老牌了,但是,目前生态以及社区活跃度都比不上 Elasticsearch。
- Elasticsearch:开源,分布式,RESTful 搜索引擎。
- Solr : Solr(读作“solar”)是 Apache Lucene 项目的开源企业搜索平台。
认证授权
- WxJava : WxJava (微信开发 Java SDK),支持包括微信支付、开放平台、小程序、企业微信/企业号和公众号等的后端开发。
- Sa-Token :轻量级 Java 权限认证框架。支持认证授权、单点登录、踢人下线、自动续签等功能。
- JustAuth :小而全而美的第三方登录开源组件。目前已经集成了诸如:Github、Gitee、支付宝、新浪微博、微信、Google、Facebook、Twitter、StackOverflow 等国内外数十家第三方平台。
日志
- EKL 老三件套 : 最原始的时候,ELK 是由 3 个开源项目的首字母构成,分别是 Elasticsearch 、Logstash、Kibana。
- 新一代 ELK 架构 : Elasticsearch+Logstash+Kibana+Beats。
- EFK : EFK 中的 F 代表的是 Fluentd。
- TLog :一个轻量级的分布式日志标记追踪神器,10 分钟即可接入,自动对日志打标签完成微服务的链路追踪。
监控
- Spring Boot Admin :管理和监控 Spring Boot 应用程序。
- Metrics :捕获JVM和应用程序级别的指标。所以你知道发生了什么事。
- Grafana : Grafana是一个跨平台的开源的度量分析和可视化工具,可以通过将采集的数据查询然后可视化的展示,并及时通知。
HTTP 客户端
- Retrofit :适用于 Android 和 Java 的类型安全的 HTTP 客户端。Retrofit 的 HTTP 请求使用的是 OkHttp 库(一款被广泛使用网络框架)。
- Forest :轻量级 HTTP 客户端 API 框架,让 Java 发送 HTTP/HTTPS 请求不再难。它比 OkHttp 和 HttpClient 更高层,是封装调用第三方 restful api client 接口的好帮手,是 retrofit 和 feign 之外另一个选择。
WebSocket
- netty-websocket-spring-boot-starter :帮助你在 Spring Boot 中使用 Netty 来开发 WebSocket 服务器,并像 spring-websocket 的注解开发一样简单
测试
为了能让我们编写的系统更加健壮,必要的测试(UI 测试、单元测试...)是必须的。
框架 :
- JUnit : Java 测试框架。
- Mockito :Mockito 是一个模拟测试框架,可以让你用优雅,简洁的接口写出漂亮的单元测试。(对那些不容易构建的对象用一个虚拟对象来代替,使其在调试期间用来作为真实对象的替代品)
- PowerMock : 编写单元测试仅靠 Mockito 是不够。因为 Mockito 无法 mock 私有方法、final 方法及静态方法等。PowerMock 这个 framework,主要是为了扩展其他 mock 框架,如 Mockito、EasyMock。它使用一个自定义的类加载器,纂改字节码,突破 Mockito 无法 mock 静态方法、构造方法、final 类、final 方法以及私有方法的限制。
- WireMock :模拟 HTTP 服务的工具(Mock your APIs)。
测试平台 :
- MeterSphere : 一站式开源持续测试平台,涵盖测试跟踪、接口测试、性能测试、团队协作等功能,全面兼容 JMeter、Postman、Swagger 等开源、主流标准。
相关阅读:
- The Practical Test Pyramid- Martin Fowler (很赞的一篇文章,不过是英文的)
- 浅谈测试之 PowerMock
工具类库
代码质量
- lombok :使用 Lombok 我们可以简化我们的 Java 代码,比如使用它之后我们通过注释就可以实现 getter/setter、equals 等方法。
- guava :Guava 是一组核心库,其中包括新的集合类型(例如 multimap 和 multiset),不可变集合,图形库以及用于并发、I / O、哈希、原始类型、字符串等的实用程序!
- p3c :Alibaba Java Coding Guidelines pmd implements and IDE plugin。Eclipse 和 IDEA 上都有该插件,推荐使用!
- arthas : Arthas 是 Alibaba 开源的 Java 诊断工具。
- sonarqube :SonarQube 支持所有开发人员编写更干净,更安全的代码。
- checkstyle :Checkstyle 是一种开发工具,可帮助程序员编写符合编码标准的 Java 代码。它使检查 Java 代码的过程自动化,从而使人们不必执行这项无聊(但很重要)的任务。这使其成为想要实施编码标准的项目的理想选择。
- pmd : 可扩展的多语言静态代码分析器。
- spotbugs :SpotBugs 是 FindBugs 的继任者。静态分析工具,用于查找 Java 代码中的错误。
- hutool : Hutool 是一个 Java 工具包,也只是一个工具包,它帮助我们简化每一行代码,减少每一个方法,让 Java 语言也可以“甜甜的”。
Excel
- easyexcel :快速、简单避免 OOM 的 java 处理 Excel 工具。
- pdfbox :用于处理 PDF 文档的开放源码 Java 工具。该项目允许创建新的 PDF 文档、对现有文档进行操作以及从文档中提取内容。PDFBox 还包括几个命令行实用程序。PDFBox 是在 Apache 2.0 版许可下发布的。
- OpenPDF :OpenPDF 是一个免费的 Java 库,用于使用 LGPL 和 MPL 开源许可创建和编辑 PDF 文件。OpenPDF 基于 iText 的一个分支。
- itext7 :iText 7 代表了想要利用利用好 PDF 的开发人员的更高级别的 sdk。iText 7 配备了更好的文档引擎、高级和低级编程功能以及创建、编辑和增强 PDF 文档的能力,几乎对每个工作流都有好处。
- FOP :Apache FOP 项目的主要的输出目标是 PDF。
API 请求
- Insomnia :像人类而不是机器人一样调试 API。我平时经常用的,界面美观且轻量,总之很喜欢。
- postman :API 请求生成器。
- postwoman :API 请求生成器-一个免费、快速、漂亮的 Postma 替代品。
验证码
- EasyCaptcha :Java图形验证码,支持gif、中文、算术等类型,可用于Java Web、JavaSE等项目。
- AJ-Captcha :行为验证码(滑动拼图、点选文字),前后端(java)交互。
其他
- oshi :一款为 Java 语言提供的基于 JNA 的(本机)操作系统和硬件信息库。
Devpos
CI
- Jenkins : Jenkins 是领先的开源自动化服务器。它使用 Java 构建,提供了 1600 多个插件来支持几乎任何东西的自动化,从而使人类实际上可以将时间花在做机器无法做到的事情上。
工具
开发必备
- Redis Manager :Redis Manager 是 Redis 一站式管理平台,支持集群(cluster、master-replica、sentinel)的监控、安装(除 sentinel)、管理、告警以及基本的数据操作功能。
- Portainer :可视化管理 Docker 和 Kubernetes。相关阅读:《吊炸天的 Docker 图形化工具 Portainer,必须推荐给你!》。
Markdown
- Typora :我一直用的一款 Markdown 工具,直接文件夹试图和目录试图,支持 Markdown 格式直接导出成 PDF、HTML 等格式。
- markdown-here :使用 markdown 语法发邮件,并且提供多种主题,快来拯救你的邮件格式吧!
大数据
- Spark :Spark 是用于大规模数据处理的统一分析引擎。
- Flink :Apache Flink 是一个框架和分布式处理引擎,用于在无边界和有边界数据流上进行有状态的计算。Flink 能在所有常见集群环境中运行,并能以内存速度和任意规模进行计算。
- HBase: HBase – Hadoop Database,是一个高可靠性、高性能、面向列、可伸缩的分布式存储系统,利用 HBase 技术可在廉价 PC Server 上搭建起大规模结构化存储集群。
- Flume :Apache Flume 是一个分布式的、可靠的、可用的,从多种不同的源收集、聚集、移动大量日志数据到集中数据存储的系统。
- Storm : 一个分布式,高容错的实时计算系统。
机器学习
- Deeplearning4j : Deeplearning4j 是第一个为 Java 和 Scala 编写的商业级,开源,分布式深度学习库。
- Smile :基于 Java 和 Scala 的机器学习库。
相关阅读:Java 能用于机器学习和数据科学吗?-InfoQ
其他
- ip2region :最自由的 ip 地址查询库,ip 到地区的映射库,提供 Binary,B 树和纯内存三种查询算法,妈妈再也不用担心我的 ip 地址定位。
- thingsboard :开源物联网平台 - 设备管理,数据收集,处理和可视化。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?