User login is by and large the most common feature of a website. Of course, it’s not that hard to implement user login using Spring Boot, and today I’m going to share the process of building a user login feature from scratch.

Of course, you still need to master the basic use of Spring Boot and MyBatis configuration (SSM basic operation), then look at this article is better.

At the beginning, let’s agree that the main class is located in the project package path: com.example.userlogin, and the rest of the newly created software packages are under this package path.

At the end of this article, I’ll also give you an example repository address.

1, basic knowledge – the implementation principle of user login

For user login, we’ve all heard the word cookie. In fact, cookies are one of the most widely used technologies in network programming. They are used to store a user’s login information. It is stored locally to the user and is used by the server to determine whether the user is logged in every time a network request is sent to the server. Take a look at this:

After login, the client will put the login information in the request to the server, and the server will verify it. After successful login, the server will put the user information in the cookie. With the response returned to the client, the client will store the cookie. The next time you visit the website, the cookie will be sent to the server together with the request from the client. The server verifies that the information is correct and determines that the user is logged in.

Cookie also stores data in the form of key-value, and cookie has its own attributes such as life cycle, effective domain name, and so on.

But in fact, the cookie stored in the user information, it is easy to exist security risks, cookies can be intercepted or even forged.

Therefore, the session mechanism is now used for website login. The biggest difference between it and cookie mechanism is that the user information is not placed on the client but on the server. The interaction process is shown in the figure below:

It can be seen that the session mechanism uses cookie as the carrier, which only stores a session ID and communicates with the server. Each client and server will generate a unique session ID for each login request, which means that the client can find the corresponding client data as long as the client tells the server the session ID.

So in the following examples we use the session mechanism. Spring Boot makes it easy to read and write sessions.

2. Start – Perform configuration

We need to configure the following dependencies:

  • jackson-annotationsJson configuration Comments
<! -- Jackson --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> The < version > 2.12.4 < / version > < / dependency >Copy the code
  • codecCode encryption, used to encrypt and store user passwords
<! <dependency> <groupId> Commons -codec</groupId> <artifactId> Commons -codec</artifactId> </dependency>Copy the code
  • common-lang3Utility set
<! <dependency> <groupId>org.apache.commons</groupId> <artifactId> Commons -lang3</artifactId> </dependency>Copy the code
  • Spring WebBasic Spring Boot network support
<! -- Spring Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>Copy the code
  • ValidationFor information verification
<! -- Spring Validation --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>Copy the code
  • Spring SessionSpring Boot supports session operations
<! -- Spring Session --> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-core</artifactId> </dependency>Copy the code
  • MyBatisThe well-known database persistence framework operates databases
<! -- MyBatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> < artifactId > mybatis - spring - the boot - starter < / artifactId > < version > 2.2.0 < / version > < / dependency > <! -- MySQL --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>Copy the code
  • LombokCode simplification, not much to say
<! -- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>Copy the code
  • Spring TestBuilt-in test functionality, not to mention
<! -- Spring Test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>Copy the code

Dependencies can be added or deleted depending on your situation, but I’m using them here, and most of them can also be selected when creating A Spring Boot project.

Then configure the project configuration file application.properties

Server.port =8802 Setting serialization to save by not unknown fields and null field flow spring. Jackson. Deserialization. Fail - on - unknown - properties = false Spring.jackson.default-property-inclusion =non_null # MySQL database address and account configuration spring.datasource.url=jdbc:mysql://localhost:3306/miyakogame?serverTimezone=GMT%2B8 spring.datasource.username=swsk33 spring.datasource.password=dev-2333Copy the code

Mybatis Mapper XML file is located under SRC /main/resources/com/{XXX}/{XXX}/dao/ by default, corresponding to your DAO package location. For example we project Mapper classes generally in com. Example. Userlogin. Dao, then Spring Boot will default to the local scan Mapper XML: SRC/main/resources/com/example/userlogin/dao, need to manually create the directory. Of course we can also specify:

