springBoot+springDataJpa+Redis+JWT realize login_ ZL

Posted by wrathyimp on Sun, 19 Dec 2021 20:54:33 +0100

preface

Personally, I think the configuration of mainstream Spring Boot and Spring MVC is cumbersome. Only some older projects are used, or government agencies will use it. However, Spring Boot convention is greater than configuration. Now microservice technology is common, and the use of Spring Cloud is based on Spring Boot.

Why use JWT Token instead of Session?

When the user logs in successfully for the first time, the Session will be passed in to the server and a SessionId will be returned to the client. The client will save the SessionId in the Cookie. The user requests the server again with the SessionId, and the server will look for the Session with the SessionId. The Session is stored in memory. If the service volume is large, it will occupy a lot of memory, and the expansibility of the Session is not strong.

The Token is like an ID card. After the user successfully logs in to the server for the first time, a unique Token string will be returned. The client will save it to the cache and request to carry the Token again. The server will parse the Token to obtain the user information and query it in the database. SSO single sign on in the same domain or across domains is basically realized through Token, which has strong expansibility. As just said, the Token must check the database every time it is parsed. If the service volume is particularly large and concurrent, the database will be accessed frequently, which is easy to cause database connection interruption or waiting state. At this time, we only need to store the Token in redis. Each request will only judge whether this Token exists in redis. Redis is single threaded, which can effectively control concurrency, and the processing level of redis is millisecond. Let's get to the point.

1, Use IDEA to build Spring Boot project and integrate Spring Data JPA

 

1. New project

Note here that Spring Web and Spring Data JPA must be checked for a new project. If it is done in an existing project, you can add dependencies.

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--Spring Data JPA rely on-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

2. Introduce MySQL dependency

        <!--Mysql rely on-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.42</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!--JSON tool-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>

Our information is added to the database, so here we add the dependency between MySQL and Alibaba Druid connection pool. JSON tool will be used below, and we add it in advance.

3. application.properties configuration

#Port number

server.port=8011

#mysql driver

spring.datasource.driver-class-name=com.mysql.jdbc.Driver

#mysql address

spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8

#mysql user name

spring.datasource.username=root

#mysql password

spring.datasource.password=123456

#JPA connection to mysql

spring.jpa.database=mysql

#Run display sql

spring.jpa.show-sql=true

spring.jpa.database represents the database type to which JPA will connect

2, Describes using Spring Data JPA

JPA is the abbreviation of Java Persistence API and its Chinese name is java persistence layer API. It is the mapping relationship between JDK 5.0 annotation or XML description object relational table, and persistes the entity object at run time to the database. Sun introduced the new JPA ORM specification for two reasons: first, simplify the existing Java EE and Java SE application development; Second, sun hopes to integrate ORM technology to realize the unification of the world.

1. Create a new table user

CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT,-- Primary key ID
  `username` varchar(100) DEFAULT NULL,-- user name
  `password` varchar(100) DEFAULT NULL,-- password
  `realname` varchar(100) DEFAULT NULL,-- Real name
  `create_time` datetime NOT NULL,-- Creation time
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8

2. Create a new UserEntity

/**
  * user
  * @date 2019/10/24 11:35
  */
@Entity
@Table(name = "user", schema = "test", catalog = "")
public class UserEntity {
    
    private int id;
    private String username;
    private String password;
    private String realname;
    private Date createTime;

    @Id
    @Column(name = "id")
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Basic
    @Column(name = "username")
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Basic
    @Column(name = "password")
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Basic
    @Column(name = "realname")
    public String getRealname() {
        return realname;
    }

    public void setRealname(String realname) {
        this.realname = realname;
    }

    @Basic
    @Column(name = "create_time")
    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date date) {
        this.createTime = date;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        UserEntity that = (UserEntity) o;
        return id == that.id &&
                Objects.equals(username, that.username) &&
                Objects.equals(password, that.password) &&
                Objects.equals(realname, that.realname) &&
                Objects.equals(createTime, that.createTime);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, username, password, realname, createTime);
    }
}
  • @Entity: represents an entity that needs to be automatically scanned by Spring.
  • @Table(name = "user", schema = "test", catalog = ""): name corresponds to the name of the table in the database and the schema of the schema table. We can fill in the database name. The directory of the catalog table is not filled in by default. Generally, we can directly use @ Table(name = "user").
  • @Column: corresponds to the field name in the database.
  • @ID: indicates the primary key ID.

