1、完善个人信息
用户在首次登录时需要完善个人信息,包括性别、昵称、生日、城市、头像等。
其中,头像数据需要做图片上传,这里采用阿里云的OSS服务作为我们的图片服务器,并且对头像要做人脸识别,非人脸照片不得上传。
1.1、图片上传
1.1.1、图片存储解决方案
实现图片上传服务,需要有存储的支持,那么我们的解决方案将以下几种:
- 直接将图片保存到服务的硬盘
- 优点:开发便捷,成本低
- 缺点:扩容困难
- 使用分布式文件系统进行存储
- 优点:容易实现扩容
- 缺点:开发复杂度稍大(有成熟的产品可以使用,比如:FastDFS)
- 使用nfs做存储
- 优点:开发较为便捷
- 缺点:需要有一定的运维知识进行部署和维护
- 使用第三方的存储服务
- 优点:开发简单,拥有强大功能,免维护
- 缺点:付费
选用阿里云的OSS服务进行图片存储。
1.1.2、阿里云OSS存储
流程:
1.1.2.1、什么是OSS服务?
地址:https://www.aliyun.com/product/oss
1.1.2.2、购买服务
使用第三方服务最大的缺点就是需要付费。
说明:OSS的上行流量是免费的,但是下行流量是需要购买的。
1.1.2.3、创建Bucket
使用OSS,首先需要创建Bucket,Bucket翻译成中文是水桶的意思,把存储的图片资源看做是水,想要盛水必须得有桶,就是这个意思了。
进入控制台,https://oss.console.aliyun.com/overview
选择Bucket后,即可看到对应的信息,如:url、消耗流量、文件管理、查看文件。
1.1.2.4、创建用户
创建用户,需要设置oss权限。
1.1.3、导入依赖
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>2.8.3</version>
</dependency>
1.1.4、OSS配置
aliyun.properties:
aliyun.endpoint = http://oss-cn-zhangjiakou.aliyuncs.com
aliyun.accessKeyId = ***********
aliyun.accessKeySecret = ***************
aliyun.bucketName= yile-dev
aliyun.urlPrefix=http://yile-dev.oss-cn-zhangjiakou.aliyuncs.com/
AliyunConfig:
package com.yile.sso.config;
import com.aliyun.oss.OSSClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@PropertySource("classpath:aliyun.properties")
@ConfigurationProperties(prefix = "aliyun")
@Data
public class AliyunConfig {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
private String urlPrefix;
@Bean
public OSSClient oSSClient() {
return new OSSClient(endpoint, accessKeyId, accessKeySecret);
}
}
1.1.5、PicUploadService
package com.yile.sso.service;
import com.aliyun.oss.OSSClient;
import com.yile.sso.config.AliyunConfig;
import com.yile.sso.vo.PicUploadResult;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
@Service
public class PicUploadService {
// 允许上传的格式
private static final String[] IMAGE_TYPE = new String[]{".bmp", ".jpg",".jpeg", ".gif", ".png"};
@Autowired
private OSSClient ossClient;
@Autowired
private AliyunConfig aliyunConfig;
public PicUploadResult upload(MultipartFile uploadFile) {
PicUploadResult fileUploadResult = new PicUploadResult();
//图片做校验,对后缀名
boolean isLegal = false;
for (String type : IMAGE_TYPE) {
if (StringUtils.endsWithIgnoreCase(uploadFile.getOriginalFilename(),
type)) {
isLegal = true;
break;
}
}
if (!isLegal) {
fileUploadResult.setStatus("error");
return fileUploadResult;
}
// 文件新路径
String fileName = uploadFile.getOriginalFilename();
String filePath = getFilePath(fileName);
// 上传到阿里云
try {
// 目录结构:images/2021/04/05/xxxx.jpg
ossClient.putObject(aliyunConfig.getBucketName(), filePath, new ByteArrayInputStream(uploadFile.getBytes()));
} catch (Exception e) {
e.printStackTrace();
//上传失败
fileUploadResult.setStatus("error");
return fileUploadResult;
}
// 上传成功
fileUploadResult.setStatus("done");
fileUploadResult.setName(this.aliyunConfig.getUrlPrefix() + filePath);
fileUploadResult.setUid(String.valueOf(System.currentTimeMillis()));
return fileUploadResult;
}
private String getFilePath(String sourceFileName) {
DateTime dateTime = new DateTime();
return "images/" + dateTime.toString("yyyy")
+ "/" + dateTime.toString("MM") + "/"
+ dateTime.toString("dd") + "/" + System.currentTimeMillis() +
RandomUtils.nextInt(100, 9999) + "." +
StringUtils.substringAfterLast(sourceFileName, ".");
}
}
所需其他的代码:
PicUploadResult:
package com.yile.sso.vo;
import lombok.Data;
@Data
public class PicUploadResult {
// 文件唯一标识
private String uid;
// 文件名
private String name;
// 状态有:uploading done error removed
private String status;
// 服务端响应内容,如:'{"status": "success"}'
private String response;
}
1.1.6、PicUploadController
package com.yile.sso.controller;
import com.yile.sso.service.PicUploadService;
import com.yile.sso.vo.PicUploadResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
@RequestMapping("pic/upload")
@Controller
public class PicUploadController {
@Autowired
private PicUploadService picUploadService;
@PostMapping
@ResponseBody
public PicUploadResult upload(@RequestParam("file") MultipartFile multipartFile) {
return this.picUploadService.upload(multipartFile);
}
}
1.1.7、测试
1.2、人脸识别
人脸识别技术采用虹软开放平台实现(免费使用)。官网:https://www.arcsoft.com.cn/
1.2.1、使用说明
使用虹软平台需要先注册开发者账号:https://ai.arcsoft.com.cn/ucenter/user/userlogin
注册完成后进行登录,然后进行创建应用:
创建完成后,需要进行实名认证,否则相关的SDK是不能使用的。
实名认证后即可下载对应平台的SDK,我们需要下载windows以及linux平台。
添加SDK(Linux与Windows平台):
下载SDK,打开解压包,可以看到有提供相应的jar包以及示例代码:
需要特别说明的是:每个账号的SDK包不通用,所以自己要下载自己的SDK包。
1.2.2、安装jar到本地仓库
进入到libs目录,需要将arcsoft-sdk-face-3.0.0.0.jar安装到本地仓库:
mvn install:install-file -DgroupId=com.arcsoft.face -DartifactId=arcsoft-sdk-face -Dversion=3.0.0.0 -Dpackaging=jar -Dfile=arcsoft-sdk-face-3.0.0.0.jar
安装成功后,即可通过maven坐标引用了:
<dependency>
<groupId>com.arcsoft.face</groupId>
<artifactId>arcsoft-sdk-face</artifactId>
<version>3.0.0.0</version>
<!--<scope>system</scope>-->
<!--如果没有安装到本地仓库,可以将jar包拷贝到工程的lib下面下,直接引用-->
<!--<systemPath>${project.basedir}/lib/arcsoft-sdk-face-3.0.0.0.jar</systemPath>-->
</dependency>
1.2.3、开始使用
说明:虹软的SDK是免费使用的,但是首次使用时需要联网激活,激活后可离线使用。使用周期为1年,1年后需要联网再次激活。
个人免费激活SDK总数量为100。
配置:application.properties
#虹软相关配置(在虹软应用中找到对应的参数)
arcsoft.appid=******************
arcsoft.sdkKey=*****************
arcsoft.libPath=F:\\code\\WIN64
FaceEngineService:
package com.yile.sso.service;
import com.arcsoft.face.EngineConfiguration;
import com.arcsoft.face.FaceEngine;
import com.arcsoft.face.FaceInfo;
import com.arcsoft.face.FunctionConfiguration;
import com.arcsoft.face.enums.DetectMode;
import com.arcsoft.face.enums.DetectOrient;
import com.arcsoft.face.enums.ErrorInfo;
import com.arcsoft.face.enums.ImageFormat;
import com.arcsoft.face.toolkit.ImageFactory;
import com.arcsoft.face.toolkit.ImageInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
@Service
public class FaceEngineService {
private static final Logger LOGGER = LoggerFactory.getLogger(FaceEngineService.class);
@Value("${arcsoft.appid}")
private String appid;
@Value("${arcsoft.sdkKey}")
private String sdkKey;
@Value("${arcsoft.libPath}")
private String libPath;
private FaceEngine faceEngine;
@PostConstruct
public void init() {
// 激活并且初始化引擎
FaceEngine faceEngine = new FaceEngine(libPath);
int activeCode = faceEngine.activeOnline(appid, sdkKey);
if (activeCode != ErrorInfo.MOK.getValue() && activeCode != ErrorInfo.MERR_ASF_ALREADY_ACTIVATED.getValue()) {
LOGGER.error("引擎激活失败");
throw new RuntimeException("引擎激活失败");
}
//引擎配置
EngineConfiguration engineConfiguration = new EngineConfiguration();
//IMAGE检测模式,用于处理单张的图像数据
engineConfiguration.setDetectMode(DetectMode.ASF_DETECT_MODE_IMAGE);
//人脸检测角度,全角度
engineConfiguration.setDetectFaceOrientPriority(DetectOrient.ASF_OP_ALL_OUT);
//功能配置
FunctionConfiguration functionConfiguration = new FunctionConfiguration();
functionConfiguration.setSupportAge(true);
functionConfiguration.setSupportFace3dAngle(true);
functionConfiguration.setSupportFaceDetect(true);
functionConfiguration.setSupportFaceRecognition(true);
functionConfiguration.setSupportGender(true);
functionConfiguration.setSupportLiveness(true);
functionConfiguration.setSupportIRLiveness(true);
engineConfiguration.setFunctionConfiguration(functionConfiguration);
//初始化引擎
int initCode = faceEngine.init(engineConfiguration);
if (initCode != ErrorInfo.MOK.getValue()) {
LOGGER.error("初始化引擎出错!");
throw new RuntimeException("初始化引擎出错!");
}
this.faceEngine = faceEngine;
}
/**
* 检测图片是否为人像
*
* @param imageInfo 图像对象
* @return true:人像,false:非人像
*/
public boolean checkIsPortrait(ImageInfo imageInfo) {
// 定义人脸列表
List<FaceInfo> faceInfoList = new ArrayList<FaceInfo>();
faceEngine.detectFaces(imageInfo.getImageData(), imageInfo.getWidth(), imageInfo.getHeight(), ImageFormat.CP_PAF_BGR24, faceInfoList);
return !faceInfoList.isEmpty();
}
public boolean checkIsPortrait(byte[] imageData) {
return this.checkIsPortrait(ImageFactory.getRGBData(imageData));
}
public boolean checkIsPortrait(File file) {
return this.checkIsPortrait(ImageFactory.getRGBData(file));
}
}
#问题:
Caused by: java.lang.UnsatisfiedLinkError: D:\gongju\renlian\haha\libs\WIN64\libarcsoft_face.dll: Can't find dependent libraries
解决:
安装:vcredist_x64.exe,即可解决。
1.2.4、测试
package com.yile.sso.service;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.io.File;
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class TestFaceEngineService {
@Autowired
private FaceEngineService faceEngineService;
@Test
public void testCheckIsPortrait(){
File file = new File("F:\\1.jpg");
boolean checkIsPortrait = this.faceEngineService.checkIsPortrait(file);
System.out.println(checkIsPortrait); // true|false
}
}
1.3、实现完善个人信息
完善个人信息的功能实现,分为2个接口完成,分别是:完善个人资料信息、头像上传。
mock接口:
1.3.1、UserInfoMapper
package com.yile.sso.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yile.sso.pojo.UserInfo;
public interface UserInfoMapper extends BaseMapper<UserInfo> {
}
1.3.2、UserInfoService
package com.yile.sso.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.yile.sso.enums.SexEnum;
import com.yile.sso.mapper.UserInfoMapper;
import com.yile.sso.pojo.User;
import com.yile.sso.pojo.UserInfo;
import com.yile.sso.vo.PicUploadResult;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.Map;
@Service
public class UserInfoService {
@Autowired
private UserService userService;
@Autowired
private UserInfoMapper userInfoMapper;
@Autowired
private FaceEngineService faceEngineService;
@Autowired
private PicUploadService picUploadService;
public Boolean saveUserInfo(Map<String, String> param, String token) {
//校验token
User user = this.userService.queryUserByToken(token);
if (null == user) {
return false;
}
UserInfo userInfo = new UserInfo();
userInfo.setUserId(user.getId());
userInfo.setSex(StringUtils.equalsIgnoreCase(param.get("gender"), "man") ? SexEnum.MAN : SexEnum.WOMAN);
userInfo.setNickName(param.get("nickname"));
userInfo.setBirthday(param.get("birthday"));
userInfo.setCity(param.get("city"));
return this.userInfoMapper.insert(userInfo) == 1;
}
public Boolean saveUserLogo(MultipartFile file, String token) {
//校验token
User user = this.userService.queryUserByToken(token);
if (null == user) {
return false;
}
try {
//校验图片是否是人像,如果不是人像就返回false
boolean b = this.faceEngineService.checkIsPortrait(file.getBytes());
if (!b) {
return false;
}
} catch (IOException e) {
e.printStackTrace();
}
//图片上传到阿里云OSS
PicUploadResult result = this.picUploadService.upload(file);
if (StringUtils.isEmpty(result.getName())) {
//上传失败
return false;
}
//把头像保存到用户信息表中
UserInfo userInfo = new UserInfo();
userInfo.setLogo(result.getName());
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", user.getId());
return this.userInfoMapper.update(userInfo, queryWrapper) == 1;
}
}
1.3.3、UserInfoController
package com.yile.sso.controller;
import com.yile.sso.service.UserInfoService;
import com.yile.sso.service.UserService;
import com.yile.sso.vo.ErrorResult;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("user")
public class UserInfoController {
@Autowired
private UserInfoService userInfoService;
/**
* 完善个人信息-基本信息
*
* @param param
* @return
*/
@PostMapping("loginReginfo")
public ResponseEntity<Object> saveUserInfo(@RequestBody Map<String, String> param,
@RequestHeader("Authorization") String token) {
try {
Boolean bool = this.userInfoService.saveUserInfo(param, token);
if (bool) {
return ResponseEntity.ok(null);
}
} catch (Exception e) {
e.printStackTrace();
}
ErrorResult errorResult = ErrorResult.builder().errCode("000001").errMessage("保存用户信息失败!").build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResult);
}
/**
* 完善个人信息-用户头像
*
* @return
*/
@PostMapping("loginReginfo/head")
public ResponseEntity<Object> saveUserLogo(@RequestParam("headPhoto") MultipartFile file,
@RequestHeader("Authorization") String token) {
try {
Boolean bool = this.userInfoService.saveUserLogo(file, token);
if (bool) {
return ResponseEntity.ok(null);
}
} catch (Exception e) {
e.printStackTrace();
}
ErrorResult errorResult = ErrorResult.builder().errCode("000001").errMessage("保存用户logo失败!").build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResult);
}
}
1.4.4、测试
图片上传超过1MB出错的解决方案:
#在application.properties文件中,填入下面的配置
#设置最大的文件上传大小
spring.servlet.multipart.max-request-size=30MB
spring.servlet.multipart.max-file-size=30MB
2、校验token
在整个系统架构中,只有SSO保存了JWT中的秘钥,所以只能通过SSO系统提供的接口服务进行对token的校验,所以在SSO系统中,需要对外开放接口,通过token进行查询用户信息,如果返回null说明用户状态已过期或者是非法的token,否则返回User对象数据。
2.1、UserController
/**
* 校验token,根据token查询用户数据
*
* @param token
* @return
*/
@GetMapping("{token}")
public User queryUserByToken(@PathVariable("token") String token) {
return this.userService.queryUserByToken(token);
}
2.2、UserService
public User queryUserByToken(String token) {
try {
// 通过token解析数据
Map<String, Object> body = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
User user = new User();
user.setId(Long.valueOf(body.get("id").toString()));
//需要返回user对象中的mobile,需要查询数据库获取到mobile数据
//如果每次都查询数据库,必然会导致性能问题,需要对用户的手机号进行缓存操作
//数据缓存时,需要设置过期时间,过期时间要与token的时间一致
//如果用户修改了手机号,需要同步修改redis中的数据
String redisKey = "yile_USER_MOBILE_" + user.getId();
if(this.redisTemplate.hasKey(redisKey)){
String mobile = this.redisTemplate.opsForValue().get(redisKey);
user.setMobile(mobile);
}else {
//查询数据库
User u = this.userMapper.selectById(user.getId());
user.setMobile(u.getMobile());
//将手机号写入到redis中
//在jwt中的过期时间的单位为:秒
long timeout = Long.valueOf(body.get("exp").toString()) * 1000 - System.currentTimeMillis();
this.redisTemplate.opsForValue().set(redisKey, u.getMobile(), timeout, TimeUnit.MILLISECONDS);
}
return user;
} catch (ExpiredJwtException e) {
log.info("token已经过期! token = " + token);
} catch (Exception e) {
log.error("token不合法! token = "+ token, e);
}
return null;
}