全网首例全栈实践(七)Spring Boot 用户登录功能
登录功能我们使用了Redis的缓存功能,以下为登录相关的目录结构。
其中config目录下的RedisConfig为Redis的配置,其中
@ConfigurationProperties(prefix = "redis")
加载application-dev.yml配置文件中的Redis连接配置,如下:
#redis配置
redis:
#数据库索引(默认为0)
database: 0
#服务器地址
hostName: localhost
#端口
port: 6379
#密码(默认为空)
password: xxxx
#编码格式
encode: utf-8
#最大连接数
pool:
max-active: 100
max-wait: -1
timeout: 20000
#登录成功后的token对应的key
tokenKey: TOKEN
#token维持的时间(秒)
tokenTimeout: 600
utils->Redis目录下的RedisConstants为Redis的数据库配置,Redis默认有16个库,默认连接的是 index=0 的库,具体参看如下,可以分别定义不同的库:
public class RedisConstants {
/**
* redis库1 保存登录信息
*/
public static final Integer datebase1=1;
}
一、重写RedisTemplate
为了增加选库的功能,首先我们需要重写RedisTemplate,使其支持选库插入。
public class RedisTemplate extends org.springframework.data.redis.core.RedisTemplate {
public static ThreadLocal<Integer> indexdb = new ThreadLocal<Integer>(){
@Override protected Integer initialValue() { return 0; }
};
@Override
protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) {
try {
Integer dbIndex = indexdb.get();
//如果设置了dbIndex
if (dbIndex != null) {
if (connection instanceof JedisConnection) {
if (((JedisConnection) connection).getNativeConnection().getDB().intValue() != dbIndex) {
connection.select(dbIndex);
}
} else {
connection.select(dbIndex);
}
} else {
connection.select(0);
}
} finally {
indexdb.remove();
}
return super.preProcessConnection(connection, existingConnection);
}
}
二、创建RedisUtil工具类
@Lazy
@Component
public class RedisUtil{
@Autowired
private RedisTemplate redisTemplate;
public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key, int indexdb){
redisTemplate.indexdb.set(indexdb);
return key==null?null:redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key,Object value,int indexdb,long time){
try {
redisTemplate.indexdb.set(indexdb);
if(time>0){
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
}else{
redisTemplate.opsForValue().set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
三、创建RedisConfig配置类
@ConfigurationProperties(prefix = "redis")
@Configuration
public class RedisConfig {
@Autowired
RedisProperties redisProperties;
/**
* @Description: Jedis配置
*/
@Bean
public JedisConnectionFactory JedisConnectionFactory(){
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration ();
redisStandaloneConfiguration.setHostName(redisProperties.getHostName());
redisStandaloneConfiguration.setPort(redisProperties.getPort());
//由于我们使用了动态配置库,所以此处省略
//redisStandaloneConfiguration.setDatabase(database);
redisStandaloneConfiguration.setPassword(RedisPassword.of(redisProperties.getPassword()));
JedisClientConfiguration.JedisClientConfigurationBuilder jedisClientConfiguration = JedisClientConfiguration.builder();
jedisClientConfiguration.connectTimeout(Duration.ofMillis(redisProperties.getTimeout()));
JedisConnectionFactory factory = new JedisConnectionFactory(redisStandaloneConfiguration,
jedisClientConfiguration.build());
return factory;
}
/**
* @Description: 实例化 RedisTemplate 对象
*/
@Bean
public RedisTemplate functionDomainRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
LOGGER.info("RedisTemplate实例化成功!");
RedisTemplate redisTemplate = new RedisTemplate();
initDomainRedisTemplate(redisTemplate, redisConnectionFactory);
return redisTemplate;
}
/**
* @Description: 引入自定义序列化
*/
@Bean
public RedisSerializer fastJson2JsonRedisSerializer() {
return new FastJson2JsonRedisSerializer<Object>(Object.class);
}
/**
* @Description: 设置数据存入 redis 的序列化方式,并开启事务
*/
private void initDomainRedisTemplate(RedisTemplate redisTemplate, RedisConnectionFactory factory) {
//如果不配置Serializer,那么存储的时候缺省使用String,如果用User类型存储,那么会提示错误User can't cast to String!
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setValueSerializer(fastJson2JsonRedisSerializer());
// 开启事务
redisTemplate.setEnableTransactionSupport(true);
redisTemplate.setConnectionFactory(factory);
}
/**
* @Description: 注入封装RedisTemplate
*/
@Bean(name = "redisUtil")
public RedisUtil redisUtil(RedisTemplate redisTemplate) {
LOGGER.info("RedisUtil注入成功!");
RedisUtil redisUtil = new RedisUtil();
redisUtil.setRedisTemplate(redisTemplate);
return redisUtil;
}
}
到此为止,我们已经将Redis的工具类封装好,便于登录成功后使用。
四、用户Service的设计
首先,我们在UserMapper中新增用户查询和更新的dao,如下:
/**
* 根据手机号,查询用户
*
* @param phone 手机号
*/
User findByPhone(@Param("phone") String phone);
/**
* 根据手机号码,更新用户登录时间
*
* @param user 用户
*/
int updateUserLoginTime(@Param("user") User user);
其次我们在UserService中增加相应的服务,如下:
/**
* 根据手机号,查询用户
*
* @param phone 手机号
*/
User findByPhone(String phone);
/**
* 根据手机号码,更新用户登录时间
*
* @param user 用户
*/
int updateUserLoginTime(User user);
然后在UserServiceImpl中实现UserService:
@Override
public User findByPhone(String phone) {
return userMapper.findByPhone(phone);
}
@Override
public int updateUserLoginTime(User user) {
return userMapper.updateUserLoginTime(user);
}
最后,在UserMapper.xml中编写相应的sql:
<select id="findByPhone" resultMap="BaseResultMap" parameterType="java.lang.String">
select * from user where phone = #{phone}
</select>
<!-- 对应userMapper中的updateUserLoginTime方法, -->
<insert id="updateUserLoginTime" >
<!-- mysql插入数据后,获取id -->
<selectKey keyProperty="id" resultType="int" order="AFTER" >
SELECT LAST_INSERT_ID() as id
</selectKey>
update user set login_time = #{user.loginTime, jdbcType=TIMESTAMP} where phone = #{user.phone}
</insert>
五、登录Api的实现
@RestController
public class LoginController {
@Autowired
private UserService userService;
@Autowired
RedisUtil redisUtil;
@Autowired
RedisProperties redisProperties;
/*
* 登录接口,参数为json,请求参数: {"phone":1,"password":1}
*/
@RequestMapping(value = "/api/login", method = RequestMethod.POST)
public BaseEntity login(@RequestBody User user) {
BaseEntity result = new BaseEntity();
if (null == user) {
result.setFailMsg("2-00-001");
return result;
}
if (StringUtils.isEmpty(user.getPhone()) || StringUtils.isEmpty(user.getPassword())) {
result.setFailMsg("2-00-001");
return result;
}
//获取用户信息
User userInfo = userService.findByPhone(user.getPhone());
if (null == userInfo) {
result.setFailMsg("2-00-006");
return result;
}
//判断密码是否正确
if (!user.getPassword().equals(userInfo.getPassword())) {
result.setFailMsg("2-00-003");
return result;
}
//设置登录时间
userInfo.setLoginTime(new Date());
userService.updateUserLoginTime(userInfo);
//保存登录token,key的格式为TOKEN-XXX
String token = UUID.randomUUID().toString().replaceAll("-", "");
String key = redisProperties.getTokenKey() + "-" + token;
//根据需要保存token对应的用户信息的字段
User sessionUser = new User();
sessionUser.setPhone(userInfo.getPhone());
sessionUser.setLoginTime(userInfo.getLoginTime());
sessionUser.setName(userInfo.getName());
sessionUser.setToken(token);
// 插入缓存,默认token有效期为
redisUtil.set(key, sessionUser, RedisConstants.datebase1, redisProperties.getTokenTimeout());
//返回登录状态,包括token
sessionUser.setSuccessMsg("2-00-005");
return sessionUser;
}
}
实现的思路如下:
-
首先校验参数是否为空,为空给出提示;
-
然后取出参数中的手机号码,在数据库中查找该号码是否存在,如果存在则比对用户密码是否一致,实际项目中一般的做法是密码参数进行md5等加密;
-
密码校验通过后,更新用户的登录时间;
-
生成token,并将用户的信息对象(包括token)保存到Redis中;
-
返回用户登录信息(包括token)。
其中
redisUtil.set(key, sessionUser, RedisConstants.datebase1, redisProperties.getTokenTimeout());
这里RedisConstants.datebase1,我们默认将token保存到Redis的库1中。保存token的目的是在后续项目开发过程中在需要校验用户登录状态的接口中,对用户身份进行校验,这也是商业项目通常的做法。
六、总结
用户登录功能的实现,主要涉及到用户身份的校验,以及登录会话的保持,安全性验证等细节,业界有相对成熟的标准,结合Redis等非关系型数据库,效率更高。本章涉及的代码,部分在前几章有讲解,后续我们会将所有代码开源。