mybatis.mapper-locations=file:Resources/mybatisMapper/*.xml
Copy the code

Instead of configuring the XML location for MyBatis, our project uses the default location. Fill in the configuration according to the actual situation.

3. Encapsulate a request result classResult<T>

For convenience, we usually encapsulate a result class that contains the request result code, the message, whether the operation was successful, and the data body, which can be modified according to your needs.

Create software package model and create Result

in it. The content is as follows:

package com.example.userlogin.model; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import java.io.Serializable; Public class Result<T> implements Serializable {/** ** @param <T> constructor */ @argsconstructor public class Result<T> implements Serializable {/** ** @param <T> constructor */ @argsconstructor public class Result<T> implements Serializable { / / private String message; /** * Whether the operation succeeded */ private Boolean success; */ private T data; /** * private T data; Public void setResultSuccess(String MSG, int, int, int, int, int, int, int, int) T data) { this.message = msg; this.success = true; this.data = data; Public void setResultFailed(String MSG) {this.message = MSG; this.success = false; this.data = null; }}Copy the code

This result object is usually returned to the front end for easier delivery of success, manipulation messages, and so on.

Note that passing interactive data back and forth requires a serialized interface and a parameterless constructor. Lombok’s annotations are used here, as they are below.

4, create user class, and initialize the database table

Create software package DataObject and establish the User class User in it. In practice, the User class may have many attributes according to different business needs. Here, we only establish the simplest one as follows:

package com.example.userlogin.dataobject; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Size; import java.io.Serializable; /** * User class */ @set@getter @noargsconstructor @jsonignoreProperties (value = {"password"}, AllowSetters = true) public class User implements Serializable {/** * User ID */ private Integer ID; /** * username */ @notempty (message = "username cannot be empty!") ) private String username; /** * password */ @notempty (message = "password cannot be empty!") ) @size (min = 8, message = "Password length cannot be less than 8!" ) private String password; }Copy the code

We set the most basic attributes of the user and set Validation rules for them (if you’re not familiar with Spring Validation, see this article). The password is sensitive information, so we use the Jackson annotation to set the password field to not allow serialization (not to be passed to the front end).

SQL > create user class; SQL > create user class; SQL > create user class; SQL > create user class;

drop table if exists `user`;
create table `user`
(
   `id`       int unsigned auto_increment,
   `username` varchar(16) not null,
   `password` varchar(32) not null,
   primary key (`id`)
) engine = InnoDB
  default charset = utf8mb4;
Copy the code

Connect to MySQL and use the corresponding database. Execute the SQL file using the source command.

Each object has a primary key ID and is generally set to an autoincrementing unsigned integer for maximum database read and write efficiency. Passwords are usually encrypted using MD5, so the password length is fixed to 32 bits. In general, every database table has the gmT_CREATED and GMT_Modified fields, which are omitted here for simplicity.Copy the code

5, create data service layer -DAO and configure Mapper XML

The DAO layer is mainly a Java interface and implementation class for database operations. The power of MyBatis is that you can operate the database only by defining the interface.

Create interface UserDAO:

package com.example.userlogin.dao; import com.example.userlogin.dataobject.User; import org.apache.ibatis.annotations.Mapper; @mapper public interface UserDAO {/** * New user ** @param user user object * @return Number of successful new entries */ int add(user user); /** * Modify user information ** @param user object * @return Number of successful changes */ int update(user user); /** * Get User by id ** @param id User ID * @return User object */ User getById(Integer ID); /** * Get a User by Username ** @param username Username * @return User object */ User getByUsername(String username); }Copy the code

According to the actual need to define the database to add, delete and change the search method, here to define these. Note that the @mapper annotation is placed on the interface to indicate that it is a data persistence interface. Generally speaking, the return value of the add, delete and change methods is int, indicating the number of successful operation records, and the search method generally returns the corresponding object or the List of objects.

Then write Mapper XML files, we under the project folder SRC/main/resources directory created under multi-level directory: com/example/userlogin/dao, in this directory to store the XML file.

Create userdao. XML with the following content:

<? The XML version = "1.0" encoding = "utf-8"? > <! DOCTYPE mapper PUBLIC "- / / mybatis.org//DTD mapper / 3.0 / EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > < mapper namespace="com.example.userlogin.dao.UserDAO"> <resultMap id="userResultMap" type="com.example.userlogin.dataobject.User"> <id column="id" property="id"/> <result column="username" property="username"/> <result column="password" property="password"/> </resultMap> <insert id="add" parameterType="com.example.userlogin.dataobject.User"> insert into `user` (username, password) values (#{username}, #{password}) </insert> <update id="update" parameterType="com.example.userlogin.dataobject.User"> update `user` set password=#{password} where id = #{id} </update> <select id="getById" resultMap="userResultMap"> select * from `user` where id = #{id} </select> <select id="getByUsername" resultMap="userResultMap"> select * from `user` where username = #{username} </select> </mapper>Copy the code

