1. Login based on session
Send verification code:
- The user submits the mobile phone number;
- Check whether the mobile phone number is legal:
- If it is not legal, the user is required to re-enter the mobile phone number;
- If the mobile phone number is legal, the background will generate the corresponding verification code at this time, save the verification code at the same time, and then send the verification code to the user via SMS
SMS verification code login and registration:
- The user enters the verification code and mobile phone number;
- The background gets the current verification code from the session, and then checks it with the verification code entered by the user:
- If they are inconsistent, the check cannot be passed;
- If they are consistent, the background will query the user according to the mobile phone number. If the user does not exist, the account information will be created for the user and saved to the database; no matter whether it exists or not, the user information will be saved in the session, so that the current login information can be obtained later
Verify login status:
- When the user requests, the sessionId will be carried from the cookie to the background;
- The background gets the user information from the session through the sessionId:
- If there is no session information, intercept it,
- If there is session information, save the user information in threadLocal and let it go
2. Problem analysis of session sharing
Each tomcat has its own session. If the user visits the first tomcat, he stores his information in the session of the first tomcat. If the user visits the second tomcat next time, the session of the second tomcat will have no user information, then there will be problems with the login interception function.
Solution: Synchronizing sessions on all servers will put too much pressure on the server and cause delays. Therefore, replace the session with redis, and the redis data itself is shared, which can avoid the problem of session sharing
3. Redis replaces the session business process
- Question 1: Change the verification code stored in session to redis, which data structure does redis use for storage? The String type can be used; the key cannot be SESSIONID, because SESSIONID is unique to each Tomcat server, when the request is switched to a different Tomcat server, SESSIONID will change, which means that the same verification cannot be taken out when accessing the redis database code. Therefore, use the mobile phone number Phone as the key;
- Question 2: What kind of data structure does redis use to store user information in session instead of in redis? The Hash structure is adopted, because the Hash structure can store each field in the object independently, which means that CRUD can be done for a single field, which is more flexible and takes up less memory (if the String type is used, the value can be saved in JSON format, It is relatively intuitive, but once the object data is very long, many symbols will be generated, which will cause additional consumption of memory);
- Question 3: What should be set as the key for storing objects to ensure uniqueness? It is not recommended to use mobile phone numbers, and it is recommended to use random tokens as keys to store user data.
4. Firstly, send SMS verification code, the steps are as follows:
- The tool class randomly generates a 6-digit verification code;
- Put in redis, key is constant prefix + phone number, value is verification code;
- When logging in later, it will judge whether the input verification code is correct according to the verification code in redis;
Service layer,
@Override public Result sendCode(String phone, HttpSession session) { // 1. Check the phone number if (RegexUtils.isPhoneInvalid(phone)){ // 2. The mobile phone format is incorrect return Result.fail("The mobile phone format is incorrect"); } // 3. Generate verification code String code = RandomUtil. randomNumbers(6); // 4. Save the verification code to redis stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit. MINUTES); // 5. Simulate sending verification code log.debug("Send SMS verification code successfully, verification code: {}", code); return Result.ok(); }
5. Login verification function:
- Randomly generate UUID as token;
- User information is converted into a map and put into redis, key is the constant prefix + token, value is the converted user map;
- If the login is successful, the token will be returned to the front end;
@Override public Result loginToIndex(LoginFormDTO loginForm, HttpSession session) { // 1. Check the phone number String phone = loginForm. getPhone(); if (RegexUtils.isPhoneInvalid(phone)){ // 2. The mobile phone format is incorrect return Result.fail("The mobile phone format is incorrect!"); } // 2. Verify verification code String code = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + loginForm.getPhone()); if (code == null || !code.equals(loginForm.getCode())){ return Result.fail("Verification code error!"); } // 3. Check if the user exists User user = query().eq("phone", phone).one(); // Create a user if the user does not exist if (user == null){ user = createUserWithPhone(phone); } // 4. Generate token String token = UUID.randomUUID().toString(true); // 4. Save the user to redis // Convert UserDTO object to HashMap storage UserDTO userDTO = BeanUtil. copyProperties(user, UserDTO. class); Map<String, Object> targetMap = BeanUtil. beanToMap(userDTO, new HashMap<>(), CopyOptions. create() .setIgnoreNullValue(true) .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())); // store String tokenKey = RedisConstants. LOGIN_USER_KEY + token; stringRedisTemplate.opsForHash().putAll(tokenKey, targetMap); // Set the token validity period stringRedisTemplate.expire(tokenKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES); return Result.ok(token); }
6. Login interceptor
The validity period of the token is refreshed in the interceptor, but can each page be visited, that is, the validity period of the token can be refreshed for each request? The answer is no. So not every request is in the range of paths intercepted by the interceptor!
Solution: In the case of an existing interceptor, add another interceptor, and the interception path is all.
There are two types of access content, one is accessible only after logging in; the other is accessible without logging in.
Therefore, two interceptors can be set, the first interceptor only intercepts content that does not require login,
The first and second interceptors block access to content that can only be accessed after login.
First interceptor:
- Obtain the token from the request header, if there is no token, it will be released directly;
- There is a token, so as to obtain the user information in redis, if not, it means that the token has expired and is released;
- If there is user information, put it into ThreadLocal, update the token expiration time, and let it go
Second interceptor:
- Determine whether ThreadLocal has user information
- If there is one, it has been logged in, the token has not expired, and it is released;
- If there is no login or the token expires, an error message will be returned
public class RefreshInterceptor implements HandlerInterceptor { private StringRedisTemplate stringRedisTemplate; public RefreshInterceptor(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. Get token String token = request. getHeader("authorization"); // 2. Determine whether there is a token if (StrUtil.isBlank(token)){ return true; } // 3. Save UserDto to ThreadLocal String tokenKey = RedisConstants. LOGIN_USER_KEY + token; Map<Object, Object> targetMap = stringRedisTemplate.opsForHash().entries(tokenKey); if (targetMap == null || targetMap.isEmpty()){ return true; } // convert UserDTO userDTO = BeanUtil. fillBeanWithMap(targetMap, new UserDTO(), false); // save UserHolder. saveUser(userDTO); // Refresh the validity period stringRedisTemplate.expire(tokenKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserHolder. removeUser(); } }
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { UserDTO user = UserHolder. getUser(); if (user == null){ response. setStatus(401); return false; } return true; } }
After the interceptor is added to mvcconfig, it will be registered as an InterceptorRegisteration, which has a default attribute order of 0. If the order is not set, the order in which multiple interceptors are executed is the order in which the interceptors are added. For strictness, the order attribute can be set. The larger the order value, the lower the execution priority.
@Configuration public class MvcConfig implements WebMvcConfigurer { @Resource private StringRedisTemplate stringRedisTemplate; @Override public void addInterceptors(InterceptorRegistry registry) { // login interceptor registry. addInterceptor(new LoginInterceptor()) .excludePathPatterns( "/user/login", "/user/code", "/blog/hot", "/shop/**", "/shop-type/**", "/upload/**", "/voucher/**" ).order(1); // token refresh interceptor registry.addInterceptor(new RefreshInterceptor(stringRedisTemplate)) .addPathPatterns("/**").order(0); } }