Environment preparation
Current model
The role of nginx server
-
The mobile phone or app initiates a request to the nginx server. nginx uses the HTTP protocol based on the seven-layer model, which can directly bypass tomcat and access Redis based on Lua.
-
nginx can also be used as a static resource server, easily handling tens of thousands of concurrencies and load balancing to the downstream tomcat server, using the cluster to support the entire project.
-
After using nginx to deploy front-end projects, dynamic and static separation can be achieved to further reduce the pressure on the tomcat service.
The concurrency that enterprise-class MySQL plus solid-state drives can support is about 4,000 to 7,000. For tens of thousands of concurrencies, if tomcat directly accesses MySQL, the CPU and hard disk of the MySQL server will be fully loaded in an instant
- In high concurrency scenarios, you need to choose to use MySQL cluster. At the same time, in order to further reduce the pressure on MySQL and increase access performance, you generally need to use Redis cluster.
Import data/front-end/back-end
For the SQL script
required to execute the project, the version of MySQL must be version 5.7 or above, otherwise some SQL statements cannot be executed when executing the script
Table | Description |
---|---|
tb_user | User table |
tb_user_info | User details table |
tb_shop | Business information table |
tb_shop_type | Business type table |
tb_blog | User Diary Form (Expert’s Store Visit Diary) |
tb_follow | User follow table |
tb_voucher | Coupon table |
tb_voucher_order | Coupon order form |
Import backend project
, put the project into your idea workspace, and then open it with idea
- Step 1: Modify the connection address of MySQL and Reids in the application.yaml file to the address of your own service
- Step 2: Start the project and visit http://localhost:8081/shop-type/list in the browser. If you can see the JSON data, the import is successful.
Import front-end project
and put the nginx decompression directory into a directory you specify (make sure the directory does not contain Chinese, special characters and spaces)
- Step 1: Open a CMD window in the directory where ngnix is located, and execute the
start nginx.exe
command to start the ngnix service - Step 2: Open the browser and adjust the page to mobile mode, visit http://localhost:8080/ to access the project homepage (the data on the page is obtained from the back-end query)
server { listen 9999; server_name localhost; #Specify the location of the front-end project location/{ roothtml/hmdp; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { roothtml; } # Listening path location /api { default_type application/json; #internal; keepalive_timeout 30s; keepalive_requests 1000; #Support keep-alive proxy_http_version 1.1; rewrite /api(/.*) $1 break; proxy_pass_request_headers on; #more_clear_input_headers Accept-Encoding; proxy_next_upstream error timeout; # Location of reverse proxy proxy_pass http://127.0.0.1:8081; #proxy_pass http://backend; } }
SMS login
Implement login process based on Session
Implement the function of sending SMS verification code
The user clicks the Generate verification code button
to initiate a request
The sendCode
method in UserController
handles verification code requests, and the specific business logic is written in UserServiceImpl
- When using email verification, we also need to modify the field type of phone in the database, changing
varchar(11) to varchar(100)
/** *Send mobile phone verification code */ @PostMapping("code") public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {<!-- --> return userService.sendcode(phone,session); }
/** *Send mobile phone verification code */ @Override public Result sendcode(String phone, HttpSession session) {<!-- --> // 1. Verify mobile phone number. RegexUtils is a tool class we created, which also requires the RegexPatterns tool class. if (RegexUtils.isPhoneInvalid(phone)) {<!-- --> // 2. If it does not match, return an error message return Result.fail("Mobile phone number format is wrong!"); } // 3. Compliant with generating verification code, RandomUtil is a tool class of hutool-all String code = RandomUtil.randomNumbers(6); // 4. Save the verification code to the session session.setAttribute("code",code); // 5.Send verification code log.debug("Sending SMS verification code successfully, verification code: {}", code); // 6. Return successful information return Result.ok(); } /** *Send email verification code */ @Override 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 = MailUtils.achieveCode(); session.setAttribute(phone, code); log.info("Send login verification code: {}", code); MailUtils.sendTestMail(phone, code); return Result.ok(); }
Implement login function
The user clicks the Login button
to initiate a request
/** * 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){<!-- --> return userService.login(loginForm, session); }
//LoginForm encapsulates login parameters, including mobile phone number, verification code or mobile phone number and password @Data public class LoginFormDTO {<!-- --> private String phone; private String code; private String password; }
@Override public Result login(LoginFormDTO loginForm, HttpSession session) {<!-- --> // 1. Verify the format of mobile phone number String phone = loginForm.getPhone(); if (RegexUtils.isPhoneInvalid(phone)) {<!-- --> // 2. If it does not match, return an error message return Result.fail("Mobile phone number format is wrong!"); } // 3. Compare the verification code obtained from the session with the verification code submitted by the user Object cacheCode = session.getAttribute("code"); String code = loginForm.getCode(); // The objects obtained from the session by default are of type Object, so we need to convert them to String type if(cacheCode == null || !cacheCode.toString().equals(code)){<!-- --> // 4. If the verification code is inconsistent, an error will be reported return Result.fail("Verification code error"); } // 5. If the verification code is consistent, the user will be retrieved from the database based on the mobile phone number submitted by the user. User user = query().eq("phone", phone).one(); // Determine whether the user exists if(user == null){<!-- --> //6. Create if it does not exist user = createUserWithPhone(phone); } //7. Save user information to session session.setAttribute("user",user); //Return successful information return Result.ok(); } //Create a new user private User createUserWithPhone(String phone) {<!-- --> //Create user User user = new User(); //Set mobile phone number user.setPhone(phone); //Set the nickname (default name), a fixed prefix + random string, USER_NICK_NAME_PREFIX is a system constant in the tool class user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(8)); //Save user information to database save(user); return user; }
Implement login verification function (interceptor)
When we log in successfully, the front end will also initiate a user/me
request for login verification. Since accessing each Controller
requires login verification, So we can write the verification process in the interceptor
The first step; Define a tool class UserHolder
specifically to store user information, and each thread corresponds to its own user information
public class UserHolder {<!-- --> private static final ThreadLocal<User> tl = new ThreadLocal<>(); public static void saveUser(User user){<!-- --> // Save user information, the default key is the current thread tl.set(user); } public static User getUser(){<!-- --> // Get user information, the default key is the current thread return tl.get(); } public static void removeUser(){<!-- --> //Delete user information, the default key is the current thread tl.remove(); } }
Step 2: Create a LoginInterceptor
class to implement the HandlerInterceptor
interface to prevent users from accessing project functions directly through url path
. Before rewriting Set interceptor method and completion processing method
preHandle method
: Used for permission verification before we log in. At the same time, the user information obtained from the session is saved to the ThreadLocal of the UserHolder to facilitate obtaining user information in the Controller later.afterCompletion method
: used to process information after login to avoid memory leaks
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //1. Get session HttpSession session = request.getSession(); //2. Get the user information saved in the session Object user = session.getAttribute("user"); //3. Determine whether the user exists if(user == null){ //4. If the user does not exist, the request will be intercepted and a 401 status code will be returned. response.setStatus(401); return false; } //5. If the user exists, save the user information to Threadlocal of UserHolder. UserHolder.saveUser((User)user); //6. Release return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserHolder.removeUser(); } }
Step 3: Write configuration class
, register the login interceptor and set the requests that the interceptor needs to intercept
@Configuration public class MvcConfig implements WebMvcConfigurer {<!-- --> @Override public void addInterceptors(InterceptorRegistry registry) {<!-- --> //Register the login interceptor to make it effective registry.addInterceptor(new LoginInterceptor()) .excludePathPatterns(//Set requests that the interceptor does not need to intercept "/shop/**", "/voucher/**", "/shop-type/**", "/upload/**", "/blog/hot", "/user/code", "/user/login" ); } }
Step 4: Obtain the login user information corresponding to the current thread and respond to the front end to complete the login verification
@GetMapping("/me") public Result me() {<!-- --> User user = UserHolder.getUser(); return Result.ok(user); }
Hide sensitive user information
It is unsafe to respond to all the logged in user information to the browser during login verification, so we should hide the user’s sensitive information before returning the logged in user information
{<!-- --> "success":true, "data":{<!-- --> "id":1010, "phone":"1586385296", "password":"", "nickName":"user_i1b3ir09", "icon":"", "createTime":"2022-10-22T14:20:33", "updateTime":"2022-10-22T14:20:33" } }
Step 1: Create a new UserDto
object that does not contain user sensitive information, and convert the User object containing user sensitive information returned during login verification into a UserDto object
@Data public class UserDTO {<!-- --> private Long id; private String nickName; private String icon; }
Step 2: Modify the generic type of ThreadLocal in the UserHolder tool class
public class UserHolder {<!-- --> private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>(); public static void saveUser(UserDTO user){<!-- --> tl.set(user); } public static UserDTO getUser(){<!-- --> return tl.get(); } public static void removeUser(){<!-- --> tl.remove(); } }
Step 3: Modify the login method and convert the User object saved in the session domain into a UserDto object
@Override public Result login(LoginFormDTO loginForm, HttpSession session) {<!-- --> // 1. Verify the format of mobile phone number String phone = loginForm.getPhone(); if (RegexUtils.isPhoneInvalid(phone)) {<!-- --> // 2. If it does not match, return an error message return Result.fail("Mobile phone number format is wrong!"); } // 3. Compare the verification code obtained from the session with the verification code submitted by the user Object cacheCode = session.getAttribute("code"); String code = loginForm.getCode(); // The objects obtained from the session by default are of type Object, so we need to convert them to String type if(cacheCode == null || !cacheCode.toString().equals(code)){<!-- --> // 4. If the verification code is inconsistent, an error will be reported return Result.fail("Verification code error"); } // 5. If the verification code is consistent, the user will be retrieved from the database based on the mobile phone number submitted by the user. User user = query().eq("phone", phone).one(); // Determine whether the user exists if(user == null){<!-- --> //6. Create if it does not exist user = createUserWithPhone(phone); } //7. Hide the user's information and then save it to the session. session.setAttribute("user",user); //UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); //session.setAttribute("user", userDTO); session.setAttribute("user", BeanUtils.copyProperties(user,UserDTO.class)); //Return successful information return Result.ok(); }
Step 4: Modify the interceptor to save the UserDTO
object obtained from the session that hides the user information into the ThreadLocal of the UserHolder class
public class LoginInterceptor implements HandlerInterceptor {<!-- --> @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {<!-- --> //1. Get session HttpSession session = request.getSession(); //2. Obtain the UserDTO type user object with user information hidden in the session, and save it to the ThreadLocal of the UserHolder class UserDTO user = (UserDTO) session.getAttribute("user"); UserHolder.saveUser(user); //3. Determine whether the user exists if(user == null){<!-- --> //4. If it does not exist, intercept the current request and return the 401 status code response.setStatus(401); return false; } //5. If there is any hidden information saved by the user to Threadlocal UserHolder.saveUser((User)user); //6. Release return true; } }
Step 5: Modify the login verification method and return the UserDTO
object saved by ThreadLocal in UserHolder
//Modify the type of information obtained to be the user’s hidden information @GetMapping("/me") public Result me() {<!-- --> UserDTO user = UserHolder.getUser(); return Result.ok(user); }
Step 6: Restart the server. The user information returned after login verification no longer contains sensitive information
{<!-- --> "success":true, "data":{<!-- --> "id":1016, "nickName":"user_zkhf7cfv", "icon":"" } }