In this way, the database operation layer is complete!

General convention a certain object (XXX) database operation layer interface is generally named xxxDAO (some enterprises also named xxxMapper), an xxxDAO interface corresponds to an xxxDAO. XML file.Copy the code

6. Create the user service layer

Now it’s time for the formal service layer logic to complete our main functions: user registration, login, and information modification.

In fact, User registration is the process of front-end sending User registration information (encapsulated as User object), back-end inspection and then adding a User record to the database; Login is also a process in which the front-end sends the User login information, which is also encapsulated as a User object. The back-end retrieves the User from the database according to the username field of the object, makes a comparison and finally sets the session. Modifying a User is also a process in which the front-end sends the User object with modified User information, the back-end compares it, and then modifies the corresponding record in the database.

Create package service and add UserService interface UserService to it:

package com.example.userlogin.service; import com.example.userlogin.dataobject.User; import com.example.userlogin.model.Result; import org.springframework.stereotype.Service; @service public interface UserService {/** * User registration ** @param user Object */ return registration Result */ Result< user > register(user user); /** * User login ** @param user user object * @return login Result */ Result< user > login(user user); /** * Modify user information ** @param user user object * @return Result */ Result< user > update(user user); }Copy the code

Create the impl under the package service and create UserServiceImpl within it:

