一、简介
本篇博客实现登陆接口,登陆成功会返回一个token,每种登陆方式只能拥有一个有效token,即唯一登陆。
流程如下:
1、校验账号密码是否正确
2、查询数据库中用户同种登陆方式的有效token,若存在则设置为失效,并从redis中移除。其中需要开启redis和db事务
3、生成token,存入数据库和redis中
4、返回token
二、实战
2.1创建用户令牌表
CREATE TABLE `u_user_token` (
`user_token_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`token` varchar(255) DEFAULT NULL COMMENT 'token',
`login_type` TINYINT (1) DEFAULT NULL COMMENT '登陆方式 1=pc,2=android,3=ios',
`device_infor` varchar(255) DEFAULT NULL COMMENT '设备信息',
`status` TINYINT (1) DEFAULT NULL COMMENT 'token状态 0=失效 1=有效',
`user_id` int(11) DEFAULT NULL COMMENT '用户id',
`create_time` TIMESTAMP NULL DEFAULT NULL COMMENT '创建时间',
`update_time` TIMESTAMP NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`user_token_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2
2.2创建DO和DAO
package com.liazhan.member.dao.entity;
import lombok.Data;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import java.util.Date;
/**
* @version:V1.0
* @Description: 用户令牌表实体类
* @author: Liazhan
* @date 2020/4/29 9:25
*/
@Data
@Entity(name = "u_user_token")
@DynamicInsert
@DynamicUpdate
@EntityListeners(AuditingEntityListener.class)
public class UserTokenDO {
/*
* 主键
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer userTokenId;
/*
* 令牌
*/
private String token;
/*
* 登陆方式 1=pc,2=android,3=ios
*/
private Integer loginType;
/*
* 设备信息
*/
private String deviceInfor;
/*
* token状态 0=失效 1=有效
*/
private Integer status;
/*
* 用户id
*/
private Integer userId;
/**
* 创建时间
*/
@CreatedDate
private Date createTime;
/**
* 修改时间
*/
@LastModifiedDate
private Date updateTime;
}
package com.liazhan.member.dao;
import com.liazhan.member.dao.entity.UserTokenDO;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* @version:V1.0
* @Description: 用户令牌dao层
* @author: Liazhan
* @date 2020/4/29 9:43
*/
public interface UserTokenDao extends JpaRepository<UserTokenDO,Integer> {
/*
* 根据用户id、登陆类型、token状态获取用户token记录
*/
UserTokenDO findByUserIdAndLoginTypeAndStatus(Integer userId,Integer loginType,Integer status);
}
2.3 修改redis工具类,添加事务功能
package com.liazhan.core.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @version:V1.0
* @Description: Redis工具类
* @author: Liazhan
* @date 2020/4/22 15:56
*/
@Component
public class RedisUtil {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 存放String类型,有过期时间
* @param key
* @param data
* @param timeout 过期时间,单位为秒
*/
public void setString(String key,String data,Long timeout){
stringRedisTemplate.opsForValue().set(key,data);
if(timeout!=null){
stringRedisTemplate.expire(key,timeout,TimeUnit.SECONDS);
}
}
/**
* 存放String类型
* @param key
* @param data
*/
public void setString(String key,String data){
stringRedisTemplate.opsForValue().set(key,data);
}
/**
* 根据key获取String类型数据
* @param key
* @return String
*/
public String getString(String key){
return stringRedisTemplate.opsForValue().get(key);
}
/**
* 根据key删除
* @param key
* @return Boolean
*/
public Boolean delKey(String key){
return stringRedisTemplate.delete(key);
}
/**
* 开启redis事务
*/
public void begin(){
//开启redis事务权限
stringRedisTemplate.setEnableTransactionSupport(true);
//开启事务
stringRedisTemplate.multi();
}
/**
* 提交事务
*/
public void exec(){
stringRedisTemplate.exec();
}
/**
* 回滚redis事务
*/
public void discard(){
stringRedisTemplate.discard();
}
}
2.4 创建redis和db的事务工具类
package com.liazhan.core.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
/**
* @version:V1.0
* @Description: redis与数据库事务工具类
* @author: Liazhan
* @date 2020/4/29 11:09
*/
@Component
//多例
@Scope(ConfigurableListableBeanFactory.SCOPE_PROTOTYPE)
public class RedisAndDBTransactionUtil {
@Autowired
private RedisUtil redisUtil;
@Autowired
private PlatformTransactionManager transactionManager;
/**
* 开启事务 采用默认传播行为
* @return
*/
public TransactionStatus begin(){
//手动开启数据库事务
TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionAttribute());
redisUtil.begin();
return transaction;
}
/**
* 提交事务
* @param transactionStatus
* @throws Exception
*/
public void commit(TransactionStatus transactionStatus) throws Exception {
if(transactionStatus==null){
throw new Exception("transactionStatus is null");
}
//redis与数据库事务会同时提交
transactionManager.commit(transactionStatus);
}
/**
* 回滚事务
* @param transactionStatus
* @throws Exception
*/
public void rollback(TransactionStatus transactionStatus) throws Exception {
if(transactionStatus==null){
throw new Exception("transactionStatus is null");
}
//redis与数据库事务会同时回滚
transactionManager.rollback(transactionStatus);
}
}
2.5 创建token工具类
package com.liazhan.core.utils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.UUID;
/**
* @version:V1.0
* @Description: token工具类
* @author: Liazhan
* @date 2020/4/28 10:08
*/
@Component
public class TokenUtil {
@Autowired
private RedisUtil redisUtil;
/**
* 生成令牌
*
* @param prefix
* 令牌key前缀
* @param redisValue
* redis存放的值
* @return 返回token
*/
public String createToken(String keyPrefix, String redisValue) {
return createToken(keyPrefix, redisValue, null);
}
/**
* 生成令牌
*
* @param prefix
* 令牌key前缀
* @param redisValue
* redis存放的值
* @param time
* 有效期
* @return 返回token
*/
public String createToken(String keyPrefix, String redisValue, Long time) {
if (StringUtils.isEmpty(redisValue)) {
new Exception("redisValue Not null");
}
String token = keyPrefix + UUID.randomUUID().toString().replace("-", "");
redisUtil.setString(token, redisValue, time);
return token;
}
/**
* 根据token获取redis中的value值
*
* @param token
* @return
*/
public String getToken(String token) {
if (StringUtils.isEmpty(token)) {
return null;
}
String value = redisUtil.getString(token);
return value;
}
/**
* 移除token
*
* @param token
* @return
*/
public Boolean removeToken(String token) {
if (StringUtils.isEmpty(token)) {
return null;
}
return redisUtil.delKey(token);
}
}
2.6 会员服务创建常量类
package com.liazhan.member.consts;
/**
* @version:V1.0
* @Description: 会员服务常量类
* @author: Liazhan
* @date 2020/4/28 10:11
*/
public interface MemberConst {
//登录token在redis的key前缀
String MEMBER_LOGIN_TOKEN_PREFIX = "login.token";
//登录token过期时间 1小时
Long MEMBER_LOGIN_TOKEN_TIMEOUT = 3600L;
//登陆token失效状态
Integer MEMBER_LOGIN_TOKEN_INVALID = 0;
//登陆token有效状态
Integer MEMBER_LOGIN_TOKEN_VALID = 1;
}
2.7 github上的会员服务配置文件member-dev.yml添加redis和登陆类型相关配置
#服务端口号
server:
port: 8300
spring:
application:
name: liazhan-member
datasource:
druid:
# 数据库访问配置, 使用druid数据源
db-type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://47.98.183.103:3306/shop-member?serverTimezone=GMT%2b8&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 123456
# 连接池配置
initial-size: 5
min-idle: 5
max-active: 20
# 连接等待超时时间
max-wait: 30000
# 配置检测可以关闭的空闲连接间隔时间
time-between-eviction-runs-millis: 60000
##Jpa配置
jpa:
hibernate:
ddl-auto: update
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
show-sql: true
#redis配置
redis:
host: 47.98.183.103
password: 123456
port: 6379
pool:
max-idle: 100
min-idle: 1
max-active: 1000
max-wait: -1
####swagger相关配置
swagger:
base-package: com.liazhan.member.service
title: 微服务电商项目-会员服务接口
description: 会员服务
version: 1.1
terms-of-service-url: www.baidu.com
contact:
name: liazhan
email: 33421352+liazhan@users.noreply.github.com
####会员登陆类型相关配置
login:
type:
max: 3
value: pc,android,ios
配置文件github地址https://github.com/liazhan/shop-project-config/tree/b96d7e2a4b603df4f186d14394ec3fd23aae522b
2.8 创建登陆接口的输入dto类
package com.liazhan.member.input.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
/**
* @version:V1.0
* @Description:
* @author: Liazhan
* @date 2020/4/27 15:37
*/
@Data
@ApiModel(value = "用户登录输入实体类")
public class UserLoginInpDTO {
@NotBlank(message = "请输入手机号!")
@Pattern(regexp = "^(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\\d{8}$",message = "手机号格式错误!")
@ApiModelProperty(value = "手机号码")
private String phone;
@NotBlank(message = "请输入密码!")
@ApiModelProperty(value = "密码")
private String password;
@NotNull(message = "登陆类型为空!")
@ApiModelProperty(value = "登录类型 1=pc,2=android,3=ios")
private Integer loginType;
@NotBlank(message = "请输入设备信息!")
@ApiModelProperty(value = "设备信息")
private String deviceInfor;
}
2.9 UserDao添加根据手机号和密码查询用户的方法
package com.liazhan.member.dao;
import com.liazhan.member.dao.entity.UserDO;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* @version:V1.0
* @Description: 用户dao层
* @author: Liazhan
* @date 2020/4/21 10:58
*/
public interface UserDao extends JpaRepository<UserDO,Integer> {
/**
* 根据手机号查询用户是否存在
*/
boolean existsByPhone(String phone);
/*
* 根据手机号和密码查询用户
*/
UserDO findByPhoneAndPassword(String phone,String password);
}
2.10 创建登陆相关接口
package com.liazhan.member.service;
import com.alibaba.fastjson.JSONObject;
import com.liazhan.base.BaseResponse;
import com.liazhan.member.input.dto.UserLoginInpDTO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import javax.validation.Valid;
/**
* @version:V1.0
* @Description: 会员登录相关接口
* @author: Liazhan
* @date 2020/4/27 16:33
*/
@Api(tags = "会员登录相关接口")
public interface MemberLoginService {
@PostMapping("/login")
@ApiOperation(value = "登录接口")
BaseResponse<JSONObject> login(@RequestBody @Valid UserLoginInpDTO userLoginInpDTO) throws Exception;
}
package com.liazhan.member.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.liazhan.base.BaseResponse;
import com.liazhan.base.BaseServiceImpl;
import com.liazhan.core.utils.DtoUtil;
import com.liazhan.core.utils.RedisAndDBTransactionUtil;
import com.liazhan.core.utils.TokenUtil;
import com.liazhan.member.consts.MemberConst;
import com.liazhan.member.dao.UserDao;
import com.liazhan.member.dao.UserTokenDao;
import com.liazhan.member.dao.entity.UserDO;
import com.liazhan.member.dao.entity.UserTokenDO;
import com.liazhan.member.input.dto.UserLoginInpDTO;
import com.liazhan.member.service.MemberLoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
/**
* @version:V1.0
* @Description: 会员登录相关接口实现类
* @author: Liazhan
* @date 2020/4/28 9:24
*/
@RestController
@RefreshScope
public class MemberLoginServiceImpl extends BaseServiceImpl<JSONObject> implements MemberLoginService {
@Value("${login.type.max}")
private Integer loginTypeMax;
@Value("${login.type.value}")
private String loginTypeValue;
@Autowired
private UserDao userDao;
@Autowired
private UserTokenDao userTokenDao;
@Autowired
private TokenUtil tokenUtil;
@Autowired
private RedisAndDBTransactionUtil redisAndDBTransactionUtil;
@Transactional
@Override
public BaseResponse<JSONObject> login(@Valid UserLoginInpDTO userLoginInpDTO) throws Exception {
//1.校验登陆类型
Integer loginType = userLoginInpDTO.getLoginType();
if(loginType<1 || loginType>loginTypeMax){
return getResultError("登陆类型错误!");
}
//2.校验账号密码是否正确
String oldPassword = userLoginInpDTO.getPassword();
String newPassword = DigestUtils.md5DigestAsHex(oldPassword.getBytes());
UserDO userDo = userDao.findByPhoneAndPassword(userLoginInpDTO.getPhone(), newPassword);
if(userDo==null){
return getResultError("账号或密码错误!");
}
/*
* 3.将同类型的token失效
*/
//查询同类型的有效token记录
UserTokenDO oldUserTokenDo = userTokenDao.findByUserIdAndLoginTypeAndStatus(userDo.getUserId(),
userLoginInpDTO.getLoginType(), MemberConst.MEMBER_LOGIN_TOKEN_VALID);
//开启事务 防止出现异常时redis和db数据不一致
TransactionStatus transactionStatus = redisAndDBTransactionUtil.begin();
try {
if(oldUserTokenDo!=null) {
//移除redis的旧token
tokenUtil.removeToken(oldUserTokenDo.getToken());
//将数据库的token失效
oldUserTokenDo.setStatus(MemberConst.MEMBER_LOGIN_TOKEN_INVALID);
userTokenDao.save(oldUserTokenDo);
}
//4.生成token
String[] loginTypeArray = loginTypeValue.split(",");
String loginTypeStr = loginTypeArray[loginType-1];
String key = loginTypeStr+"."+MemberConst.MEMBER_LOGIN_TOKEN_PREFIX;
String token = tokenUtil.createToken(
key, userDo.getUserId() + "", MemberConst.MEMBER_LOGIN_TOKEN_TIMEOUT);
//5.dto转do
UserTokenDO userTokenDO = DtoUtil.dtoToDo(userLoginInpDTO, UserTokenDO.class);
userTokenDO.setToken(token);
userTokenDO.setStatus(MemberConst.MEMBER_LOGIN_TOKEN_VALID);
userTokenDO.setUserId(userDo.getUserId());
//6.保存token记录
userTokenDao.save(userTokenDO);
redisAndDBTransactionUtil.commit(transactionStatus);
//7.返回数据
JSONObject jsonObject = new JSONObject();
jsonObject.put("token",token);
return getResultSuccess(jsonObject);
}catch (Exception e){
redisAndDBTransactionUtil.rollback(transactionStatus);
}
return getResultError("登陆失败!");
}
}
ok,如此便大工告成。
依次启动config、eureka、member服务。
访问http://localhost:8300/swagger-ui.html 进行测试
github项目地址https://github.com/liazhan/shop-project/tree/58aeb838b392792f0df5a36b6b07277a75affa7a
版本号为58aeb838b392792f0df5a36b6b07277a75affa7a