There is a method to automatically generate JPAEntity on the Internet. We Baidu ourselves.

3. Create a new UserRepository interface and inherit the JpaRepository

public interface UserRepository extends JpaRepository<UserEntity, Integer> {

}

  • UserEntity: corresponding entity. If you have other entities, fill in your corresponding entity, or fill in Map.
  • Integer: database primary key type.

 

 

JpaRepository inherits the interfaces PagingAndSortingRepository and QueryByExampleExecutor, while PagingAndSortingRepository inherits CrudRepository, so the JpaRepository interface has basic addition, deletion, modification, query and paging at the same time. Therefore, we only need to inherit the JpaRepository.

4. Create a new UserController (we don't write the Service layer here for convenience) and inject the UserRepository

The detailed code is as follows:

@RestController
@RequestMapping("/api/system")
public class UserController {

    @Autowired
    private UserRepository userRepository;
}

@RestController is equivalent to the combination of @ Controller and @ ResponseBody. It cannot return pages, and the view parser cannot parse JSP and HTML pages.

@RequestMapping maps the request path.

@Usage rules of Autowired:

  • There are candidate beans of this type in the container
  • The container can contain multiple candidate beans of this type
  • Spring 3. Before x, there could only be one Bean in the spring container, and multiple beans reported an exception, BeanCreationException
  • Spring 3. After X, there can be multiple beans. When using @ Autowired, the variable name must be the same as one of the multiple beans of this type (that is, @ Autowired private student; in the above, student is the id of one of the multiple beans)
  • If Rule 4 is violated, a BeanCreationException will be thrown

https://cloud.tencent.com/developer/article/1479065

5. Create a new return tool class ResultUtils

/**
  * Define return tool class
  * @date 2019/10/24 11:58
  */
public class ResultUtils {

    /**
    * Successful return
    * @param data
    * @param msg
    * @return
    */
    public static Object success(Object data,String msg){
        Result result = new Result();
        result.setState(true);
        result.setData(data);
        result.setMsg(msg);
        return result;
    }

    public static Object success(String msg){
        Result result = new Result();
        result.setState(true);
        result.setMsg(msg);
        return result;
    }

    public static Object success(Object data){
        Result result = new Result();
        result.setState(true);
        result.setData(data);
        return result;
    }

    public static Object success(){
        Result result = new Result();
        result.setState(true);
        return result;
    }

    /**
    * Error return
    * @return
    */
    public static Object error(){
        Result result = new Result();
        result.setState(false);
        return result;
    }

    public static Object error(String msg){
        Result result = new Result();
        result.setState(false);
        result.setMsg(msg);
        return result;
    }
}
public static class Result{
        private boolean state;//Return status
        private Object data;//Return data
        private String msg;//Return information
        public boolean isState() {
            return state;
        }

        public void setState(boolean state) {
            this.state = state;
        }

        public Object getData() {
            return data;
        }

        public void setData(Object data) {
            this.data = data;
        }

        public String getMsg() {
            return msg;
        }

        public void setMsg(String msg) {
            this.msg = msg;
        }
}

This is my complete directory structure:

 

6. Add the annotation @ EntityScan("com.logindemo.logindemo.system.entity") in the startup class LogindemoApplication

@Entity scan scans the entities under the specified package when Spring starts. According to my experience, if it is not added, it may return the injection exception. If there are many services, the packages are separated and can be used* Instead.

For example, @ EntityScan("com.logindemo.logindemo. *") is used to scan all entities under logindemo if we have more than system and many other directories.

7. Write a new user method in UserController. The saveUser code is as follows:

    /**
     * New user
     * @return
     */
    @RequestMapping("/saveUser")
    public Object saveUser(){
        UserEntity userEntity = new UserEntity();
        //user name
        userEntity.setUsername("admin");
        //Password note: use Spring Boot's own encryption to encrypt the password
        userEntity.setPassword( DigestUtils.md5DigestAsHex("123456".getBytes()));
        //Real name
        userEntity.setRealname("administrators");
        //Creation time
        userEntity.setCreateTime(new Date());
        //To add a new user, call the Spring Data JPA's own method to add a new user
        UserEntity save = userRepository.save(userEntity);
        //If it is not equal to null, return the tool class we just defined
        if (save!=null){
            return ResultUtils.success("Operation succeeded");
        }
        return ResultUtils.error("operation failed");
    }

Request using browser http://localhost:8011/api/system/saveUser , the browser response returns the message:

 

Here is a simple demonstration, which will explain the user-defined query when used later.

3, Integrated JWT

1. Introduce JWT dependency
 

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

https://baijiahao.baidu.com/s?id=1608021814182894637&wfr=spider&for=pc

2. What is a Token

Token means "token". It is a string generated by the server as an identification of the client's request.

Composition of simple Token; uid (user's unique identity), time (timestamp of current time), sign (signature).

3. Write a Token generation tool class and create a new JwtUtils tool class, as follows:

package com.logindemo.logindemo.utils;

import com.logindemo.logindemo.system.entity.UserEntity;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
/**
 * jwt Tool class
 * @date 2019/10/24 9:38
 */
public class JwtUtils {
    public static final String SUBJECT="test";//Signature publisher
    public static final String APPSECRET="logindemo";//autograph

    /**
     * Generate token
     * @param userEntity
     * @return
     */
    public static String genJsonWebToken(UserEntity userEntity){
        String token="";
        if (userEntity!=null){
            token=Jwts.builder().setSubject(SUBJECT)//publisher
                    .claim("username",userEntity.getUsername())
                    .claim("realName",userEntity.getRealname())
                    .setIssuedAt(new Date())//Issue date
                    .signWith(SignatureAlgorithm.HS256,APPSECRET).compact();//autograph
        } else {
            token="";
        }
        return token;
    }
}





Later, we will use Redis to verify the Token instead of JWT's own verification method.

4. Test to generate Token

public static void main(String[] args) {
    UserEntity userEntity = new UserEntity();
    userEntity.setUsername("admin");
    userEntity.setRealname("administrators");
    System.out.println(genJsonWebToken(userEntity));
    //Input content: eyjhbgcioijiuzi1nij9 eyJzdWIiOiJ0ZXN0IiwidXNlcm5hbWUiOiJhZG1pbiIsInJlYWxOYW1lIjoi566h55CG5ZGYIiwiaWF0IjoxNTcxODkyNDU0fQ. S7pMyYZ_ D_ MSa-JcSKNRTc08bbAsEc8y7vSwgMktp2s
}

Token:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0IiwidXNlcm5hbWUiOiJhZG1pbiIsInJlYWxOYW1lIjoi566h55CG5ZGYIiwiaWF0IjoxNTcxODkyNDU0fQ.S7pMyYZ_D_MSa-JcSKNRTc08bbAsEc8y7vSwgMktp2s

The above is the Token we generated.

A Token is a string of strings generated by the server as a Token requested by the client. After the first login, the server generates a Token and returns the Token to the client. Later, the client only needs to bring the Token to request data without bringing the user name and password again.

4, Integrate Redis (Redis service needs to be built by yourself)

Redis installation reference under Windows:

https://www.cnblogs.com/W-Yentl/p/7831671.html

Redis installation under Linux reference:

https://www.cnblogs.com/zuidongfeng/p/8032505.html

1. Introduce Redis dependency

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

2. application. Redis properties configuration

#Redis

#Redis address

spring.redis.host=39.96.167.196

#Redis port

spring.redis.port=6379

#Redis password (can not be written if it is not set)

spring.redis.password=******

#Redis timeout

spring.redis.timeout=10000

#Redis database index (0 by default)

spring.redis.database=0

3. Write Redis tool class

package com.logindemo.logindemo.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

/**
 * Redis Tool class
 * @date 2019/10/24 12:58
 */
@Component
public class RedisUtils {

    @Autowired
    public StringRedisTemplate redisTemplate;

    public static RedisUtils redisUtils;

    /**
     * This method is loaded only once when Spring starts
     */
    @PostConstruct
    public void init(){
        redisUtils=this;
        redisUtils.redisTemplate=this.redisTemplate;
    }

    /**
     *redis Save data and set cache time
     * @param key key
     * @param value value
     * @param time second
     */
    public void set(String key,String value,long time){
        redisUtils.redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
    }


    /**
     * redis Store data
     * @param key key
     * @param value value
     */
    public void set(String key,String value){
        redisUtils.redisTemplate.opsForValue().set(key, value);
    }

    /**
     * Get expiration time according to key
     * @param key
     * @return
     */
    public Long getExpire(String key){
        return redisUtils.redisTemplate.getExpire(key,TimeUnit.SECONDS);
    }

    /**
     * Determine whether the key exists
     * @param key
     * @return
     */
    public Boolean hasKey(String key){
        return redisUtils.redisTemplate.hasKey(key);
    }

    /**
     * Set the expiration time according to the key
     * @param key key
     * @param time second
     * @return
     */
    public Boolean expire(String key,long time){
        return redisUtils.redisTemplate.expire(key,time , TimeUnit.SECONDS);
    }
}

5, Write login interface in UserController

1. Inject RedisUtils into UserController

    @Autowired
    private RedisUtils redisUtils;

Here, we can directly use @ Autowired auto injection.

2. UserRepository method

    /**
     * Query whether the user exists according to the user name
     * @param username
     * @return
     */
    int countByUsername(String username);

    /**
     * Query password according to user name
     * @param username
     * @return
     */
    @Query(value = "select * from user where username=?1",nativeQuery = true)
    UserEntity getPassword(String username);

Introduction to custom writing:

  • The countByUsername method is a dynamic SQL query of Spring Data JPA.
  • count stands for quantity. ByUsername is queried according to user name. Note that the initial letter of the condition must be capitalized. The overall meaning is to query quantity according to user name.
  • countByUsername is the same as select count (1) from user where username='admin '.

Example: query the number of users according to user name and password

int countByUsernameAndPassword(String username,String password);

-m if there are still conditions, you can continue to splice And. If the relationship of "Or" is used, the initial letter of Or must also be capitalized

  • If you need to query, the user needs to use find, which is similar to count. You only need to replace count with find

Description of @ Query annotation:

@Query(value = "select * from user where username=?1",nativeQuery = true)
UserEntity getPassword(String username);
  • nativeQuery = true ` indicates that the native SQL query is used
  • ? 1 ¢ represents the first parameter. If there is a password besides username, the writing method is as follows:
@Query(value = "select * from user where username=?1 and password=?2",nativeQuery = true)
UserEntity getPassword(String username,String password);

Write @ Param as follows:

@Query(value = "select * from user where username=:username and password=:password",nativeQuery = true)

UserEntity getPassword(@Param("username") String username,@Param("password") String password);

The above two writing methods depend on your personal preferences.

For naming rules of Spring Data JPA methods, please refer to:

https://blog.csdn.net/qq8057656qq/article/details/86744132

3. Login interface code

    /**
     * Login interface
     * @param request
     * @return
     */
    @PostMapping("/login")
    public Object login(@RequestBody String request){
        JSONObject jsonObject = JSON.parseObject(request);//Convert to JSON object
        if (jsonObject.get("username")==null&&"".equals(jsonObject.get("username").toString())){
            return ResultUtils.error("User name cannot be empty");
        }
        if (jsonObject.get("password")==null&&"".equals(jsonObject.get("password").toString())){
            return ResultUtils.error("User name cannot be empty");
        }
        String username = jsonObject.get("username").toString();
        String password = jsonObject.get("password").toString();
        if (userRepository.countByUsername(username)>0){//Determine whether the user exists
            UserEntity userEntity = userRepository.getPassword(username);//Password in database
            if (DigestUtils.md5DigestAsHex(password.getBytes()).equals(userEntity.getPassword())){//Verify that the passwords are consistent
                String token = JwtUtils.genJsonWebToken(userEntity);//Get Token
                redisUtils.set(token,userEntity.getRealname(),60);//After successful login, put the token into Redis Key to save the token, and value to save the user's real name
                //After successful login, return the token and real name
                Map<String,Object> map = new HashMap<>();
                map.put("realname",userEntity.getRealname());
                map.put("token",token);
                return ResultUtils.success(map,"Login succeeded");//Login succeeded
            }
            return ResultUtils.error("Password error, please re-enter");
        } else {
            return ResultUtils.error("user name does not exist");
        }

    }

Because we use the @ PostMapping annotation, we can't use the browser to request directly in the later test (the browser request uses the GET request).

If you don't understand the @ RequestBody annotation, you can refer to it

https://www.jianshu.com/p/c1b8315c5a03

There are many tools for converting JSON strings to JSON objects. Here we use Alibaba's.

General process:

 

4. Use Postman to request the address

http://localhost:8011/api/system/login

Request JSON:

{

"username":"admin",

"password":"123456"

}

Return data:

{

"state": true,

"data": "login succeeded",

"msg": "Administrator"

}

6, Writing Spring Boot interceptors

1. Create a new package config and a new class WebSecurityConfig to inherit WebMvcConfigurationSupport

2. Add @ Configuration annotation to websecurityconfig

@The Configuration annotation on the class is equivalent to taking the class as the XML Configuration file of Spring. Its function is to configure the Spring container (application context).

3. Implement addInterceptors method and configure user-defined interception

    /**
     * Configure custom interceptors
     * @param registry
     */
    public void  addInterceptors(InterceptorRegistry registry){
        InterceptorRegistration addInterceptor = registry.addInterceptor(getSecurityInterceptor());
        List<String> list = new ArrayList<>();
        list.add("/api/system/saveUser");//Release new user interface address
        list.add("/api/system/login");//Release login interface address
        addInterceptor.excludePathPatterns(list);
        addInterceptor.addPathPatterns("/**");//Block all requests
    }

4. Create a new internal class SecurityInterceptor in WebSecurityConfig, inherit the HandlerInterceptorAdapter, and implement the preHandle method

  • preHandle call time: the Controller method returns true and false before processing. If false is returned, the execution will be interrupted, otherwise the execution will continue. In addition to preHandle, there are postHandle and afterCompletion methods. postHandle is called after the Controller method is processed and before rendering the view.
  • afterCompletion is invoked after rendering the view.

Here we only implement the preHandle method.

5. Inject SecurityInterceptor into WebSecurityConfig and add annotation @ Bean to the method

Note: @ Bean should be used with @ Configuration (you don't need to add @ Configuration when using the startup class).

6. Inject RedisUtils tool class into WebSecurityConfig class and improve preHandle method

The overall code is as follows:

package com.logindemo.logindemo.config;

import com.logindemo.logindemo.utils.RedisUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.List;

/**
 * Login interception configuration
 */
@Configuration
public class WebSecurityConfig extends WebMvcConfigurationSupport {

    @Autowired
    private RedisUtils redisUtils;

    /**
     * Inject Bean to let Spring scan SecurityInterceptor
     * Otherwise, the filter will not work
     * @return
     */
    @Bean
    public SecurityInterceptor getSecurityInterceptor(){
        return new SecurityInterceptor();
    }

    /**
     * Configure custom interceptors
     * @param registry
     */
    public void  addInterceptors(InterceptorRegistry registry){
        InterceptorRegistration addInterceptor = registry.addInterceptor(getSecurityInterceptor());
        List<String> list = new ArrayList<>();
        list.add("/api/system/saveUser");//Release new user interface address
        list.add("/api/system/login");//Release login interface address
        addInterceptor.excludePathPatterns(list);
        addInterceptor.addPathPatterns("/**");//Block all requests
    }

    private class SecurityInterceptor extends HandlerInterceptorAdapter {

        /**
         * Called before the business processor processes the request

         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
            ServletOutputStream out = response.getOutputStream();//Create an output stream
            OutputStreamWriter ow=new OutputStreamWriter(out,"utf-8");//Set the coding format to prevent Chinese characters from being garbled
            String token = request.getHeader("token");//Get Token
            if(token!=null){//Judge whether the Token is empty
                if (redisUtils.hasKey(token)){//Judge whether the Token exists
                    redisUtils.expire(token,60); //If the Token exists, re assign the expiration time and release it
                    return true;
                }
                ow.write("token Error, please login again");//Information to return
                ow.flush();//Flush out the flow and send all buffered data to the destination
                ow.close();//Close flow
                return false;//intercept
            }
            ow.write("token Null, please login again");//Information to return
            ow.flush();//Flush out the flow and send all buffered data to the destination
            ow.close();//Close flow
            return false;//intercept
        }
    }
}

When sending a request, the user will first enter the addInterceptors method. If there is a path to be intercepted, the user will enter the preHandle method. Pass request. In the preHandle method Getheader ("Token") gets the Token (the Token will be put into the request header during the test). If there is no direct release in addInterceptors, the request path will not enter the preHandle method.

Here, we can echo the error message to the user through the output stream.

7. To facilitate viewing the effect, add the findAll method in UserController to return all users

    /**
     * Get all users
     * @return
     */
    @RequestMapping("/findAll")
    public Object findAll(){
        List<UserEntity> all = userRepository.findAll();
        for (UserEntity temp:all) {
            temp.setPassword(null);//Filter passwords
        }
        return all;
    }

Here, we cycle all user passwords to empty. No matter what business it is, as long as the information echoed to the user, the password is generally not displayed

7, Use Postman test

1. First, we directly access the findAll method

 

Prompt Token is empty, please login again.

2. Call the login interface to log in

 

 

3. Fill the returned Token into the Headers of findAll address

 

4. Re request findAll method

 

We can see that our request has been successful!