全网首例全栈实践(七)Spring Boot 用户登录功能

登录功能我们使用了Redis的缓存功能,以下为登录相关的目录结构。


全网首例全栈实践(七)Spring Boot 用户登录功能_第1张图片

其中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 indexdb = new ThreadLocal(){
        @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.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 LAST_INSERT_ID() as id
    
    update user set login_time = #{user.loginTime, jdbcType=TIMESTAMP} where phone = #{user.phone}

五、登录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;
    }
}

实现的思路如下:

  1. 首先校验参数是否为空,为空给出提示;

  2. 然后取出参数中的手机号码,在数据库中查找该号码是否存在,如果存在则比对用户密码是否一致,实际项目中一般的做法是密码参数进行md5等加密;

  3. 密码校验通过后,更新用户的登录时间;

  4. 生成token,并将用户的信息对象(包括token)保存到Redis中;

  5. 返回用户登录信息(包括token)。

其中

redisUtil.set(key, sessionUser, RedisConstants.datebase1, redisProperties.getTokenTimeout());

这里RedisConstants.datebase1,我们默认将token保存到Redis的库1中。保存token的目的是在后续项目开发过程中在需要校验用户登录状态的接口中,对用户身份进行校验,这也是商业项目通常的做法。

六、总结

用户登录功能的实现,主要涉及到用户身份的校验,以及登录会话的保持,安全性验证等细节,业界有相对成熟的标准,结合Redis等非关系型数据库,效率更高。本章涉及的代码,部分在前几章有讲解,后续我们会将所有代码开源。

你可能感兴趣的:(全网首例全栈实践(七)Spring Boot 用户登录功能)