用户微服务(单点登录)

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);
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
若依是一个微服务框架,可以实现单点登录功能。单点登录(SSO)是指多个站点共用一台认证授权服务器,用户在其中一个站点登录后,可以免登录访问其他所有站点,并且各站点间可以通过该登录状态直接交互。若依通过部署auth、gateway和system模块,并将文件上传到服务器的/aService目录来实现单点登录功能。同时,还需要使用配置文件/etc/docker/daemon.json来配置镜像加速器,以提高镜像下载速度。可以在该文件中添加如下内容来配置镜像加速器:{ "registry-mirrors": \["https://mr63yffu.mirror.aliyuncs.com"\] }。这样就可以实现若依微服务单点登录功能了。 #### 引用[.reference_title] - *1* [微服务单点登录实现](https://blog.csdn.net/qq_61393507/article/details/121869165)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [【手把手教程】若依微服务版服务器部署](https://blog.csdn.net/IUTStar/article/details/127671293)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值