package com.example.userlogin.service.impl; import com.example.userlogin.dao.UserDAO; import com.example.userlogin.dataobject.User; import com.example.userlogin.model.Result; import com.example.userlogin.service.UserService; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class UserServiceImpl implements UserService { @Autowired private UserDAO userDAO; @Override public Result<User> register(User user) { Result<User> result = new Result<>(); User getUser = null; // getUser = null; try { getUser = userDAO.getByUsername(user.getUsername()); } catch (Exception e) { e.printStackTrace(); } if (getUser ! = null) {result. SetResultFailed (" the username already exists! ") ); return result; } // Encrypt and store the user's password user.setPassword(digestutils.md5hex (user.getPassword())); Userdao.add (user); // Return user data with a success message result.setresultSuccess (" Registered user succeeded! ") , user); return result; } @Override public Result<User> login(User user) { Result<User> result = new Result<>(); User getUser = null; try { getUser = userDAO.getByUsername(user.getUsername()); } catch (Exception e) { e.printStackTrace(); } if (getUser == null) {result.setresultfailed (" user does not exist! ); return result; } // compare the password (the database retrieved the user password is encrypted, so the front end sent the user password encryption and then compare); Getuser.getpassword ().equals(digestUtils.md5hex (user.getPassword()))) {result.setresultFailed (" User name or password error! ") ); return result; } result. SetResultSuccess (" Login succeeded! ") , getUser); return result; } @Override public Result<User> update(User user) { Result<User> result = new Result<>(); If (user.getid () == null) {result.setresultFailed (" User ID cannot be empty! ") ); return result; } User getUser = null; try { getUser = userDAO.getById(user.getId()); } catch (Exception e) { e.printStackTrace(); } if (getUser == null) {result.setresultfailed (" user does not exist! ); return result; } // Check whether the field value in the passed object is null, If (stringutils.isEmpty (user.getPassword())) {user.setPassword(getUser.getPassword()); } else {// encrypt the user.setPassword(digestutils.md5hex (user.getPassword())); } // Save to the database userdao.update (user); Result. SetResultSuccess (" User modification succeeded! ") , user); return result; }}Copy the code

Note that the Service interface requires the @Service annotation, the interface implementation class requires the @Component annotation, and the DAO instance is automatically injected for database operations.

7. Create user login API

With the service layer written, it’s time to bridge the interaction between the front and back ends – write the API so the front end can send requests to call our back end services.

Our API to achieve user login, registration, judge whether the user login, modify user information, user logout these several functions.

Create a new package API and create a UserAPI class inside it:

package com.example.userlogin.api; import com.example.userlogin.dao.UserDAO; import com.example.userlogin.dataobject.User; import com.example.userlogin.model.Result; import com.example.userlogin.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javax.validation.Valid; Public static final String SESSION_NAME = "userInfo"; public static final String SESSION_NAME = "userInfo"; @Autowired private UserService userService; @Autowired private UserDAO userDAO; /** * @param user @param Errors Validation @param request @param request */ @postMapping ("/register") public Result<User> register(@requestBody @valid User User, BindingResult errors, HttpServletRequest request) { Result<User> result; If (errors.haserrors ()) {result = new result <>(); result.setResultFailed(errors.getFieldError().getDefaultMessage()); return result; } result = userService.register(user); If (result.isSuccess()) {request.getSession().setAttribute(SESSION_NAME, result.getData()); } return result; } /** * @param user @param errors Validation @param request @param request Session * @return login Result */ @postmapping ("/login") public Result<User> login(@requestBody @valid User User, BindingResult errors, HttpServletRequest request) { Result<User> result; If (errors.haserrors ()) {result = new result <>(); result.setResultFailed(errors.getFieldError().getDefaultMessage()); return result; } result = userService.login(user); // If the login is successful, set session if (result.isSuccess()) {request.getSession().setAttribute(SESSION_NAME, result.getData()); } return result; } /** * Determine whether the user has logged in ** @param request Request object, obtain the user information in the session to determine whether the user has logged in * @return result object, has logged in the result is successful, and the data body is the user information; Otherwise, the result is failure, */ @getMapping ("/ isLogin ") public Result<User> isLogin(HttpServletRequest Request) {HttpSession Session = request.getSession(); Result<User> result = new Result<>(); User sessionUser = (User) session.getAttribute(SESSION_NAME); If (sessionUser == null) {result.setresultFailed (" user is not logged in! "); ); return result; } User getUser = null; // getUser = null; // getUser = null; try { getUser = userDAO.getByUsername(sessionUser.getUsername()); } catch (Exception e) { e.printStackTrace(); } if (getUser == null || ! Getuser.getpassword ().equals(sessionUser.getPassword())) {result.setresultFailed (" user information invalid! ") ); return result; } result.setresultSuccess (" User logged in! ") , getUser); return result; } /** * Modify user information ** @param user Modify user information object * @param request Request object, */ @postMapping ("/update") public Result<User> update(@requestBody User User, HttpServletRequest request) { Result<User> result = new Result<>(); HttpSession session = request.getSession(); User sessionUser = (User) session.getAttribute(SESSION_NAME); // Check whether the User in the session is consistent with the current User. if (sessionUser.getId() ! = user.getid ()) {result. SetResultFailed (" The current login user and the modified user are inconsistent, terminate! ") ); return result; } result = userService.update(user); If (result.isSuccess()) {session.setAttribute(SESSION_NAME, result.getData()); } return result; } /** * @param request */ @getMapping ("/logout") public Result logout(HttpServletRequest Request) {Result Result = new Result(); Request.getsession ().setAttribute(SESSION_NAME, null); Result. SetResultSuccess (" User logged out successfully! ") , null); return result; }}Copy the code

Since it is an API, @RestController is used to annotate the class. It can be seen that registration and login are requested by the front-end POST request and received by the back-end. In addition, the HttpServletRequest Request parameter is added to each method, and the session can be read and written through the request object. Each request has a unique session, and the information in each session is stored as a key-value. In this case, we only store the user information in the session, so we set the key of the user information to a fixed name, userInfo. By creating a constant SESSION_NAME.

Each request from a different machine generates a unique session. The above code operation can be understood as: after logging in/registering the user, we get the user information. We store the user information of each different machine in the corresponding session, and set the key of the user information as userInfo.

The HttpServletRequest getSession method is used to retrieve the session object of type HttpSession. The setAttribute method is used to set the key-value pairs in the session. The getAttribute method is used to obtain information about a session.

8. Configure cookie properties

In fact, by step 7 above, our function is basically complete, but there are still some important configuration to do.

We also need to enable the session function and configure cookie properties such as expiration time and so on.

Create a new package, config, and create the configuration class SessionConfig in it:

package com.example.userlogin.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.session.MapSessionRepository; import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; import org.springframework.session.web.http.CookieSerializer; import org.springframework.session.web.http.DefaultCookieSerializer; import java.util.concurrent.ConcurrentHashMap; /** * SessionConfig class */ @Configuration @enablesPringHttpSession public class SessionConfig {/** * Set cookie serializer properties */ @bean public CookieSerializer cookieSerializer() { DefaultCookieSerializer serializer = new DefaultCookieSerializer(); serializer.setCookieName("JSESSIONID"); / / configuration with regular expression matching domain name, can be compatible with localhost, various scenarios such as 127.0.0.1 serializer. SetDomainNamePattern (" ^. +? \.(\w+\.[a-z]+)$"); Serializer.setcookiepath ("/"); // Cookie takes effect path serializer.setCookiepath ("/"); / / set can only be amended server, the browser can't modify the serializer. SetUseHttpOnlyCookie (false); / / maximum life cycle of the unit is minutes serializer setCookieMaxAge (24 * 60 * 60); return serializer; } /** * register serializer */ @bean public MapSessionRepository() {return new MapSessionRepository(new) ConcurrentHashMap<>()); }}Copy the code

Note that the Configuration class is annotated @Configuration and @enablespringHttpSession is enabled.

9. Configure interceptors

Although we do have aN API for judging user logins, if we have a lot of pages and every one of them has to judge logins, it can be very cumbersome. Interceptor can be used to set the interception point on the specified path.

In the config package, create the interceptor class UserInterceptor:

package com.example.userlogin.config; import com.example.userlogin.api.UserAPI; import com.example.userlogin.dao.UserDAO; import com.example.userlogin.dataobject.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; /** * interceptor */ public class UserInterceptor implements HandlerInterceptor {@autowired private UserDAO UserDAO; @override public Boolean preHandle(HttpServletRequest Request, HttpServletResponse Response, Object handler) throws Exception { HttpSession session = request.getSession(); User sessionUser = (User) session.getAttribute(userapi.session_name); If (sessionUser == null) {response.sendredirect ("/"); return false; } User getUser = null; try { getUser = userDAO.getById(sessionUser.getId()); } catch (Exception e) { e.printStackTrace(); } / / if the user information is invalid in the session, then redirect to the home page if (getUser = = null | |! getUser.getPassword().equals(sessionUser.getPassword())) { response.sendRedirect("/"); return false; } return true; } @override public void postHandle(HttpServletRequest request, HttpServletResponse Response, Object handler, Throws Exception {} @override public void. // Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }Copy the code

You can see that the interceptor has three methods, one for each pointcut. In general, we only need to change the one before the Controller executes, which can operate on the session like the API, and return true to allow access to the Controller, or terminate access to the Controller. Redirects can be sent through the sendRedirect method of HttpServletResponse.

With the interceptor class written, it’s time to register the interceptors. Create the InterceptorRegister configuration class in the config class:

package com.example.userlogin.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.ArrayList; import java.util.List; @Configuration public class InterceptorRegister implements WebMvcConfigurer {/** * Register our interceptor class as a Bean */ @Bean public HandlerInterceptor getInterceptor() { return new UserInterceptor(); } /** * add interceptors, */ @override public void addInterceptors(InterceptorRegistry Registry) {List<String> pathPatterns = new ArrayList<>(); pathPatterns.add("/update"); registry.addInterceptor(getInterceptor()).addPathPatterns(pathPatterns); }}Copy the code

In the addInterceptors method, we register the interceptor and configure the interceptor address. The above example only adds the intercept/Update path and does not intercept the rest.

You can also set the addInterceptors to intercept all except /login by writing the addInterceptors method:

@Override
public void addInterceptors(InterceptorRegistry registry) {
   List<String> pathPatterns = new ArrayList<>();
   pathPatterns.add("/login");
   registry.addInterceptor(getInterceptor()).excludePathPatterns(pathPatterns);
}
Copy the code

Here, a relatively simple but complete user registration login is done!

10,

It may seem like a lot of writing to log in, but in fact the process is clear and easy to understand.

We find that the DAO layer simply operates on the database and returns user objects; The Service layer basically validates the information and returns a encapsulated Result object; The Controller layer further sets the session, which also returns the Result object encapsulated. Each of these different parts does its own job to complete our business logic.

Example repository address