赞
踩
linux下安装ffmpeg的详细教程 - 完全参照这个安装的
FFmpeg视频处理入门教程----从安装到使用(Linux版)
linux下ffmpeg安装教程(小学生都能看懂)
文件上传java报Processing of multipart/form-data request failed. java.io.EOFException: Unexpected EOF read
1、下载解压
wget http://www.ffmpeg.org/releases/ffmpeg-3.1.tar.gz
tar -zxvf ffmpeg-3.1.tar.gz
2、 进入解压后目录,输入如下命令/usr/local/ffmpeg为自己指定的安装目录
cd ffmpeg-3.1
./configure --prefix=/usr/local/ffmpeg
make && make install
3、配置变量
vi /etc/profile
在最后PATH添加环境变量:
export PATH=$PATH:/usr/local/ffmpeg/bin
保存退出
查看是否生效
source /etc/profile 设置生效
4、查看版本
ffmpeg -version # 查看版本
注意,若安装过程中出现以下错误
yasm/nasm not found or too old. Use –disable-yasm for a crippled build. If you think configure made a mistake, make sure you are using the latest version from Git. If the latest version fails, report the problem to the ffmpeg-user@ffmpeg.org mailing list or IRC #ffmpeg on irc.freenode.net. Include the log file “config.log” produced by configure as this will help solve the problem.
需要安装 yasm
wget http://www.tortall.net/projects/yasm/releases/yasm-1.3.0.tar.gz
tar -zxvf yasm-1.3.0.tar.gz
cd yasm-1.3.0
./configure
make && make install
因为分为本地开发环境 和 正式环境,如果是每次切换环境,还要把配置文件的参数改来改去的话,就太麻烦了,因此使用springboot支持的profile指定多环境配置。
本地开发dev环境
和 线上正式prod环境
,方便使用maven切换环境,并且此时可在application.properties中使用@profileActive@来引用maven选择的环境,让指定的环境的配置文件生效。<?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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.1</version> <relativePath/> </parent> <modelVersion>4.0.0</modelVersion> <groupId>com.easypan</groupId> <artifactId>easypan</artifactId> <version>1.0</version> <packaging>jar</packaging> <name>easypan</name> <description>easypan</description> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <skipTests>true</skipTests> <springboot.version>2.6.1</springboot.version> <mybatis.version>1.3.2</mybatis.version> <logback.version>1.2.10</logback.version> <mysql.version>8.0.23</mysql.version> <aspectjweaver.version>1.9.4</aspectjweaver.version> <okhttp3.version>3.2.0</okhttp3.version> <fastjson.version>1.2.66</fastjson.version> <commons.lang3.version>3.4</commons.lang3.version> <commons.codec.version>1.9</commons.codec.version> <commons.io.version>2.5</commons.io.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> </exclusion> <exclusion> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> </exclusion> </exclusions> </dependency> <!--邮件发送--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> <version>${springboot.version}</version> </dependency> <!--redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>${springboot.version}</version> </dependency> <!--mybatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis.version}</version> </dependency> <!-- 数据库--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!-- 日志版本 --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>${logback.version}</version> </dependency> <!--切面--> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>${aspectjweaver.version}</version> </dependency> <!--okhttp--> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>${okhttp3.version}</version> </dependency> <!--fastjson--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency> <!--apache common--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>${commons.lang3.version}</version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>${commons.codec.version}</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>${commons.io.version}</version> </dependency> </dependencies> <profiles> <profile> <!-- 开发环境 --> <id>dev</id> <properties> <profileActive>dev</profileActive> </properties> <!-- 默认激活的环境 --> <activation> <activeByDefault>true</activeByDefault> </activation> </profile> <profile> <!-- 生产环境 --> <id>prod</id> <properties> <profileActive>prod</profileActive> </properties> </profile> </profiles> <build> <resources> <resource> <directory>src/main/resources</directory> <includes> <include>application-${profileActive}.yml</include> <include>application.yml</include> <include>**/*.xml</include> <include>application.properties</include> <include>application-${profileActive}.properties</include> </includes> <filtering>true</filtering> </resource> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.2.6.RELEASE</version> <executions> <execution> <goals> <goal> repackage </goal> </goals> </execution> </executions> <configuration> <mainClass>com.easypan.EasyPanApplication</mainClass> </configuration> </plugin> </plugins> </build> </project>
使用@profileActive@可以引用使用maven选择的环境
#spring.profiles.active=prod
spring.profiles.active=@profileActive@
本地开发环境配置
# 应用服务 WEB 访问端口 server.port=7090 server.servlet.context-path=/api #session过期时间 60M 一个小时 server.servlet.session.timeout=PT60M #处理favicon spring.mvc.favicon.enable=false spring.servlet.multipart.max-file-size=15MB spring.servlet.multipart.max-request-size=15MB #错误页处理 spring.mvc.throw-exception-if-no-handler-found=true spring.web.resources.add-mappings=false #数据库配置 spring.datasource.url=jdbc:mysql://127.0.0.1:3306/easypan?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.hikari.pool-name=HikariCPDatasource spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.idle-timeout=180000 spring.datasource.hikari.maximum-pool-size=10 spring.datasource.hikari.auto-commit=true spring.datasource.hikari.max-lifetime=1800000 spring.datasource.hikari.connection-timeout=30000 spring.datasource.hikari.connection-test-query=SELECT 1 #发送邮件配置相关 # 配置邮件服务器的地址 smtp.qq.com spring.mail.host=smtp.qq.com # 配置邮件服务器的端口(465或587) spring.mail.port=587 # 配置用户的账号 spring.mail.username=1255112011@qq.com # 配置用户的密码 spring.mail.password=填入自己的授权码 # 配置默认编码 spring.mail.default-encoding=UTF-8 # SSL 连接配置 spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory # 开启 debug,这样方便开发者查看邮件发送日志 spring.mail.properties.mail.debug=true #邮件配置结束 #Spring redis配置 # Redis数据库索引(默认为0) spring.redis.database=0 spring.redis.host=127.0.0.1 spring.redis.port=6379 # 连接池最大连接数(使用负值表示没有限制) spring.redis.jedis.pool.max-active=20 # 连接池最大阻塞等待时间(使用负值表示没有限制) spring.redis.jedis.pool.max-wait=-1 # 连接池中的最大空闲连接 spring.redis.jedis.pool.max-idle=10 # 连接池中的最小空闲连接 spring.redis.jedis.pool.min-idle=0 # 连接超时时间(毫秒) spring.redis.timeout=2000 #项目目录 project.folder=D:/document/easypan/easypan-java/ #日志级别配置 log.root.level=info #超级管理员id admin.emails=1255112011@qq.com #是否是开发环境 dev=true ##qq登陆相关## qq.app.id=123456 qq.app.key=123456 qq.url.authorization=https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=%s&redirect_uri=%s&state=%s qq.url.access.token=https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s qq.url.openid=https://graph.qq.com/oauth2.0/me?access_token=%S qq.url.user.info=https://graph.qq.com/user/get_user_info?access_token=%s&oauth_consumer_key=%s&openid=%s qq.url.redirect=http://easypan.wuhancoder.com/qqlogincalback
# 应用服务 WEB 访问端口 server.port=7091 server.servlet.context-path=/api #session过期时间 60M 一个小时 server.servlet.session.timeout=PT60M #处理favicon spring.mvc.favicon.enable=false spring.servlet.multipart.max-file-size=15MB spring.servlet.multipart.max-request-size=15MB #错误页处理 spring.mvc.throw-exception-if-no-handler-found=true spring.web.resources.add-mappings=false #数据库配置 spring.datasource.url=jdbc:mysql://127.0.0.1:3306/easypan?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false spring.datasource.username=root spring.datasource.password=数据库密码 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.hikari.pool-name=HikariCPDatasource spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.idle-timeout=180000 spring.datasource.hikari.maximum-pool-size=10 spring.datasource.hikari.auto-commit=true spring.datasource.hikari.max-lifetime=1800000 spring.datasource.hikari.connection-timeout=30000 spring.datasource.hikari.connection-test-query=SELECT 1 #发送邮件配置相关 # 配置邮件服务器的地址 smtp.qq.com spring.mail.host=smtp.qq.com # 配置邮件服务器的端口(465或587) spring.mail.port=587 # 配置用户的账号 spring.mail.username=1255112011@qq.com # 配置用户的密码 spring.mail.password=填入自己的授权码 # 配置默认编码 spring.mail.default-encoding=UTF-8 # SSL 连接配置 spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory # 开启 debug,这样方便开发者查看邮件发送日志 spring.mail.properties.mail.debug=true #邮件配置结束 #Spring redis配置 # Redis数据库索引(默认为0) spring.redis.database=0 spring.redis.host=127.0.0.1 spring.redis.port=6379 spring.redis.password=填写redis密码 # 连接池最大连接数(使用负值表示没有限制) spring.redis.jedis.pool.max-active=20 # 连接池最大阻塞等待时间(使用负值表示没有限制) spring.redis.jedis.pool.max-wait=-1 # 连接池中的最大空闲连接 spring.redis.jedis.pool.max-idle=10 # 连接池中的最小空闲连接 spring.redis.jedis.pool.min-idle=0 # 连接超时时间(毫秒) spring.redis.timeout=2000 #项目目录 project.folder=/usr/local/blog/easypan/backend/ #日志级别配置 log.root.level=info #超级管理员id admin.emails=1255112011@qq.com #是否是开发环境 dev=true ##qq登陆相关## qq.app.id=123456 qq.app.key=123456 qq.url.authorization=https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=%s&redirect_uri=%s&state=%s qq.url.access.token=https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s qq.url.openid=https://graph.qq.com/oauth2.0/me?access_token=%S qq.url.user.info=https://graph.qq.com/user/get_user_info?access_token=%s&oauth_consumer_key=%s&openid=%s qq.url.redirect=http://easypan.wuhancoder.com/qqlogincalback
<?xml version="1.0" encoding="UTF-8" ?> <configuration scan="true" scanPeriod="10 minutes"> <include resource="org/springframework/boot/logging/logback/defaults.xml" /> <appender name="stdot" class="ch.qos.logback.core.ConsoleAppender"> <!-- <layout class="ch.qos.logback.classic.PatternLayout"> <pattern>%d{yyyy-MM-dd HH:mm:ss,GMT+8} [%p][%-40.40logger{39}][%M][%L]-> %m%n</pattern> </layout>--> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <!-- %d 是时间、 %p 是日志级别 %5p指定日志级别最小宽度为5且右对齐(不足时左补空)、 ${PID:- } 的意思是从上下文里面去取进程id,如果能取到的就取,娶不到的话,就是后面的空字符串、 %t 是线程名称, %5.10t 的意思是最小占5位最多占10位且默认情况下会右对齐(如果需要左对齐,应该写%-5.10t),但一般写%10.10t对的比较整齐一点,如果超过了最大长度,左边会被截掉。对齐的含义只会出现在当前的内容长度不够最小位数的情况 %m是打印的日志内容 %n是换行 %logger 输出日志的logger名,%logger{36} 表示logger名字最长36个字符,否则按照句点分割,%logger{0} 设置为0表示只输入logger最右边,。 %line %clr是 ColorConverter 中实现的(如果需要修改,那么需要继承ColorConverter修改里面里面的实现即可),可以写的颜色有faint、red、green、yellow、blue、magenta、cyan --> <pattern>%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:-取不到}){magenta} %clr(---){faint} %clr([%5.10t]){yellow} [%logger{0}:%line] %m%n</pattern> </encoder> </appender> <springProperty scope="context" name="log.path" source="project.folder"/> <springProperty scope="context" name="log.root.level" source="log.root.level"/> <property name="LOG_FOLDER" value="logs"/> <property name="LOG_FILE_NAME" value="easypan.log"/> <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${log.path}/${LOG_FOLDER}/${LOG_FILE_NAME}</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${log.path}/${LOG_FOLDER}/${LOG_FILE_NAME}.%d{yyyyMMdd}.%i</FileNamePattern> <cleanHistoryOnStart>true</cleanHistoryOnStart> <TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <MaxFileSize>20MB</MaxFileSize> </TimeBasedFileNamingAndTriggeringPolicy> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <charset>utf-8</charset> <pattern>%d{yyyy-MM-dd HH:mm:ss,GMT+8} [%p][%c][%M][%L]-> %m%n</pattern> </encoder> <append>false</append> <prudent>false</prudent> </appender> <logger name="com.easypan.mappers" level="debug" additivity="false"> <appender-ref ref="stdot"/> </logger> <root level="${log.root.level}"> <appender-ref ref="stdot"/> <appender-ref ref="file"/> </root> </configuration>
有两个东西需要配置:
网盘的前端项目访问 和 后台接口请求转发 配置
文件上传配置参数(遇到过问题:上传到5M的时候,就停了,然后报错如下。本地是可以上传超过5M的,因此怀疑是nginx的配置相关的问题,可参考这篇解决:文件上传java报Processing of multipart/form-data request failed. java.io.EOFException: Unexpected EOF read
Caused by: java.io.IOException: org.apache.tomcat.util.http.fileupload.impl.IOFileUploadException: Processing of multipart/form-data request failed. java.io.EOFException at org.apache.catalina.connector.Request.parseParts(Request.java:2966) at org.apache.catalina.connector.Request.getParts(Request.java:2823) at org.apache.catalina.connector.RequestFacade.getParts(RequestFacade.java:1098) at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:95) ... 43 common frames omitted Caused by: org.apache.tomcat.util.http.fileupload.impl.IOFileUploadException: Processing of multipart/form-data request failed. java.io.EOFException at org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:292) at org.apache.catalina.connector.Request.parseParts(Request.java:2921) ... 46 common frames omitted Caused by: org.apache.catalina.connector.ClientAbortException: java.io.EOFException at org.apache.catalina.connector.InputBuffer.realReadBytes(InputBuffer.java:322) at org.apache.catalina.connector.InputBuffer.checkByteBufferEof(InputBuffer.java:600) at org.apache.catalina.connector.InputBuffer.read(InputBuffer.java:340) at org.apache.catalina.connector.CoyoteInputStream.read(CoyoteInputStream.java:132) at org.apache.tomcat.util.http.fileupload.MultipartStream$ItemInputStream.makeAvailable(MultipartStream.java:975) at org.apache.tomcat.util.http.fileupload.MultipartStream$ItemInputStream.read(MultipartStream.java:879) at java.io.InputStream.read(InputStream.java:101) at org.apache.tomcat.util.http.fileupload.util.Streams.copy(Streams.java:97) at org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:288) ... 47 common frames omitted Suppressed: org.apache.catalina.connector.ClientAbortException: java.io.EOFException at org.apache.catalina.connector.InputBuffer.realReadBytes(InputBuffer.java:322) at org.apache.catalina.connector.InputBuffer.checkByteBufferEof(InputBuffer.java:600) at org.apache.catalina.connector.InputBuffer.read(InputBuffer.java:340) at org.apache.catalina.connector.CoyoteInputStream.read(CoyoteInputStream.java:132) at org.apache.tomcat.util.http.fileupload.MultipartStream$ItemInputStream.makeAvailable(MultipartStream.java:975) at org.apache.tomcat.util.http.fileupload.MultipartStream$ItemInputStream.close(MultipartStream.java:919) at org.apache.tomcat.util.http.fileupload.MultipartStream$ItemInputStream.close(MultipartStream.java:898) at org.apache.tomcat.util.http.fileupload.util.Streams.copy(Streams.java:117) ... 48 common frames omitted Caused by: java.io.EOFException: null at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.fillReadBuffer(NioEndpoint.java:1294) at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.read(NioEndpoint.java:1206) at org.apache.coyote.http11.Http11InputBuffer.fill(Http11InputBuffer.java:805) at org.apache.coyote.http11.Http11InputBuffer.access$400(Http11InputBuffer.java:42) at org.apache.coyote.http11.Http11InputBuffer$SocketInputBuffer.doRead(Http11InputBuffer.java:1172) at org.apache.coyote.http11.filters.IdentityInputFilter.doRead(IdentityInputFilter.java:101) at org.apache.coyote.http11.Http11InputBuffer.doRead(Http11InputBuffer.java:249) at org.apache.coyote.Request.doRead(Request.java:640) at org.apache.catalina.connector.InputBuffer.realReadBytes(InputBuffer.java:317) ... 55 common frames omitted Caused by: java.io.EOFException: null at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.fillReadBuffer(NioEndpoint.java:1294) at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.read(NioEndpoint.java:1206) at org.apache.coyote.http11.Http11InputBuffer.fill(Http11InputBuffer.java:805) at org.apache.coyote.http11.Http11InputBuffer.access$400(Http11InputBuffer.java:42) at org.apache.coyote.http11.Http11InputBuffer$SocketInputBuffer.doRead(Http11InputBuffer.java:1172) at org.apache.coyote.http11.filters.IdentityInputFilter.doRead(IdentityInputFilter.java:101) at org.apache.coyote.http11.Http11InputBuffer.doRead(Http11InputBuffer.java:249) at org.apache.coyote.Request.doRead(Request.java:640) at org.apache.catalina.connector.InputBuffer.realReadBytes(InputBuffer.java:317) ... 55 common frames omitted
./sbin/nginx -t 检查nginx配置文件是否有语法错误
./sbin/nginx -s reload 当修改完nginx配置文件后,重新载入配置文件
events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; client_max_body_size 50m; client_body_buffer_size 5M; client_header_timeout 1m; client_body_timeout 1m; gzip on; gzip_min_length 1k; gzip_buffers 4 16k; gzip_comp_level 4; gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; gzip_vary on; server { listen 80; server_name 119.23.61.24; location / { root /usr/local/blog/bootblog/web/; index index.html index.htm; try_files $uri $uri/ /index.html; } location ^~ /api/ { proxy_pass http://119.23.61.24:9091/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } server { listen 81; server_name 119.23.61.24; ## 个人博客前端项目 location / { root /usr/local/blog/bootblog/admin/; index index.html index.htm; try_files $uri $uri/ /index.html; } ## 个人博客后台接口 location ^~ /api/ { proxy_pass http://119.23.61.24:9091/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /img/ { root /usr/local/blog/bootblog/res/img/; autoindex on; autoindex_exact_size off; autoindex_format html; autoindex_localtime on; } } server { listen 82; server_name 119.23.61.24; location / { proxy_pass http://119.23.61.24:8080/websocket; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_set_header Host $host:$server_port; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } server { listen 83; server_name 119.23.61.24; location / { root /usr/local/blog/bootblog/res/img/; autoindex on; index a.html; #指明index文件,默认为index.html,如果此文件不存在,访问域名时会显示目录结构 autoindex_exact_size off; autoindex_format json; #指明返回的为json格式,也可以是html格式 autoindex_localtime on; } } server { listen 7090; server_name 119.23.61.24; ## 网盘前端项目 location / { root /usr/local/blog/easypan/web/; index index.html index.htm; try_files $uri $uri/ /index.html; } ## 后台接口请求转发 location ^~ /api/ { proxy_pass http://localhost:7091; ## 注意这里的7091后面没有带/,所以匹配到的请求的请求路径/api/xxx都会拼接到7091后面去,即形成了http://localhost:7091/api/xxx ## 添加如下的配置,否则会出现上传超过5M就报错了 client_max_body_size 500m; proxy_max_temp_file_size 1024m; #client_header_buffer_size 24k; client_body_buffer_size 24k; #client_max_body_size 200m; client_body_timeout 3600s; client_body_temp_path /temp; resolver_timeout 3600s; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } }
nohup java -jar easypan.jar > /dev/null &
ps -ef|grep java # 查看是否起来了
netstat -anp |grep 7091 # 查看端口是否占用了
访问即可:http://119.23.61.24:7090/login
步骤:
如下放置qq登录的图标,并且绑定点击事件,触发登录。所谓的触发登录其实就是请求后台接口,来获取qq登录的授权的url
,然后前端再让用户重定向到这个qq登录的授权的url
,用户重定向到qq登录授权的url后的这个时候跟我们没关系了,这时qq它自己要完成验证用户身份
(它怎么完成认证的,我们并不关心),并且qq在用户确认授权后,就会重定向到我们指定的页面
。
https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id={appId}&redirect_uri={redirectUri}&state={state}
qq肯定要对这个用户进行身份认证
(你是哪个qq用户),qq确认是自己的用户后,然后看一下这个appId的客户端有没有在自己这里登记过
(需要申请qq互联登录),再看下这个重定向地址是不是在登记的时候记录了
,发现都没问题,才会让用户重定向到redirectUri的地址,并且会携带code授权码
和 我们带过去的 state
,如:http://easypan.wuhancoder.com/qqlogincalback?code=xxx&state=1234
callbackUrl的参数
,这个是当用户未登录我们的系统时,然后用户在地址栏自己输出了想去的地址,系统发现用户未登录,就会让用户重定向到登录页面,并且把想去的地址用url编码的方式处理,拼接在地址栏后面(这个在axios的响应拦截器中做
),然后随着获取登录授权url时,把这个callbackUrl存放到session(以state为key,callback为value,其中state是随机生成的)
,也就是说此时后台已经记录到当前这个用户想要去的地址。<div class="login-btn-qq" v-if="opType == 1"> 快捷登录 <img src="@/assets/qq.png" @click="qqLogin" /> </div> <script setup> import { ref, reactive, getCurrentInstance, nextTick, onMounted } from "vue"; import { useRouter, useRoute } from "vue-router"; import md5 from "js-md5"; const { proxy } = getCurrentInstance(); const router = useRouter(); const route = useRoute(); const api = { checkCode: "/api/checkCode", sendMailCode: "/sendEmailCode", register: "/register", login: "/login", resetPwd: "/resetPwd", qqlogin: "/qqlogin", }; //QQ登录 const qqLogin = async () => { let result = await proxy.Request({ url: api.qqlogin, params: { callbackUrl: route.query.redirectUrl || "", }, }); if (!result) { return; } proxy.VueCookies.remove("userInfo"); document.location.href = result.data; }; </script>
随机生成的state,然后把用户想要去的地址,保存到session中,把qq登录授权的url返回给前端,让前端作地址跳转。
@RequestMapping("qqlogin")
@GlobalInterceptor(checkLogin = false, checkParams = true)
public ResponseVO qqlogin(HttpSession session, String callbackUrl) throws UnsupportedEncodingException {
String state = StringTools.getRandomString(Constants.LENGTH_30);
if (!StringTools.isEmpty(callbackUrl)) {
session.setAttribute(state, callbackUrl);
}
String url = String.format(appConfig.getQqUrlAuthorization(), appConfig.getQqAppId(), URLEncoder.encode(appConfig.getQqUrlRedirect(), "utf-8"), state);
return getSuccessResponseVO(url);
}
上面用户通过后台返回的qq登录授权url,重定向到qq登录授权的页面,当用户点击登录授权后,qq就会让用户重定向到我们指定的重定向地址redirectUri(这个地址qq也必须知道,登记在了qq互联),并且在地址后面拼接上请求参数code和state
,
http://easypan.wuhancoder.com/qqlogincalback?code=xxx&state=1234
,这个code就代表qq向我们的应用网站 授权了这个用户,我们后面要拿着这个code去获取AccessToken访问令牌的。这个state可以用于记录用户登陆前的一些状态信息(因为qq重定向后,会把这个参数原封不动的带过来)重定向的地址会对应到我们的vue项目的一个页面
,在这个页面里,就可以拿到qq返回给我们的code和state,然后把code和state传给后台,让后台去获取访问令牌,进而获取用户的openid和用户相关信息。也就是,在后台没返回之前,用户将会停留在下面页面一段时间。
<template> <div>登录中,请勿刷新页面</div> </template> <script setup> import { ref, reactive, getCurrentInstance, nextTick } from "vue"; import { useRouter, useRoute } from "vue-router"; const { proxy } = getCurrentInstance(); const router = useRouter(); const route = useRoute(); const api = { logincallback: "/qqlogin/callback", }; const login = async () => { let result = await proxy.Request({ url: api.logincallback, params: router.currentRoute.value.query, errorCallback: () => { router.push("/"); }, }); if (!result) { return; } let redirectUrl = result.data.callbackUrl || "/"; if (redirectUrl == "/login") { redirectUrl = "/"; } proxy.VueCookies.set("userInfo", result.data.userInfo, 0); console.log("路径",redirectUrl); router.push(redirectUrl); }; login(); </script> <style lang="scss" scoped></style>
用户授权后,重定向到vue前端的页面,在此页面中将地址栏路径后面的code和state获取到,并传给后台,后台将code传给qq后台,来获取访问令牌。
@RequestMapping("qqlogin/callback") @GlobalInterceptor(checkLogin = false, checkParams = true) public ResponseVO qqLoginCallback(HttpSession session, @VerifyParam(required = true) String code, @VerifyParam(required = true) String state) { // 获取用户的qq信息,记录用户 SessionWebUserDto sessionWebUserDto = userInfoService.qqLogin(code); // 将用户身份存入会话 session.setAttribute(Constants.SESSION_KEY, sessionWebUserDto); // 1. 用户登陆前的状态信息(使用state从会话中获取) // 2. 当前用户信息 Map<String, Object> result = new HashMap<>(); result.put("callbackUrl", session.getAttribute(state)); result.put("userInfo", sessionWebUserDto); return getSuccessResponseVO(result); }
获取登录信息
@Override public SessionWebUserDto qqLogin(String code) { // 根据code,获取访问令牌 String accessToken = getQQAccessToken(code); // 根据访问令牌,获取 openid 用户唯一id String openId = getQQOpenId(accessToken); // 根据qq唯一id 判断是否第一次登录我们的网站 UserInfo user = this.userInfoMapper.selectByQqOpenId(openId); String avatar = null; if (null == user) { // 如果是第一次登录网站,则获取qq用户信息 QQInfoDto qqInfo = getQQUserInfo(accessToken, openId); user = new UserInfo(); String nickName = qqInfo.getNickname(); nickName = nickName.length() > Constants.LENGTH_150 ? nickName.substring(0, 150) : nickName; avatar = StringTools.isEmpty(qqInfo.getFigureurl_qq_2()) ? qqInfo.getFigureurl_qq_1() : qqInfo.getFigureurl_qq_2(); Date curDate = new Date(); //上传头像到本地 user.setQqOpenId(openId); user.setJoinTime(curDate); user.setNickName(nickName); user.setQqAvatar(avatar); user.setUserId(StringTools.getRandomString(Constants.LENGTH_10)); user.setLastLoginTime(curDate); user.setStatus(UserStatusEnum.ENABLE.getStatus()); user.setUseSpace(0L); user.setTotalSpace(redisComponent.getSysSettingsDto().getUserInitUseSpace() * Constants.MB); this.userInfoMapper.insert(user); user = userInfoMapper.selectByQqOpenId(openId); } else { UserInfo updateInfo = new UserInfo(); updateInfo.setLastLoginTime(new Date()); avatar = user.getQqAvatar(); this.userInfoMapper.updateByQqOpenId(updateInfo, openId); } if (UserStatusEnum.DISABLE.getStatus().equals(user.getStatus())) { throw new BusinessException("账号被禁用无法登录"); } SessionWebUserDto sessionWebUserDto = new SessionWebUserDto(); sessionWebUserDto.setUserId(user.getUserId()); sessionWebUserDto.setNickName(user.getNickName()); sessionWebUserDto.setAvatar(avatar); if (ArrayUtils.contains(appConfig.getAdminEmails().split(","), user.getEmail() == null ? "" : user.getEmail())) { sessionWebUserDto.setAdmin(true); } else { sessionWebUserDto.setAdmin(false); } UserSpaceDto userSpaceDto = new UserSpaceDto(); userSpaceDto.setUseSpace(fileInfoService.getUserUseSpace(user.getUserId())); userSpaceDto.setTotalSpace(user.getTotalSpace()); redisComponent.saveUserSpaceUse(user.getUserId(), userSpaceDto); return sessionWebUserDto; }
获取访问令牌的url,如:https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&client_id={appId}&client_secret={appKey}&code={code}&redirect_uri={redirectUrl}
private String getQQAccessToken(String code) { /** * 返回结果是字符串 access_token=*&expires_in=7776000&refresh_token=* 返回错误 callback({UcWebConstants.VIEW_OBJ_RESULT_KEY:111,error_description:"error msg"}) */ String accessToken = null; String url = null; try { url = String.format(appConfig.getQqUrlAccessToken(), appConfig.getQqAppId(), appConfig.getQqAppKey(), code, URLEncoder.encode(appConfig.getQqUrlRedirect(), "utf-8")); } catch (UnsupportedEncodingException e) { logger.error("encode失败"); } String tokenResult = OKHttpUtils.getRequest(url); if (tokenResult == null || tokenResult.indexOf(Constants.VIEW_OBJ_RESULT_KEY) != -1) { logger.error("获取qqToken失败:{}", tokenResult); throw new BusinessException("获取qqToken失败"); } String[] params = tokenResult.split("&"); if (params != null && params.length > 0) { for (String p : params) { if (p.indexOf("access_token") != -1) { accessToken = p.split("=")[1]; break; } } } return accessToken; }
携带访问令牌,https://graph.qq.com/oauth2.0/me?access_token={accessToken}
private String getQQOpenId(String accessToken) throws BusinessException { // 获取openId String url = String.format(appConfig.getQqUrlOpenId(), accessToken); String openIDResult = OKHttpUtils.getRequest(url); String tmpJson = this.getQQResp(openIDResult); if (tmpJson == null) { logger.error("调qq接口获取openID失败:tmpJson{}", tmpJson); throw new BusinessException("调qq接口获取openID失败"); } Map jsonData = JsonUtils.convertJson2Obj(tmpJson, Map.class); if (jsonData == null || jsonData.containsKey(Constants.VIEW_OBJ_RESULT_KEY)) { logger.error("调qq接口获取openID失败:{}", jsonData); throw new BusinessException("调qq接口获取openID失败"); } return String.valueOf(jsonData.get("openid")); }
https://graph.qq.com/user/get_user_info?access_token={accessToken}&oauth_consumer_key=%s&openid={openid}
private QQInfoDto getQQUserInfo(String accessToken, String qqOpenId) throws BusinessException { String url = String.format(appConfig.getQqUrlUserInfo(), accessToken, appConfig.getQqAppId(), qqOpenId); String response = OKHttpUtils.getRequest(url); if (StringUtils.isNotBlank(response)) { QQInfoDto qqInfo = JsonUtils.convertJson2Obj(response, QQInfoDto.class); if (qqInfo.getRet() != 0) { logger.error("qqInfo:{}", response); throw new BusinessException("调qq接口获取用户信息异常"); } return qqInfo; } throw new BusinessException("调qq接口获取用户信息异常"); }
作如下的配置,就可以在本地调试qq登录了,等调好了,再发布到公网
可以修改本地hosts文件(在:C:\Windows\System32\drivers\etc\目录下)
修改vue.config.js中devServer的配置,添加host为all(否则,访问时会出现Invalid Host Header
)
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
port: 80,
allowedHosts:'all', // 添加这个
}
})
<template> <span class="iconfont icon-qq" @click="jumpToQq"></span> </template> <script> ... jumpToQq() { getQQAuthUrl().then(({authUrl,state})=>{ location.href = authUrl // 记录state sessionStorage.setItem("state",state) // 记录跳转之前所在的页面 sessionStorage.setItem(state,JSON.stringify({fullPath:this.$route.fullPath})) }) } ... </script>
const routes = [
...
{
path:'/oauth/login/qq',
component: ()=> import(`@/views/oauth/QQLogin.vue`)
}
...
]
当qq授权页面通过时,qq会让浏览器重定向到redirectUri的页面,就是此页面,在此页面中,获取到地址栏路径上的参数,将此参数(授权码)传给后台,后台拿到这个授权码,就可以请求qq后台,获取用户的信息了,然后做登录即可
<template> <div class="qqCover"> qq登录中,请勿刷新页面... </div> </template> <script> import {getQQUserInfo} from '@/api/loginApi' export default { name: 'qqLogin', mounted() { console.log(this.$route,'当前路由'); getQQUserInfo({code:this.$route.query.code}).then(token=>{ // console.log('token',token); this.$store.dispatch('user/doQQLogin',{token}).then(res=>{ console.log('router--',res,this.$router); let state = sessionStorage.getItem('state') let fullPath = JSON.parse(sessionStorage.getItem(state)) // console.log('fullPath', fullPath); sessionStorage.removeItem('state') sessionStorage.removeItem(state) if(fullPath) { this.$router.push(fullPath.fullPath) } else { this.$router.push('/') } }) }) }, components: { } } </script> <style lang="scss"> .qqCover { position: fixed; width: 100%; height: 100%; background-color: #fff; z-index: 999; } </style>
import storage from '@/utils/storage' import axiosInstance from '@/utils/request' export default { namespaced: true, state: { token: localStorage.getItem('token') || '', userInfo: storage.get('userInfo') }, mutations: { SET_TOKEN(state, token) { state.token = token }, SET_USER_INFO(state, userInfo) { state.userInfo = userInfo } }, actions: { clearUserInfo({commit}){ commit('SET_TOKEN', '') commit('SET_USER_INFO', null) localStorage.removeItem('token') storage.removeKey('userInfo') console.log('clearUserInfo'); }, getUserInfo({ commit }) { // 获取用户信息 return new Promise(async (resolve, reject) => { let userInfo = await axiosInstance({ url: '/admin/user/getCurrUserInfo', method: 'post', }) commit('SET_USER_INFO', userInfo) storage.set('userInfo', userInfo) resolve() }) }, doLogin({ commit, dispatch }, payload) { return new Promise(async (resolve, reject) => { try { let formData = new FormData() const { username, password } = payload formData.append("username", username) formData.append("password", password) // 登录 let token = await axiosInstance({ url: '/login', method: 'Post', data: formData, }) commit('SET_TOKEN', token) localStorage.setItem('token', token) await dispatch('getUserInfo') resolve() } catch (error) { reject(error) } }) }, doQQLogin({commit,dispatch},payload){ return new Promise(async (resolve,reject) => { try { const {token} = payload commit('SET_TOKEN', token) localStorage.setItem('token', token) await dispatch('getUserInfo') resolve() } catch (error) { reject() } }) }, doLogout({commit}) { return new Promise(async (resolve, reject) => { await axiosInstance({ url: '/logout', method: 'Post', }) commit('SET_TOKEN', '') commit('SET_USER_INFO', null) localStorage.removeItem('token') localStorage.removeItem('userInfo') resolve() }) } }, getters: { } }
@Api(tags = "第三方登录") @RestController public class OauthLoginController { @Autowired private OauthService oauthService; @ApiOperation("获取qq授权url") @PostMapping("qq/getQQAuthUrl") public Result<Map<String,Object>> getQQAuthUrl() { return Result.ok(oauthService.getQQAuthUrl()); } @ApiOperation("获取qq用户信息") @GetMapping("qq/getQQUserInfo") public void getUserInfo(@RequestParam("code") String code) { oauthService.getQQUserInfo(code); } }
@Slf4j @Service public class OauthServiceImpl implements OauthService { @Autowired private RestTemplate restTemplate; @Autowired private QQProperties qqProperties; @Autowired private TokenSessionAuthenticationStrategy tokenSessionAuthenticationStrategy; @Autowired private IUserAuthService userAuthService; @Autowired private IUserInfoService userInfoService; @Autowired private IUserRoleService userRoleService; @Autowired private UserInfoMapper userInfoMapper; @Autowired private HttpServletRequest request; @Autowired @SuppressWarnings("all") private HttpServletResponse response; @Override public Map<String,Object> getQQAuthUrl() { // 生成的state给前端使用 String state = UUID.randomUUID().toString().replaceAll("-", ""); String authorizationUrl = UriComponentsBuilder.fromHttpUrl(QQProperties.AUTHORIZATION_URL) .queryParam("response_type", "code") .queryParam("client_id", qqProperties.getAppId()) .queryParam("redirect_uri", qqProperties.getRedirectUri()) .queryParam("state", state) .build().toUriString(); return MapBuilder.newHashMap() .put("state", state) .put("authUrl", authorizationUrl) .build(); } @Override @Transactional public void getQQUserInfo(String code) { // 获取访问令牌 String accessToken = getQQAccessToken(code); // 获取qq用户openid String openid = getQQOpenid(accessToken); UserAuthEntity existingUserAuthEntity = userAuthService.getOne(new QueryWrapper<UserAuthEntity>() .lambda() .eq(UserAuthEntity::getUsername, openid) .eq(UserAuthEntity::getLoginType, LoginTypeEnum.QQ_LOGIN.loginType()) ); if (existingUserAuthEntity == null) { // 获取qq用户信息 URI uri = UriComponentsBuilder.fromHttpUrl(QQProperties.GET_USER_INFO) .queryParam("access_token", accessToken) .queryParam("oauth_consumer_key", qqProperties.getAppId()) .queryParam("openid", openid) .build() .toUri(); String userInfoJson = restTemplate.getForObject(uri, String.class); log.info("userInfoJson -> {}", userInfoJson); Map<String,String> data = JsonUtil.json2Obj(userInfoJson, Map.class); UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setAvatar(data.get("figureurl_qq")); userInfoEntity.setNickname(data.get("nickname")); userInfoEntity.setDisabled(SysConstants.AVAILABLE); userInfoService.save(userInfoEntity); UserAuthEntity userAuthEntity = new UserAuthEntity(); userAuthEntity.setUsername(openid); userAuthEntity.setPassword(accessToken); userAuthEntity.setLoginType(LoginTypeEnum.QQ_LOGIN.loginType()); userAuthEntity.setUserInfoId(userInfoEntity.getId()); userAuthService.save(userAuthEntity); UserRoleEntity userRoleEntity = new UserRoleEntity(); userRoleEntity.setRoleId(SysConstants.DEFAULT_USER_ROLE_ID); userRoleEntity.setUserInfoId(userInfoEntity.getId()); userRoleService.save(userRoleEntity); existingUserAuthEntity = userAuthEntity; } // 用户信息 和 加载用户权限 UserDetailDTO userDetailDTO = new UserDetailDTO(); UserInfoEntity userInfoEntity = userInfoService.getOne(new QueryWrapper<UserInfoEntity>() .lambda() .eq(UserInfoEntity::getId, existingUserAuthEntity.getUserInfoId()) ); BeanUtil.copyBeanProps(existingUserAuthEntity, userDetailDTO); BeanUtil.copyBeanProps(userInfoEntity, userDetailDTO); userDetailDTO.setUserInfoId(userInfoEntity.getId()); userDetailDTO.setUserAuthId(existingUserAuthEntity.getId()); userDetailDTO.setLoginType(LoginTypeEnum.QQ_LOGIN.loginType()); userDetailDTO.setLoginTime(new Date()); parseLoginInfo(userDetailDTO); UserPermDTO userPermDTO = userInfoMapper.listRoleAndPermsForUser(userInfoEntity.getId()); userDetailDTO.setPerms(userPermDTO.getPerms().stream().filter(p->!StringUtils.isEmpty(p)).collect(Collectors.toList())); userDetailDTO.setRoles(userPermDTO.getRoles().stream().filter(r -> !StringUtils.isEmpty(r)).collect(Collectors.toList())); UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetailDTO, "N/A", userDetailDTO.getAuthorities()); tokenSessionAuthenticationStrategy.onAuthentication(usernamePasswordAuthenticationToken, request, response); } private void parseLoginInfo(UserDetailDTO userDetailDTO) { String ipAddress = IpUtil.getIpAddress(request); String ipSource = IpUtil.getIpSource(ipAddress); UserAgent userAgent = IpUtil.getUserAgent(request); OperatingSystem os = userAgent.getOperatingSystem(); userDetailDTO.setIpAddress(ipAddress); userDetailDTO.setIpSource(ipSource); userDetailDTO.setOsName(os.getName()); userDetailDTO.setBrowserName(userAgent.getBrowser().getName()); } private String getQQOpenid(String accessToken) { URI uri = UriComponentsBuilder.fromHttpUrl(QQProperties.GET_OPENID_URL) .queryParam("access_token", accessToken) .queryParam("fmt", "json") .build() .toUri(); String result = restTemplate.getForObject(uri, String.class); Map<String,Object> data = JsonUtil.json2Obj(result, Map.class); Object openid= data.get("openid"); if (openid == null) { throw BizException.QQ_LOGIN_ERR; } return String.valueOf(openid); } private String getQQAccessToken(String code) { URI uri = UriComponentsBuilder.fromHttpUrl(QQProperties.ACCESS_TOKEN_URL) .queryParam("grant_type", "authorization_code") .queryParam("client_id", qqProperties.getAppId()) .queryParam("client_secret", qqProperties.getAppKey()) .queryParam("code", code) .queryParam("redirect_uri", qqProperties.getRedirectUri()) .queryParam("fmt", "json") .build() .toUri(); String result = restTemplate.getForObject(uri, String.class); Map<String,Object> data = JsonUtil.json2Obj(result, Map.class); Object access_token = data.get("access_token"); if (access_token == null) { throw BizException.QQ_LOGIN_ERR; } return String.valueOf(access_token); } }
@Data @Component @ConfigurationProperties(prefix = "qq") public class QQProperties { // 跳转qq授权页面 // 需要携带 response_type=code client_id redirect_uri state public static final String AUTHORIZATION_URL = "https://graph.qq.com/oauth2.0/authorize"; // 获取访问令牌 // 需要携带: grant_type=grant_type client_id client_secret code fmt=json public static final String ACCESS_TOKEN_URL = "https://graph.qq.com/oauth2.0/token"; // 获取授权用户的openid // 需要携带: access_token fmt=json public static final String GET_OPENID_URL = "https://graph.qq.com/oauth2.0/me"; // 获取授权用户信息 // 需要携带: access_token oauth_consumer_key openid public static final String GET_USER_INFO = "https://graph.qq.com/user/get_user_info"; private String appId; private String appKey; private String redirectUri; }
qq:
appId: 填入自己在qq互联申请的appId
appKey: 填入自己在qq互联申请的appKey
redirectUri: http://www.pscool.fun/oauth/login/qq
有几个踩了,可以注意一下:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。