全网首例全栈实践(七)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;
    }
}

实现的思路如下:

  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等非关系型数据库,效率更高。本章涉及的代码,部分在前几章有讲解,后续我们会将所有代码开源。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容