Reference article: https://cyborg2077.github.io/2022/10/22/RedisPractice/
1. SMS login
- Applies to Redis shared session to achieve
Import backend project:
- Unzip the code provided by Dark Horse and put it into your own workSpace
- Modify MySQL and Redis to your own configuration
- Access http://localhost:8081/shop-type/list. If you see JSON data, the access is successful.
Import front-end project (for mac):
- Download the front-end decompression package provided by Dark Horse, and put the html file (hmdp) inside into the address (/opt/homebrew/var/www)
- Overwrite nginx.config in the config package with nginx.config in the adjustment address (/opt/homebrew/etc/nginx), and restart nginx after modification:
brew services restart nginx
- Visit http://localhost:8083/ and turn on developer mode
: If 404 appears during startup, you can try to modify the port number in nginx.config
1.1. Implement the login process based on session (log in using email)
- Send verification code
After the user submits their email address, it will be verified whether the email address is legal. If it is not legal, the user will be asked to re-enter their email address. If the email is legitimate, a corresponding verification code is generated in the background, the verification code is saved, and then the verification code is sent to the user via email.
- Email verification code to log in and register
The user enters the verification code and email address, and the descendant gets the current verification code from the session, and then verifies it with the verification code entered by the user. If it is inconsistent, the verification fails. If they are consistent, the background will query the user based on the mailbox. If the user does not exist, create account information for the user and save the database. Regardless of whether it exists, the user’s information will be saved in the session to facilitate subsequent acquisition of the current login information.
- Verify login status
When the user makes a request, the sessionId will be carried from the cookie to the background. The background will get the user information from the session through the sessionId. If there is no session information, it will be intercepted. If there is session information, the user information will be saved in threadLocal and release
1.1.1. Implement the function of sending email verification code
- Enter the email number, click the Send Verification Code button to view the sent request
Request address: http://localhost:8083/api/user/[email protected] Request method: POST
- Import maven required for email verification
<!-- https://mvnrepository.com/artifact/javax.activation/activation --> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency> <!-- https://mvnrepository.com/artifact/javax.mail/mail --> <dependency> <groupId>javax.mail</groupId> <artifactId>mail</artifactId> <version>1.4.7</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-email --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-email</artifactId> <version>1.4</version> </dependency>
- Write a tool class for users to send verification codes
import javax.mail.MessagingException; import javax.mail.Session; import javax.mail.Transport; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; import javax.mail.Authenticator; import javax.mail.PasswordAuthentication; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Properties; public class MailUntils { public static void main(String[] args) throws MessagingException{ //You can test your own methods directly here sendMail("[email protected]", achieveCode()); } /** * send email * @param email address * @param code verification code * @throws MessagingException */ public static void sendMail(String email, String code) throws MessagingException { //Wear the Properties property, which is used to record some properties of the mailbox Properties properties = new Properties(); //Indicates that SMTP sends emails and must undergo identity authentication. properties.put("mail.smtp.auth", "true"); //Fill in the SMTP server here properties.put("zail.smtp.host", "smtp.qq.com"); //Port number, qq is 587 properties.put("ail.smtp.port", "587"); //Fill in the account number of the writer here properties.put("mail.user", "[email protected]"); //Fill in the 16-digit SMTP password here, which can be opened in the qq mailbox properties.put("mail.password", "wglXXXXXXrcdec"); //Build authorization information for SMTP authentication Authenticator authenticator = new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { //Username Password String userName = properties.getProperty("mail.user"); String password = properties.getProperty("mail.password"); PasswordAuthentication passwordAuthentication = new PasswordAuthentication(userName, password); return passwordAuthentication; } }; //Use environment attributes and authorization information to create an email session Session mailSession = Session.getInstance(properties, authenticator); //Create email message MimeMessage message = new MimeMessage(mailSession); //Set the sender InternetAddress from = new InternetAddress(properties.getProperty("mail.user")); message.setFrom(from); //Set the recipient's email address InternetAddress to = new InternetAddress(email); message.setRecipient(Message.RecipientType.TO, to); //Set the email title and content message.setSubject("TWENTY verification code"); message.setContent("Dear user: Hello!\\ The registration verification code is:" + code + "(valid for one minute, please do not tell others)", "text/html;charset= UTF-8"); //send email Transport.send(message); } /**、 * Verification code generation method * @return */ public static String achieveCode() { //Because the numbers 1 and 0 and the letters O and l are sometimes unclear, there are no numbers 1 and 0 String[] beforeShuffle = new String[]{"2", "3", "4", "5", "6", "7", "8\ ", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", " O", "P", "Q", "R", "S", "T", "U", "V", "W\ ", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", " j", "k", "l", "m", "n", "o", "p", "q", "r\ ", "s", "t", "u", "v", "w", "x", "y", "z"}; List<String> list = Arrays.asList(beforeShuffle);//Convert array to collection Collections.shuffle(list); //Shuffle the order of collections StringBuilder sb = new StringBuilder(); for (String s : list) { sb.append(s); //Convert the collection into a string } return sb.substring(3, 8); } }
- Complete the sendCode method, the logic is as follows:
-
- Verify email format
-
-
- If incorrect, an error message will be returned.
- Send the verification code correctly
-
/** *Send email verification code */ @PostMapping("code") public Result sendCode(@RequestParam("phone") String phone, HttpSession session) throws MessagingException { // TODO send SMS verification code and save verification code if(RegexUtils.isEmailInvalid(phone)){ return Result.fail("The email format is incorrect!!!"); } String code = MailUntils.achieveCode(); session.setAttribute(phone, code); MailUntils.sendMail(phone, code); return Result.ok(); }
- Login function
Request address: http://localhost:8083/api/user/login Request method: POST
Modify the login method, the logic is as follows
- Verify email
-
- If incorrect, an error message will be returned.
- If correct, continue to verify the verification code
-
-
- Inconsistency error
- If consistent, query the user’s mailbox first
-
-
-
-
- Create if the user does not exist
- If it exists, continue execution.
-
-
-
-
- Save user information into session
-
/** * Login function * @param loginForm login parameters, including mobile phone number and verification code; or mobile phone number and password */ @PostMapping("/login") public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){ String email = loginForm.getPhone(); String code = loginForm.getCode(); String cacheCode = session.getAttribute(email).toString(); if(RegexUtils.isEmailInvalid(email)){ return Result.fail("The email format is incorrect!!!"); } if(code == null || !cacheCode.equals(code)){ return Result.fail("The verification code is inconsistent!!!"); } LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper(); queryWrapper.eq(User::getPhone, email); User user = userService.getOne(queryWrapper); if(user == null){ user.setPhone(email); user.setNickName("user_" + RandomUtil.randomString(5)); userService.save(user); } // Return the encapsulated object UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); session.setAttribute("user", userDTO); return Result.ok(); }
1.1.2. Login interception function
- Create a LoginInterceptor class, implement the HandlerInterceptor interface, and rewrite two of its methods, the pre-interceptor and the completion processing method. The pre-interceptor is mainly used for permission verification before we log in. , the completion processing method is used to process information after login to avoid memory leaks
Interceptor
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // Get session HttpSession session = request.getSession(); // Get user information UserDTO user = (UserDTO)session.getAttribute("user"); //Determine whether the user exists if(user == null){ response.setStatus(401); return false; } //Save user information to ThreadLocal if it exists. UserHolder is a good tool class provided. UserHolder.saveUser(user); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserHolder.removeUser(); } }
Interceptor configuration
@Configuration public class MvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .excludePathPatterns( "/user/code", "/user/login", "/blog/hot", "/shop/**", "/shop-type/**", "/upload/**", "/voucher/**" ); } }
Tools
public class public class UserHolder { private static final ThreadLocal<User> tl = new ThreadLocal<>(); public static void saveUser(User user){ tl.set(user); } public static User getUser(){ return tl.get(); } public static void removeUser(){ tl.remove(); } }
1.1.3. Session sharing issues
- Each tomcat has its own session. Assume that the user accesses the first tomcat for the first time and stores his information in the session of the first server, but the user The second time this user accesses the second tomcat, then there must be no session stored on the first server on the second server, so at this time there will be a problem with the entire login interception function. How can we solve this problem? The early solution was session copy, which means that although each tomcat has a different session, whenever the session of any server is modified, it will be synchronized to the session of other Tomcat servers. In this way, the session can be realized shared
- But this solution has two big problems
-
- Each server has a complete copy of session data, and the server pressure is too high.
- There may be a delay when the session copies data.
- So the rest is based on Redis. We replace the session with Redis. The Redis data itself is shared, which can avoid the problem of session sharing.
1.2. Business process of using Redis instead of session
1.2.1. Design key structure
- First, let’s think about what data structure should be used to store data
- Since the stored data is relatively simple, you can use String or Hash
-
- If you use String to save data in JSON string, it will take up some extra space.
- If you use Hash, only the data itself will be stored in its value
- If you don’t particularly care about memory, just use String.
1.2.2. Specific details of designing key
- The simple K-V key-value pair method is used here.
- But for key processing, you cannot use phone or code as key like session.
- Because the key of Redis is shared, the code may be repeated, and sensitive fields such as phone are not suitable for storage in Redis.
- When designing the key, two points need to be met
-
- Key must be unique
- The key should be easy to carry
- So a token is randomly generated in the background, and then the front-end can complete the business logic with this token.
1.2.3. Overall access process
- When the registration is completed, the user logs in, and then verifies whether the email address submitted by the user is consistent with the verification code
-
- If they are consistent, query the user information based on the email address. If it does not exist, create a new one. Finally, save the user data to Redis and generate a token as the Key of Redis.
- When verifying whether the user is logged in, bring the token back for access, obtain the value corresponding to the token from Redis, and determine whether this data exists
-
- If it does not exist, intercept it
- If it exists, save its user information to threadLocal and release it
1.2.4. Email login based on Redis
- Modify sendCode method
Put the generated code into Redis, the key should be in the form of login:code:email, and set the validity period to 2 minutes
/** *Send mobile phone verification code using redis */ @PostMapping("code") public Result sendCode(@RequestParam("phone") String phone, HttpSession session) throws MessagingException { // TODO send SMS verification code and save verification code if(RegexUtils.isEmailInvalid(phone)){ return Result.fail("The email format is incorrect!!!"); } String code = MailUntils.achieveCode(); //session.setAttribute(phone, code); MailUntils.sendMail(phone, code); redisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES); return Result.ok(); }
Constant settings
public class RedisConstants { public static final String LOGIN_CODE_KEY = "login:code:"; public static final Long LOGIN_CODE_TTL = 2L; public static final String LOGIN_USER_KEY = "login:token:"; public static final Long LOGIN_USER_TTL = 36000L; public static final Long CACHE_NULL_TTL = 2L; public static final Long CACHE_SHOP_TTL = 30L; public static final String CACHE_SHOP_KEY = "cache:shop:"; public static final String LOCK_SHOP_KEY = "lock:shop:"; public static final Long LOCK_SHOP_TTL = 10L; public static final String SECKILL_STOCK_KEY = "seckill:stock:"; public static final String BLOG_LIKED_KEY = "blog:liked:"; public static final String FEED_KEY = "feed:"; public static final String SHOP_GEO_KEY = "shop:geo:"; public static final String USER_SIGN_KEY = "sign:"; }
- Modify login method
Use hashMap to store users and store them in Redis. Key + token is used as the key value.
/** * Login function * @param loginForm login parameters, including mobile phone number and verification code; or mobile phone number and password */ @PostMapping("/login") public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){ String email = loginForm.getPhone(); String code = loginForm.getCode(); //String cacheCode = session.getAttribute(email).toString(); String codeKey = RedisConstants.LOGIN_CODE_KEY + email; String cacheCode = (String) redisTemplate.opsForValue().get(codeKey); if(RegexUtils.isEmailInvalid(email)){ return Result.fail("The email format is incorrect!!!"); } if(code == null || !cacheCode.equals(code)){ return Result.fail("The verification code is inconsistent!!!"); } LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper(); queryWrapper.eq(User::getPhone, email); User user = userService.getOne(queryWrapper); if(user == null){ user.setPhone(email); user.setNickName("user_" + RandomUtil.randomString(5)); userService.save(user); } // Return the encapsulated object //session.setAttribute("user", userDTO); //Use redis to save user information //1. Randomly generate a token as a login token String token = UUID.randomUUID().toString(); //2. Convert userDTO object to HashMap storage UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); HashMap<String, String> userMap = new HashMap<>(); userMap.put("id", String.valueOf(userDTO.getId())); userMap.put("icon", userDTO.getIcon()); userMap.put("nickName", userDTO.getNickName()); //3. Storage String tokenKey = RedisConstants.LOGIN_USER_KEY + token; //3.1 Set the token validity period to 30 minutes redisTemplate.expire(tokenKey, 30, TimeUnit.MINUTES); redisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap); //4. Delete the verification code information after successful login redisTemplate.delete(codeKey); // 5. Return token return Result.ok(token); }
1.2.5. Solve the problem of status login refresh
- Initial plan
- We can use the requests intercepted by the interceptor to prove whether the user is operating. If the user does not perform any operations for 30 minutes, the token will disappear and the user needs to log in again.
- By checking the request, we find that the token we saved is in the request header, so we refresh the token survival time in the interceptor
authorization: 6867061d-a8d0-4e60-b92f-97f7d698a1ca
Modify interceptor
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //1. Get the token in the request header String token = request.getHeader("authorization"); //2. If the token is empty, then you are not logged in and intercept it. if (StrUtil.isBlank(token)) { response.setStatus(401); return false; } String key = RedisConstants.LOGIN_USER_KEY + token; //3. Get user data in Redis based on token Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key); //4. Determine whether the user exists. If not, intercept it. if (userMap.isEmpty()) { response.setStatus(401); return false; } //5. Convert the queried Hash data into a UserDto object UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); //6. Save user information to ThreadLocal UserHolder.saveUser(userDTO); //7. Refresh tokenTTL. Set the survival time here as needed. I changed the constant value here to 30 minutes. stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES); return true; }
- In this solution, he can indeed use the interception of the corresponding path and refresh the survival time of the login token at the same time, but now this interceptor only intercepts the path that needs to be intercepted. Assuming that the current If the user accesses some paths that do not need to be intercepted, then the interceptor will not take effect, so the token refresh action will not actually be executed at this time, so there is a problem with this solution.
- Optimization plan
- Since the previous interceptor cannot take effect on paths that do not need to be intercepted, you can add an interceptor, intercept all paths in the first interceptor, and make the second interceptor Put the things into the first interceptor and refresh the token at the same time. Because the first interceptor has threadLocal data, so at this time the second interceptor only needs to determine whether the user object in the interceptor exists. , complete the overall refresh function.
Refresh token interceptor
public class RefreshTokenInterceptor implements HandlerInterceptor { private RedisTemplate redisTemplate; public RefreshTokenInterceptor(RedisTemplate redisTemplate){ this.redisTemplate = redisTemplate; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //Get the token in the request header String token = request.getHeader("authorization"); //If empty, release it directly and hand it over to LoginInterceptor for processing. if(StrUtil.isBlank(token)){ return true; } String key = RedisConstants.LOGIN_USER_KEY + token; //Get user information Map<Object, Object> userMap = redisTemplate.opsForHash().entries(key); //Determine whether the user exists, and allow it if it exists. if(userMap.isEmpty()){ return true; } //Convert the queried user into a userDTO object UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); UserHolder.saveUser(userDTO); //Refresh token redisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserHolder.removeUser(); } }
Modify the LoginInterceptor. You only need to determine whether the user exists. If not, intercept it.
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if(UserHolder.getUser() == null){ response.setStatus(401); return false; } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserHolder.removeUser(); } }
Define interceptor order
@Configuration public class MvcConfig implements WebMvcConfigurer { @Resource private RedisTemplate redisTemplate; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .excludePathPatterns( "/user/code", "/user/login", "/blog/hot", "/shop/**", "/shop-type/**", "/upload/**", "/voucher/**" ).order(1); registry.addInterceptor(new RefreshTokenInterceptor(redisTemplate)).order(0); } }