Evolution based on session login

1. Send mobile phone verification code:

Submit mobile phone number-check mobile phone number-generate verification code->save to session->send verification code

public Result sendCode(String phone, HttpSession session) {
        // 1.Verify phone number
        Boolean phoneInvalid = RegexUtils.isPhoneInvalid(phone);
        // 2.If not, return an error message
        if (!phoneInvalid) {
            return Result.fail("Mobile number is incorrect");
        }
        // 3.Meet, generate a verification code
        String code = RandomUtil.randomNumbers(6);
        // 4.Save verification code to session
        session.setAttribute("code",code);
        // 5.Send the verification code
        log.info("SMS verification code:{}",code);
        // return ok
        return Result.ok();
    }

2. SMS verification login

Submit the mobile phone number and verification code - verify the verification code - query users according to the mobile phone number - save the existence to the session
- There is no new user created, saved in the database - stored in the session

@Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1.Verify phone number
        String phone = loginForm.getPhone();
        // 2.If not, return an error message
        if(phone!=null&&phone.equals(session.getAttribute("phone"))){
            return Result.fail("cuowu");
        }
        // test code
        String code = (String) session.getAttribute("code");
        if (code==null || !code.equals(loginForm.getCode())) {
            return Result.fail("cuowu");
        }
        // 4.Consistent, query the user according to the mobile phone number select * from tb_user where phone = ?
        User user = query().eq("phone", phone).one();
        // 5.Determine whether the user exists
        if(user==null){
            // 6.does not exist, create a new user and save
          user = createUserWithPhone(phone);
        }
        //save in session   
        session.setAttribute("user",user);
        // 6.return
        return Result.ok();
    }

3. Verify login status

The request carries the cookie - get the user from the session - determine whether the user exists - save the user to Threadlocal

- No interception exists

Q: Why Threadlocal
Every user accessing our service is an independent thread, and each thread has its own independent storage space

Intercept login requires an interceptor

public class LoginInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

    public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        // Obtain session
//        HttpSession session = request.getSession();
//        //2, Obtain session of user
//        UserDTO user = (UserDTO) session.getAttribute("user");
        //Obtained from the packet header token
        String token = request.getHeader("authorization");
        //judge token is empty,
        if (StrUtil.isBlank(token)) {
            //4. does not exist, intercept
            response.setStatus(401);
            return false;
        }
        //from redis How to get users in stringRedisTemplate,how to get hash User
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
        //3. Determine whether the user exists
        if (userMap == null || userMap.isEmpty()) {
            //4. does not exist, intercept
            response.setStatus(401);
            return false;
        }
        //Will Hashmap transform into dto
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap,new UserDTO(),false);
        //5. exist in ThreadLocal middle
        UserHolder.saveUser(userDTO);
        //Refresh validity period
        stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL,TimeUnit.MINUTES);
        //6.let go
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

The newly added interceptor has been written, but it has not yet taken effect. It is necessary to configure the interceptor and let go of some requests that do not need to be intercepted.

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    StringRedisTemplate stringRedisTemplate;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/shop/**",
                        "/blog/hot",
                        "/upload/**",
                        "/shop-type/**",
                        "/voucher/**"
                );
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**");
    }
}

3. Desensitization of user information

Because the user information user exists in the session and all are returned to the front end, which exposes the customer's private information, and the existence of the session also causes storage pressure. Define UserDTO and only save the required information.

4. When the service uses a cluster, there is a session sharing problem. The sessions are stored in tomcat, and the sessions of multiple tomcats do not coexist. At this time, we need to change an alternative method, and the alternative needs the following characteristics:

Sharing, high performance (the frequency of use is high, so the reading and writing speed should be fast), high availability, key-value structure ====== "redis

Stored in public space, it is necessary to ensure that each user has a unique key, and the client needs to be able to carry it. Generally, a random string token is used as a login credential

Saving objects to redis also needs to consider what data structure to use. The String type is to convert objects into json strings for storage, which is a bit intuitive. The disadvantage is that all updates need to be updated, and the memory usage is larger than hash

hash can store each field of an object separately, can CRUD a single attribute, and occupies a small amount of memory

  @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1.Verify phone number
        String phone = loginForm.getPhone();
        // 2.If not, return an error message
        if(!RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("cuowu");
        }
        // test code
//        String code = (String) session.getAttribute("code");
        String code = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        if (code==null || !code.equals(loginForm.getCode())) {
            return Result.fail("cuowu");
        }
        // 4.Consistent, query the user according to the mobile phone number select * from tb_user where phone = ?
        User user = query().eq("phone", phone).one();
        // 5.Determine whether the user exists
        if(user==null){
            // 6.does not exist, create a new user and save
          user = createUserWithPhone(phone);
        }
//        //save in session
//        session.setAttribute("user",BeanUtil.copyProperties(user,UserDTO.class));
        //save in redis middle
        //generate random token
        String token = UUID.randomUUID().toString(true);
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),CopyOptions.create()
                .setIgnoreNullValue(true).setFieldValueEditor((filedName,fileValue)-> fileValue.toString()));
        String tokenKey =  LOGIN_USER_KEY+token;
        stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
        //set expiration date
        stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
        // 6.return
        return Result.ok(token);
    }

Thinking: When the login token has an expiration time set, how to refresh it? Any operation by the user should refresh the token. Add a layer of interceptor to intercept all requests and refresh token

public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.Get the request header token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            return true;
        }
        // 2.based on TOKEN Obtain redis users in
        String key  = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3.Determine whether the user exists
        if (userMap.isEmpty()) {
            return true;
        }
        // 5.will be queried hash data into UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.Exist, save user information to ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.to refresh token validity period
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8.let go
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // remove user
        UserHolder.removeUser();
    }
}

Summarize:

When using redis, you need to use an appropriate data structure.

The redis data needs to set an expiration time, otherwise the infinite increase will occupy the memory.

The SMS login function mainly uses several data structures in redis, among which the String type is used to store the verification code generated by the server, and the Hash is used to store the basic information of the user. What is stored here does not need to be the complete information of the user. You can declare a class with only the most important information of the user in it. It is also necessary to set the expiration time of these key-value pairs to prevent the redis memory from being full. It is also necessary to intercept the user's request through an interceptor in order to update the valid time of the user's information and prevent the user's information from expiring during the operation.

 

Tags: Redis

Posted by ferpadro on Sat, 26 Nov 2022 17:52:21 +0300