20. SpringDataJpa
用户微服务(单点登录)框架
20.1 搭建用户微服务项目(持久层使用springdatajpa)
项目技术:springmvc+springDataJpa+SpringDataRedis
SpringDataJpa用于单表操作
20.1.1 创建spring initializer项目
20.1.2 修改默认的pom配置
主要注意springboot的版本号与mysql的驱动版本
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.furong</groupId>
<artifactId>furong_user</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>furong_user</name>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>5.1.32</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
配置全局yml文件,自定义自动装配参数
server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/hospital_user?serverTimezone=Asia/Shanghai&useTimezone=true
username: root
password: 1234
jpa:
hibernate:
#定义jpa自动建表的方式,这里一般使用update的方式建表
ddl-auto: update
show-sql: true
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
redis:
host: 127.0.0.1
port: 6379
20.1.3 用户表的crud
使用jpa后,直接定义实体,运行时会自动根据实体建表
package com.furong.user.pojo.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
@Table(name = "furong_user") //定义表名
@Entity //jpa实体
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
@Id //标记主键
@GeneratedValue //主键自增
Long id;
String username;
String password;
String salt;
String phone;
String photo;
String sex;
String nickname;
//微信授权登录的字段
@Column(name = "open_id") //指定属性在数据库字段名
String openId;
//账号状态,1正常,0禁用
Integer status;
@Column(name = "create_time")
Date createTime;
@Column(name = "last_login_time")
Date lastLoginTime;
//上次登录的ip地址
String ip;
}
20.1.4 编写repository接口
package com.furong.repository;
import com.furong.user.pojo.entity.User;
import org.springframework.data.repository.CrudRepository;
//泛型参数1为表的实体类,参数2为实体类中主键属性的类型,继承CrudRepository会自动实现crud的实现代码
public interface UserRepository extends JpaRepository<User,Long> {}
20.1.5 运行测试
@SpringBootTest
class FurongUserApplicationTests {
@Autowired
UserRepository repository;
@Test
void contextCrud() {
//新增、修改
User user = User.builder().username("kunkun").build();
repository.save(user);
//查询
Optional<User> optional = repository.findById(1L);
User user1 = optional.get();
System.out.println(user1);
//查询所有
Iterable<User> users = repository.findAll();
//删除
repository.deleteById(1L);
}
}
测试成功,jpa已成功为我们建立表并插入了数据
20.1.6 就诊卡的crud
建立实体
package com.furong.user.pojo.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
@Table(name = "furong_user_info_card")
@Entity //jpa实体
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserInfoCard {
@Id //标记主键
@GeneratedValue //配置主键自动增长
private Long id;
//用户id
@Column(name = "user_id")
private Long userId;
//就诊卡编号
private String uuid;
//姓名
private String name;
//性别
private String sex;
//身份证号
private String pid;
//手机号码
private String phone;
//出生日期
private Date birthday;
//病史
@Column(name = "medical_history")
private String medicalHistory;
//保险类型
@Column(name = "insurance_type")
private String insuranceType;
//创建时间
@Column(name = "create_time")
private Date createTime;
}
编写对应的Repository类
package com.furong.repository;
import com.furong.user.pojo.entity.User;
import com.furong.user.pojo.entity.UserInfoCard;
import org.springframework.data.repository.CrudRepository;
//泛型参数1为表的实体类,参数2为实体类中主键属性的类型
public interface UserInfoCardRepository extends JpaRepository<UserInfoCard,Long> {
}
20.1.7 jpa的Repository接口
在jpa的环境中,只要一个接口继承了Repository接口,只要我们按照jpd规范在接口中定义方法,jpa就能直接生成crud的代码,比如我们在其中定义一个findxxxById,就能直接通过AutoWired注解取得jpa帮我们编写的接口实现类对象,我们就能直接调用其中的方法进行数据库操作。
jpa的Repository家族:
Repository
CrudRepository:实现了crud功能
PagingAndSortingRepository:增加了分页和排序
JpaRepository:增加了jpa规范,对父类方法进行包装,使其更符合使用逻辑
测试JpaRepository当中的分页和排序
@SpringBootTest
class FurongUserApplicationTests {
@Autowired
UserRepository repository; //这里的UserRepository实现了JpaRepository接口
@Test
void Page() { //测试分页
//创建分页条件,page为当前页数,从0开始
PageRequest pageRequest = PageRequest.of(0, 5);
//根据分页条件进行查询
Page<User> page = repository.findAll(pageRequest);
//获取分页查询的结果
List<User> content = page.getContent();
System.out.println(content);
//取得总记录数
long totalElements = page.getTotalElements();
System.out.println(totalElements);
}
@Test
void Sort(){ //测试分页
//构建排序条件
Sort sort = Sort.by(Sort.Direction.DESC,"id");
//根据排序条件进行查询
List<User> userList = repository.findAll(sort);
System.out.println(userList);
}
@Test
void pageAndSort() { //测试分页
//构建排序条件
Sort sort = Sort.by(Sort.Direction.DESC,"id");
//创建分页条件,page为当前页数,从0开始,直接将排序条件传入
PageRequest pageRequest = PageRequest.of(0, 5,sort);
//根据分页排序条件进行查询
Page<User> page = repository.findAll(pageRequest);
//获取分页排序查询的结果
List<User> content = page.getContent();
System.out.println(content);
//取得总记录数
long totalElements = page.getTotalElements();
System.out.println(totalElements);
}
}
20.1.8 jpa中Repository的命名表达式
在编写Repository类接口中的抽象方法时,我们只要按照jpa定义的命名表达式规则命名方法,jpa就能在底层自动帮我们生成sql
如我们想要根据用户名和密码查询用户
public interface UserRepository extends JpaRepository<User,Long> {
//根据用户名和密码查找用户
User findUserByUsernameAndPassword(String username,String password);
}
测试
@Test
void test(){
User kunkun = repository.findUserByUsernameAndPassword("kunkun", "123");
System.out.println(kunkun);
}
如我们想要根据昵称模糊查询,并且想要查询结果带分页和排序条件
public interface UserRepository extends JpaRepository<User,Long> {
Page<User> findUserByNicknameContainingOrderByIdDesc(Pageable pageable,String nickname);
}
测试
@Test
void test1(){
//创建分页条件,page为当前页数,从0开始,直接将排序条件传入
PageRequest pageRequest = PageRequest.of(0, 5);
Page<User> pages = repository.findUserByNicknameContainingOrderByIdDesc(pageRequest, "k");
System.out.println(pages.getTotalElements());
}
20.1.9 自定义jpa的sql
自定义sql或hql语句进行查询时,我们可以用于多表查询,只需要根据查询结果编写对应的Vo实体类来接收查询结果
20.1.9.1 在注解中自定义hql语句进行查询,方式1
public interface UserRepository extends JpaRepository<User,Long> {
//自定义sql,通过用户名密码查找用户
@Query("select u from User u where u.username=?1 and u.password=?2") //面向对象的查询语法,不论用什么数据库,我们只要在这里写hql,底层就生成对应的数据库的sql
//把表名换成类名、数据库字段换成属性名,其中参数为?时要在后面编号
User getUser1(@Param("name") String username, @Param("pwd")String password);
}
20.1.9.2 在注解中自定义hql语句进行查询,方式2
public interface UserRepository extends JpaRepository<User,Long> {
@Query("select u from User u where u.username=:name and u.password=:pwd") //面向对象的查询语法,不论用什么数据库,我们只要在这里写hql,底层就生成对应的数据库的sql
//Param注解参数和Query的hql语句冒号后的名字对应
User getUser2(@Param("name") String username, @Param("pwd")String password);
}
20.1.9.3 编写原生sql语句进行查询
public interface UserRepository extends JpaRepository<User,Long> {
//开启本地sql模式,可以写原生sql
@Query(nativeQuery = true,value = ("select count(*) from furong_user"))
Long getCount();
}
20.1.10 SpringDataRedis的使用
springboot中有官方的redis的自动装配jar包,利用这个jar包装配完成后的redis工具类可以更方便的对redis进行操作,这里将演示使用redis工具类的基本使用方法
整合,引入redis的自动装配
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
yml中配置redis连接参数
spring:
redis:
host: 127.0.0.1
port: 6379
编写redis的java配置类,更改其默认的二进制序列化方式
RedisConfig.java
@Configuration
public class RedisConfig {
//创建RedisTemplate对象,覆盖默认的,改变序列化机制为JSON
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//创建Json序列化
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer=new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper objectMapper=new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
template.setKeySerializer(jackson2JsonRedisSerializer);//设置key的序列化器
template.setValueSerializer(jackson2JsonRedisSerializer);//设置value的序列化器
template.afterPropertiesSet();//让配置生效
return template;
}
}
直接注入RedisTemplate对象使用其redis操作方法
@SpringBootTest
class TestRedis {
@Autowired
RedisTemplate redisTemplate;
@Test
void testString(){ //测试向redis中增加查询字符串类型
//设置string
redisTemplate.boundValueOps("aaa").set("kunkun");
//设置失效时间
redisTemplate.boundValueOps("aaa").expire(60, TimeUnit.SECONDS);
Object result = redisTemplate.boundValueOps("aaa").get();
System.out.println(result);
}
@Test
void testHash(){ //测试向redis中增加查询hash类型
redisTemplate.boundHashOps("www").put("aa","xxx");
redisTemplate.boundHashOps("www").put("vv","uuu");
//获取整个map
Map map = redisTemplate.boundHashOps("www").entries();
//根据key获取map中某个值
Object res = redisTemplate.boundHashOps("www").get("aa");
}
}
20.2 一键登录
用户可以通过手机验证码,或微信授权登录
整个框架
20.2.1 对接发送短信平台
这里使用阿里的短信平台,首先导入依赖
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.0.6</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>1.1.0</version>
</dependency>
拷贝发送短信的工具类
package com.furong.utils;// This file is auto-generated, don't edit it. Thanks.
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.furong.exception.CustomException;
public class SendSms {
//产品名称:云通信短信API产品,开发者无需替换
static final String product = "Dysmsapi";
//产品域名,开发者无需替换
static final String domain = "dysmsapi.aliyuncs.com";
// TODO 此处需要替换成开发者自己的AK(在阿里云访问控制台寻找)
static final String accessKeyId = "LTAI5tRVwKYdXCUZ85CXcfE2";
static final String accessKeySecret = "j0Q12Jrbs9jhBowXQcAK9XR2X2UQRs";
public static SendSmsResponse sendSms(String phone, String code) throws CustomException {
try {
//可自助调整超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
//初始化acsClient,暂不支持region化
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
IAcsClient acsClient = new DefaultAcsClient(profile);
//组装请求对象-具体描述见控制台-文档部分内容
SendSmsRequest request = new SendSmsRequest();
//必填:待发送手机号
request.setPhoneNumbers(phone);
//必填:短信签名-可在短信控制台中找到
request.setSignName("阿里云短信测试");
//必填:短信模板-可在短信控制台中找到
request.setTemplateCode("SMS_154950909");
//可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为
request.setTemplateParam("{\"code\":\"" + code + "\"}");
//选填-上行短信扩展码(无特殊需求用户请忽略此字段)
//request.setSmsUpExtendCode("90997");
//可选:outId为提供给业务方扩展字段,最终在短信回执消息中将此值带回给调用者
request.setOutId("yourOutId");
//hint 此处可能会抛出异常,注意catch
SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
return sendSmsResponse;
}catch (Exception e){
e.printStackTrace();
throw new CustomException("发送短信失败!");
}
}
}
注意上面的工具类中有部分参数要根据自己账号的信息进行配置
20.2.2 发送一键登录短信接口
导入测试登录接口的swagger和一些参数验证的工具类
<!--swagger的依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!--swagger-ui的jar包(里面包含了swagger的界面静态文件) -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.9</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.7.Final</version>
</dependency>
编写service接口
public interface SmsService {
/**
* 发送短信接口
* @param phone
* @return
*/
void sendOneLoginSms(String phone);
}
编写service接口实现类
@Service
@Slf4j
public class SmsServiceImpl implements SmsService {
@Autowired
RedisTemplate redisTemplate;
@Autowired
UserRepository userRepository;
//用于在redis中保存验证码时的key的前缀
public final static String ONE_LOGIN_SMS="one_login_sms:";
//用于存放在redis缓存中的验证码的失效时间
public final static Integer ONE_LOGIN_SMS_EXPIRE=300;
@Override
public void sendOneLoginSms(String phone) {
if(StringUtils.isEmpty(phone)){
log.error("手机号不能为空");
throw new CustomException(ResultEnum.PHONE_NOT_EMPTY);
}
//生成验证码
StringBuilder stringBuilder = new StringBuilder();
for(int i = 0;i < 6;i ++){
stringBuilder.append(new Random().nextInt(10));
}
String code = stringBuilder.toString();
//存储验证码存到redis
redisTemplate.boundValueOps(ONE_LOGIN_SMS+phone).set(code);
//设置失效时间
redisTemplate.boundValueOps(ONE_LOGIN_SMS+phone).expire(ONE_LOGIN_SMS_EXPIRE, TimeUnit.SECONDS);
//发送短信
//SendSmsResponse response = SendSms.sendSms(phone,code);
}
}
编写controller接口
@RestController
@RequestMapping("/app/sms")
@Api(tags = "短信接口")
public class SmsController {
@Autowired
SmsService smsService;
@GetMapping("/sendOneLoginSms")
public Result sendOneLoginSms(@RequestParam String phone){
if(!Validator.isMobile(phone)) //先验证当前输入的是否是手机格式
return ResultUtils.buildFailed(10000,"手机号不合法");
smsService.sendOneLoginSms(phone);
return ResultUtils.buildSuccess();
}
}
20.2.3 实现一键登录的接口
编写用户输入验证码进行登录时的登录dto
@ApiModel("一键登录dto")
@Data
public class LoginDto implements Serializable {
@NotBlank(message = "手机号不能为空")
String phone;
@NotBlank(message = "验证码不能为空")
String code;
}
在Repository接口中新增根据用户手机号查询用户信息的接口
User findUserByPhone(String phone);
编写一键登录的service接口
public interface UserService {
/**
* 一键登录
* @param loginDto 登录成功后返回给客户端的token
* @return
*/
String oneLogin(LoginDto loginDto);
}
编写service实现
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
UserRepository userRepository;
@Autowired
RedisTemplate redisTemplate;
@Autowired(required = false) //若spring容器中没有对象就不注入,request对象是用于获取当前登录时的ip地址的
HttpServletRequest request;
//整个token令牌由两部分组成,第一部分是这里定义的TOKEN_KEY,用于在服务器端进行保存,第二部分由下面的UUID生成,会返回给客户端保存
//将这两者组合后形成的完整token会作为key存放在redis中,对应的value中存放从数据库中查询出的用户信息
public static final String TOKEN_KEY = "asdasdqwezxcsaadasd";
//设置redis中存放的用户信息的失效时间
public static final Integer TOKEN_KEY_EXPIRE = 60*60*2;
@Override
public String oneLogin(LoginDto loginDto) {
//对比服务器验证码是否和用户输入的一致
String serverCode = (String)redisTemplate.boundValueOps(SmsServiceImpl.ONE_LOGIN_SMS + loginDto.getPhone()).get();
//验证码失效
if(StringUtils.isEmpty(serverCode)){
throw new CustomException(ResultEnum.CODE_EXPIRE);
}
//对比验证码
if(!loginDto.getCode().equals(serverCode)){
throw new CustomException(ResultEnum.CODE_EXPIRE);
}
//查找是否有账号
User userByPhone = userRepository.findUserByPhone(loginDto.getPhone());
//如果没有用户就生成
if(userByPhone == null){
userByPhone = User.builder().phone(loginDto.getPhone())
.nickname("用户" + loginDto.getPhone())
.status(1)
.photo("https://hospital-1317500932.cos.ap-chengdu.myqcloud.com/hospital/76151679990276584.png")
.createTime(new Date()).build();
userRepository.save(userByPhone);
}
//判断状态是否被禁用
if(userByPhone.getStatus() == 0){
log.info("限制登录");
throw new CustomException(ResultEnum.ACCOUNT_NO_PERMISSION);
}
//处理登录逻辑,生成token令牌的第二部分
String token = UUID.randomUUID().toString();
//存储用户信息到redis,对应的key为两个token的组合
redisTemplate.boundValueOps(TOKEN_KEY+token).set(userByPhone);
//设置失效时间
redisTemplate.boundValueOps(TOKEN_KEY+token).expire(TOKEN_KEY_EXPIRE, TimeUnit.SECONDS);
//记录本次登录时间和ip地址
userByPhone.setLastLoginTime(new Date());
userByPhone.setIp(request.getRemoteAddr());
//将本次登录时间和ip写入数据库中
userRepository.save(userByPhone);
//将token的第二部分返回给客户端
return token;
}
}
编写一键登录的controller接口
@RestController
@RequestMapping("/app/user")
@Api(tags = "用户接口")
public class UserController {
@Autowired
UserService userService;
@PostMapping ("/oneLogin")
public Result oneLogin(@RequestBody LoginDto loginDto, BindingResult bindingResult){
if(bindingResult.hasErrors()){ //若用户登录时传递的参数信息不符合LoginDto中通过注解定义的规范则报错
return WebUtils.getResult(bindingResult);
}
String token = userService.oneLogin(loginDto);
return ResultUtils.buildSuccess(token);
}
}
20.2.4 启动springboot入口方法,进行测试
首先测试发送短信接口,用户输入手机号,短信接口生成随机验证码,验证码会同时发送给用户并以用户的手机号为key存到redis缓存中
swagger页面的接口测试显示成功,看redis缓存中是否有对应的验证码
redis中对应的验证码信息存储成功,下面继续利用这个手机号的验证码尝试进行登录,测试一键登录接口
swagger页面接口测试显示成功,并返回了token的第二部分,下面看redis中是否以完整的token作为key存储的了用户的信息
可以看到redis中成功存储了数据库中查询到的数据,接口测试通过
20.2.5 编写UserDto用于在redis中存放查询到的数据
UserDto
@Data
public class UserDto implements Serializable {
Long id;
String username;
String phone;
String photo;
String sex;
String nickname;
String openId;
//账号状态,1正常,0禁用
Integer status;
Date createTime;
Date lastLoginTime;
//上次登录的ip地址
String ip;
}
UserServiceImpl中在查询到User实体后,拷贝到UserDto中,并将UserDto对象存入redis中
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private RedisTemplate redisTemplate;
@Autowired(required = false)//如果没有request就不注入,避免报错
private HttpServletRequest request;
public static final String TOKEN_KEY="fvcsdvdvsdvsdvsdvsdhdrfnjdfgjngfmhgmgmhg,hgsgvsegsg";
public static final Integer LOGIN_TIME_EXPIRE=60*60*2;
@Override
public String oneLogin(LoginDto loginDto) {
//1.对比服务器的验证码是否和用户输入的验证码是否一致
String serverCode = (String) redisTemplate.boundValueOps(SmsServiceImpl.ONE_LOGIN_SMS + loginDto.getPhone()).get();
//2.验证码失效
if(StrUtil.isEmpty(serverCode)){
log.info("验证码失效:{}",loginDto.getPhone());
throw new CustomerException(ResultEnum.USERNAME_NOT_EXITS);
}
//3.对比验证码
if(!loginDto.getCode().equals(serverCode)){
log.info("验证码错误:{}",loginDto.getCode());
throw new CustomerException(ResultEnum.USERNAME_NOT_EXITS);
}
//4.查找是否有账户
User user = userRepository.findUserByPhone(loginDto.getPhone());
//5.如果没有账户,匿名生成一个账户
if(user==null){
user = User.builder().phone(loginDto.getPhone()).nickname("用户:" + loginDto.getPhone()).
status(1).photo("https://n1.hdfimg.com/g4/M04/D7/19/wIYBAGEBgT-AHuQXAAQkRbBsz1A772_200_200_1.png?9439").createTime(new Date()).build();
userRepository.save(user);
}
//6.判断账号是否被禁用
if(user.getStatus()==0){
log.info("限制登录....");
throw new CustomerException(ResultEnum.USERNAME_NOT_EXITS);
}
//7.生成token
String token= UUID.randomUUID().toString();
UserDto userDto=new UserDto();
BeanUtils.copyProperties(user,userDto);
//8.存储用户信息到redis
redisTemplate.boundValueOps(TOKEN_KEY+token).set(userDto);
//9.设置失效时间
redisTemplate.boundValueOps(TOKEN_KEY+token).expire(LOGIN_TIME_EXPIRE, TimeUnit.SECONDS);
//TODO 删除验证码
//10.记录本次登录时间和ip地址
user.setLastLoginTime(new Date());
user.setIp(request.getRemoteAddr());
//修改信息
userRepository.save(user);
return token;
}
}
20.3 编写接口根据token在redis中查询用户信息
这个接口主要是提供给其他需要登录后才能使用的接口调用的
UserService
/**
* 根据token查询用户信息
* @param token
* @return
*/
UserDto queryUserByToken(String token);
UserServiceImpl
@Override
public UserDto queryUserByToken(String token) {
//去redis查询用户信息
UserDto userDto = (UserDto) redisTemplate.boundValueOps(TOKEN_KEY + token).get();
//判断redis中是否还存在
if(userDto == null){
log.info("登录失效:{}",token);
throw new CustomException(ResultEnum.TOKEN_ERROR);
}
//为用户的token续期
redisTemplate.boundValueOps(TOKEN_KEY+token).expire(TOKEN_KEY_EXPIRE, TimeUnit.SECONDS);
return userDto;
}
UserController
@PostMapping ("/queryUserByToken")
public Result oneLogin(@RequestParam String token){
UserDto userDto = userService.queryUserByToken(token);
return ResultUtils.buildSuccess(userDto);
}
20.4 解决用户重复登录问题
重复登录是指用户可能同时在同一种终端登录同一账号,这样就会导致redis中出现同一用户多条重复的缓存信息
解决方法:用户在第一次登录时,首先为用户匿名注册账户,然后为用户生成token令牌,token令牌也存入用户信息当中,并写入数据库,然后返回token令牌给客户端,之后用户再次进行登录时,先去数据库查询对应用户数据,并将用户前一次登录时使用的token取出,将这个token从redis中删除,然后再为用户生成本次登录的新token,将新token写入数据库,并以新token为key将用户信息存入redis中
修改User的实体类,增加3个字段分别记录pc、app、wx三种登录方式下最后一次登录的token
@Column(name = "pc_token")
String pcToken;
@Column(name = "app_token")
String appToken;
@Column(name = "wx_token")
String wxToken;
编写区分pc和app端的工具类
package com.furong.utils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CheckMobile {
// \b 是单词边界(连着的两个(字母字符 与 非字母字符) 之间的逻辑上的间隔),
// 字符串在编译时会被转码一次,所以是 "\\b"
// \B 是单词内部逻辑间隔(连着的两个字母字符之间的逻辑上的间隔)
static String phoneReg = "\\b(ip(hone|od)|android|opera m(ob|in)i"
+"|windows (phone|ce)|blackberry"
+"|s(ymbian|eries60|amsung)|p(laybook|alm|rofile/midp"
+"|laystation portable)|nokia|fennec|htc[-_]"
+"|mobile|up.browser|[1-4][0-9]{2}x[1-4][0-9]{2})\\b";
static String tableReg = "\\b(ipad|tablet|(Nexus 7)|up.browser"
+"|[1-4][0-9]{2}x[1-4][0-9]{2})\\b";
//移动设备正则匹配:手机端、平板
static Pattern phonePat = Pattern.compile(phoneReg, Pattern.CASE_INSENSITIVE);
static Pattern tablePat = Pattern.compile(tableReg, Pattern.CASE_INSENSITIVE);
/**
* 检测是否是移动设备访问
* @Title: check
* @param userAgent 浏览器标识
* @return true:移动设备接入,false:pc端接入
*/
public static boolean check(String userAgent){
if(null == userAgent){
userAgent = "";
}
// 匹配
Matcher matcherPhone = phonePat.matcher(userAgent);
Matcher matcherTable = tablePat.matcher(userAgent);
if(matcherPhone.find() || matcherTable.find()){
return true;
} else {
return false;
}
}
}
对UserServiceImpl中oneLogin进行改造
//传入user对象,根据user产生token
private TokenVo handlerLogin(User userByPhone) {
//判断状态是否被禁用
if(userByPhone.getStatus() == 0){
log.info("限制登录");
throw new CustomException(ResultEnum.ACCOUNT_NO_PERMISSION);
}
//获取user-agent头
String userAgent = request.getHeader("user-agent");
TokenVo tokenVo = new TokenVo();
//处理登录逻辑,生成token令牌的第二部分
String token = UUID.randomUUID().toString();
//将User实体属性拷贝到UserDto中
UserDto userDto = new UserDto();
BeanUtils.copyProperties(userByPhone,userDto);
//存储用户信息dto到redis,对应的key为两个token的组合
redisTemplate.boundValueOps(TOKEN_KEY+token).set(userDto);
//设置失效时间
redisTemplate.boundValueOps(TOKEN_KEY+token).expire(TOKEN_KEY_EXPIRE, TimeUnit.SECONDS);
tokenVo.setToken(token);
//记录本次登录时间和ip地址
userByPhone.setLastLoginTime(new Date());
userByPhone.setIp(request.getRemoteAddr());
//记录本次登录的token,在处理重复登录流程中会使用
if(CheckMobile.check(userAgent)){
//记录本次在移动端登录的token
log.info("本次在移动端登录的token{}",token);
userByPhone.setAppToken(token);
}else {
//记录本次在pc端登录的token
log.info("本次在pc端登录的token{}",token);
userByPhone.setPcToken(token);
}
//将本次登录时间、ip和登录token写入数据库中
userRepository.save(userByPhone);
return tokenVo;
}
@Override
public TokenVo oneLogin(LoginDto loginDto) {
//对比服务器验证码是否和用户输入的一致
String serverCode = (String)redisTemplate.boundValueOps(SmsServiceImpl.ONE_LOGIN_SMS + loginDto.getPhone()).get();
//验证码失效
if(StringUtils.isEmpty(serverCode)){
throw new CustomException(ResultEnum.CODE_EXPIRE);
}
//对比验证码
if(!loginDto.getCode().equals(serverCode)){
throw new CustomException(ResultEnum.CODE_EXPIRE);
}
//查找是否有账号
User userByPhone = userRepository.findUserByPhone(loginDto.getPhone());
//如果没有用户就生成
if(userByPhone == null){
userByPhone = User.builder().phone(loginDto.getPhone())
.nickname("用户" + loginDto.getPhone())
.status(1)
.photo("https://hospital-1317500932.cos.ap-chengdu.myqcloud.com/hospital/76151679990276584.png")
.createTime(new Date()).build();
userRepository.save(userByPhone);
}else{ //如果用户存在,则在redis中先删除上次以这种方式登录时使用的token
//获取user-agent头
String userAgent = request.getHeader("user-agent");
if(CheckMobile.check(userAgent)){
//当前是移动端,获取上次登录的token
String appToken = userByPhone.getAppToken();
log.info("删除移动端上次登录的token:{}",appToken);
redisTemplate.delete(TOKEN_KEY+appToken);
}else {
//当前是pc端,获取上次登录的token
String pcToken = userByPhone.getPcToken();
log.info("删除pc端上次登录的token:{}",pcToken);
redisTemplate.delete(TOKEN_KEY+pcToken);
}
}
//生成存入redis的token
TokenVo tokenVo = handlerLogin(userByPhone);
//将token的第二部分返回给客户端
return tokenVo;
}
20.5 实现app用户长时间登录
当用户使用app登录时,我们应该为其实现长时间登录,而不是两小时redis缓存到期后就让用户重登录一次
实现方法:在用户登录时,若发现用户是使用app,则除了为用户生成token外,再多生成一个刷新token,并将这个刷新token一起存入数据库表的用户信息中,然后将用户信息以token为key存入redis,将两个token都返回给客户端,当app用户在redis缓存数据失效后再请求时,先根据刷新token去数据库查询数据,查询到后生成新token,再以新token为key将用户信息写入redis中,实现长登录
改造User实体,添加刷新token及刷新token失效时间两个字段
@Column(name = "refresh_token")
String refreshToken;
@Column(name = "refresh_token_expire")
Date refreshTokenExpire;
编写登录vo,因为当用户以app登录时,要同时返回两个token
@Data
public class TokenVo implements Serializable {
String token;
String refreshToken;
}
改造UserServiceImpl中handlerLogin方法,其中在发现当前是使用app登录时,就再生成一个刷新token并存入数据库中
//传入user对象,根据user产生token
private TokenVo handlerLogin(User userByPhone) {
//判断状态是否被禁用
if(userByPhone.getStatus() == 0){
log.info("限制登录");
throw new CustomException(ResultEnum.ACCOUNT_NO_PERMISSION);
}
//获取user-agent头
String userAgent = request.getHeader("user-agent");
TokenVo tokenVo = new TokenVo();
//处理登录逻辑,生成token令牌的第二部分
String token = UUID.randomUUID().toString();
//将User实体属性拷贝到UserDto中
UserDto userDto = new UserDto();
BeanUtils.copyProperties(userByPhone,userDto);
//存储用户信息dto到redis,对应的key为两个token的组合
redisTemplate.boundValueOps(TOKEN_KEY+token).set(userDto);
//设置失效时间
redisTemplate.boundValueOps(TOKEN_KEY+token).expire(TOKEN_KEY_EXPIRE, TimeUnit.SECONDS);
tokenVo.setToken(token);
//记录本次登录时间和ip地址
userByPhone.setLastLoginTime(new Date());
userByPhone.setIp(request.getRemoteAddr());
//记录本次登录的token,在处理重复登录流程中会使用
if(CheckMobile.check(userAgent)){
//记录本次在移动端登录的token
log.info("本次在移动端登录的token{}",token);
userByPhone.setAppToken(token);
log.info("处理长时间登录token");
//生成刷新token
String refreshToken = UUID.randomUUID().toString();
//设置刷新token
tokenVo.setRefreshToken(refreshToken);
//保存刷新token到表中
userByPhone.setRefreshToken(refreshToken);
//设置刷新token的失效时间为3个月
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH,90);
userByPhone.setRefreshTokenExpire(calendar.getTime());
}else {
//记录本次在pc端登录的token
log.info("本次在pc端登录的token{}",token);
userByPhone.setPcToken(token);
}
//将本次登录时间、ip和登录token写入数据库中
userRepository.save(userByPhone);
return tokenVo;
}
改造UserService接口及其实现,增加以刷新token查询信息接口
/**
* 根据刷新token获取最新token
* @param refreshToken
* @return
*/
TokenVo loginByRefreshToken(String refreshToken);
@Override
public TokenVo loginByRefreshToken(String refreshToken) {
//根据刷新token获取用户信息
User user = userRepository.findUserByRefreshToken(refreshToken);
if(user==null){
log.error("无效刷新token");
throw new CustomException(ResultEnum.TOKEN_ERROR);
}
//在表中判断刷新token是否过期
if (new Date().getTime() > user.getRefreshTokenExpire().getTime()) {
log.error("刷新token已经过期");
throw new CustomException(ResultEnum.TOKEN_ERROR);
}
//处理登录,并生成新的刷新token续期
TokenVo tokenVo = handlerLogin(user);
return tokenVo;
}
编写controller接口
@PostMapping ("/loginByRefreshToken")
public Result loginByRefreshToken(@RequestParam String refreshToken){
TokenVo tokenVo = userService.loginByRefreshToken(refreshToken);
return ResultUtils.buildSuccess(tokenVo);
}
20.6 实现微信授权登录
首先前端需要请求wx.login得到临时验证凭证code,前端将凭证传递给后端,我们再利用code+appid+secretid请求微信验证接口,得到当前用户的openid和session_key,openid对应用户在微信开放平台的唯一标识,我们再利用得到的openid去数据库查询是否存在对应用户,若存在直接取得对应信息,并获取其上一次使用微信登录时使用的token,将其从redis中删除,避免重复登录问题,若不存在则为用户根据openid匿名生成一个新账户,为用户本次登录生成新token,将token存入数据库,再将用户信息以token为key存入redis中,最后将token、openid和session_key返回给微信小程序端保存
前端代码
loginOrRegister: function() {
let that = this;
//获取微信用户的临时授权code
uni.login({
provider: 'weixin',
success: function(resp) {
let code = resp.code;
that.code = code;
}
});
//调用接口,获取用户的微信资料用于注册,这里我们只存入code
uni.getUserProfile({
desc: '获取用户信息',
success: function(resp) {
let info = resp.userInfo;
let nickname = info.nickName; //昵称
let photo = info.avatarUrl; //头像URL
let sex = info.gender == 0 ? '男' : '女'; //性别
let data = {
code: that.code,
//nickname: nickname,
//photo: photo,
//sex: sex
};
//提交Ajax请求,将code传给后端,执行登陆或注册
that.ajax("http://127.0.0.1:8080/app/user/wx/login", 'POST', data, function(resp) {
let msg = resp.data.msg;
that.$refs.uToast.show({
message: msg,
type: 'success',
duration: 1200,
complete: function() {
//取得后端生成的token
let token = resp.data.data.token;
//把Token保存到Storage
uni.setStorageSync('token', token);
//更新页面标志位变量
that.flag = 'login';
that.user.username = nickname;
that.user.photo = photo;
}
});
});
}
});
}
编写dto接收前端的code凭证
@Data
public class WxLoginDto implements Serializable {
String code;
}
编写vo将后端登录后的token、openid、session_key返回
@Data
public class TokenVo implements Serializable {
String token;
String refreshToken;
}
编写微信登录service接口及实现
UserService
/**
* 微信小程序授权登录
* @return
*/
WxLoginVo loginByWx(WxLoginDto wxLoginDto);
UserServiceImpl
@Override
public WxLoginVo loginByWx(WxLoginDto wxLoginDto) {
String url = "https://api.weixin.qq.com/sns/jscode2session?appid="+APP_ID+"&secret="+APP_SECRET+
"&js_code="+wxLoginDto.getCode()+"&grant_type=authorization_code";
//后端携带code和appid和密钥去微信接口(https://api.weixin.qq.com/sns/jscode2session)获取openid和session——key
String jsonData = restTemplate.getForObject(url, String.class);
log.info("微信返回的数据{}",jsonData);
try{
//将返回的json数据转换成java对象
WxLoginVo wxLoginVo = mapper.readValue(jsonData, WxLoginVo.class);
if(wxLoginVo == null || wxLoginVo.getSession_key() == null){
log.error("微信授权失败");
throw new CustomException(ResultEnum.WX_AUTHORIZED_FAILED);
}
//下面处理后端自定义登录逻辑
//先根据openid在数据库中查询是否存在该用户
User user = userRepository.findUserByOpenId(wxLoginVo.getOpenid());
if(user==null){
//用户第一次授权登录,匿名注册,存入该用户的openid
user = User.builder().openId(wxLoginVo.getOpenid())
.nickname("微信用户")
.status(1)
.photo("https://hospital-1317500932.cos.ap-chengdu.myqcloud.com/hospital/76151679990276584.png")
.createTime(new Date()).build();
}else {
//数据库中有用户数据,则先去redis中删除用户上次使用微信登录时存入的
String wxToken = user.getWxToken();
log.info("删除微信小程序上次登录的token:{}",wxToken);
redisTemplate.delete(TOKEN_KEY+wxToken);
}
//随机生成token令牌
String token = UUID.randomUUID().toString();
//将token设置到vo中,会返回给前端
wxLoginVo.setToken(token);
//将User实体属性拷贝到UserDto中
UserDto userDto = new UserDto();
BeanUtils.copyProperties(user,userDto);
//存储用户信息dto到redis,对应的key为两个token的组合
redisTemplate.boundValueOps(TOKEN_KEY+token).set(userDto);
//设置失效时间
redisTemplate.boundValueOps(TOKEN_KEY+token).expire(TOKEN_KEY_EXPIRE, TimeUnit.SECONDS);
//记录本次登录时间和ip地址
user.setLastLoginTime(new Date());
user.setIp(request.getRemoteAddr());
user.setWxToken(token);
//将用户信息更新到数据库
userRepository.save(user);
return wxLoginVo;
}catch (Exception e){
log.error(e.getMessage());
throw new CustomException(ResultEnum.TOKEN_ERROR);
}
}
编写controller
UserController
@PostMapping ("/wx/login")
public Result loginByWx(@RequestBody WxLoginDto wxLoginDto){
WxLoginVo wxLoginVo = userService.loginByWx(wxLoginDto);
return ResultUtils.buildSuccess(wxLoginVo);
}