I heard that wechat search “Java fish” will change strong oh!

This article is in Java Server, which contains my complete series of Java articles, can be read for study or interview

(1) Preface

In individual projects, people can be authenticated through cookies and sessions. However, with the development of distributed projects, session authentication in individual projects seems to become unavailable. In a cluster project, for example, we start multiple services, and the session exists in the JVM executing the current service, so authentication information that accesses the first node is not available in the second node.

So this article will take you through the handling of distributed sessions.

(2) Single Session authentication items

I will introduce today’s content through a project. In order to save space, I will replace the simple code with words, and the core code will be put up. If you need all the code in the running process, please reply in the comment section.

First of all, I simply set up a single environment under the personnel login authentication system.

2.1 Creating a SpringBoot project

Create a new SpringBoot project and introduce related dependencies:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<! -- Mybatis database dependencies -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
Copy the code

Configure the service port, database connection mode, and some path configuration for Mybatis in application.yml

server:
  port: 8189
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mycoding? serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
  type-aliases-package:
  mapper-locations: classpath:mapper/*.xml
Copy the code

2.2 Creating an Entity Class

Create a User entity class User that needs to be used:

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class User implements Serializable {
    private String username;
    private String password;
    private String levelId;
    private String nickname;
    private String phone;
    private int status;
    private Timestamp createTime;
}
Copy the code

Add a database generation statement to the user table:

CREATE TABLE `user` (
  `id` int(40) NOT NULL AUTO_INCREMENT,
  `level_id` int(4) NOT NULL,
  `username` varchar(100) NOT NULL,
  `password` varchar(100) NOT NULL,
  `nickname` varchar(100) NOT NULL,
  `phone` varchar(100) NOT NULL,
  `status` int(4) NOT NULL DEFAULT '1',
  `create_time` datetime DEFAULT NULL.PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
Copy the code

2.3 Writing login logic

First, the front end sends the user name and password to the back end through POST request. First, it determines whether the user name and password match the database. If so, it inserts the current user information into the session and returns the result of successful login; otherwise, it returns the result of unsuccessful login.

First we write a BaseController to get the basic request and response information

public class BaseController {

    public HttpServletRequest getRequest(a){
        return ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
    }

    public HttpServletResponse getResponse(a){
        return ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getResponse();
    }

    public HttpSession getHttpSession(a){
        returngetRequest().getSession(); }}Copy the code

Write UserController to inherit from BaseController and implement login logic here.

@RestController
@RequestMapping("/sso")
public class UserController extends BaseController {

    @Autowired
    private UserService userService;

    @PostMapping("/login")
    public CommonResult login(@RequestParam String username,@RequestParam String password){
        // Check whether the user exists in the database
        User user = userService.login(username, password);
        // If yes
        if(user! =null){
            getHttpSession().setAttribute("user",user);
            return new CommonResult(ResponseCode.SUCCESS.getCode(),ResponseCode.SUCCESS.getMsg(),username+"Successful landing.");
        }
        return new CommonResult(ResponseCode.USER_NOT_EXISTS.getCode(),ResponseCode.USER_NOT_EXISTS.getMsg(),""); }}Copy the code

2.4 Interceptor Intercepts the unlogged state

How to determine whether the user has logged in? We need to intercept all requests except/SSO. Create an IntercepterConfiguration class that intercepts all requests except/SSO.

@Configuration
public class IntercepterConfiguration implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        List list=new ArrayList();
        list.add("/sso/**");
        registry.addInterceptor(authInterceptorHandler())
                .addPathPatterns("/ * *")
                .excludePathPatterns(list);
    }

    @Bean
    public AuthInterceptorHandler authInterceptorHandler(a){
        return newAuthInterceptorHandler(); }}Copy the code

With the interceptor, we also need to process the intercepted request. The processing method is to verify whether the current session can be found. If there is a session, we will allow it, indicating that the login has been done.

@Slf4j
public class AuthInterceptorHandler implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("Enter interceptor.");
        if(! ObjectUtils.isEmpty(request.getSession().getAttribute("user"))) {return true;
        }
        response.setHeader("Content-Type"."application/json");
        response.setCharacterEncoding("UTF-8");
        String result = new ObjectMapper().writeValueAsString(new CommonResult(ResponseCode.NEED_LOGIN.getCode(), ResponseCode.NEED_LOGIN.getMsg(), ""));
        response.getWriter().println(result);
        return false; }}Copy the code

2.5 validation

To test creating a new IndexController, get the user name from session and return it.

@RestController
public class IndexController extends BaseController{

    @RequestMapping(value = "/index",method = RequestMethod.GET)
    public CommonResult index(a){
        User user = (User) getHttpSession().getAttribute("user");
        return newCommonResult(ResponseCode.SUCCESS.getCode(),ResponseCode.SUCCESS.getMsg(),user.getUsername()); }}Copy the code

To start the project, directly access localhost: port /index in Postman:

Because not login, was blocked, next login first:

Localhost: port /index:

At this point, a single login authentication service is actually complete.

(c) What if the project becomes cluster deployment?

When the user usage is slowly changed more, found that the server can not support fast, so the leadership of a command, do cluster! Then simulate a cluster (two nodes to test), check the Idea configuration to allow multiple applications to run:

Then start the project, modify the server.port port, and then start the project

I started two projects running on ports 8188 and 8189, which required nginx load balancing to polling each request to access two nodes. I’ll omit the manual access to simulate Nginx here.

At this time, there was a problem. After I logged in on port 8188, the authentication was successful, but once the request was sent to port 8189, I needed to authenticate again. Two servers is fine. If there are dozens of servers, users need to log in dozens of times. It doesn’t make sense.

(4) Distributed session

There are many ways to solve the above problem:

1. For example, on nginx, if the request policy is changed to IP matching policy, one IP will only access one node (usually not adopted).

2, for example, set up a unified authentication service, all requests go to the unified authentication (CAS, etc.). The unified authentication method of CAS is used in the project I am doing now, but it is rather complicated, so I will not introduce it here.

3. Today we introduce a convenient and practical way to implement distributed Session–SpringSession.

The principle behind SpringSessions is simple: Instead of storing sessions in the JVM, they are stored in a public place. Such as mysql, Redis. It’s obviously the most efficient in Redis.

4.1 Importing Dependencies

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Copy the code

Because you are using Redis, you need to introduce redis dependencies as well.

4.2 configure redis

Or the generic Redis configuration class, to ensure that the serialization of the transfer, this section of the generic, directly copy past good.

@Configuration
public class RedisConfiguration {
    // A custom redistemplate
    @Bean(name = "redisTemplate")
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
        // Create a RedisTemplate Object that returns key string and value Object for convenience
        RedisTemplate<String,Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        // Set the JSON serialization configuration
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer=new
                Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper=new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance);
        // Serialization of string
        StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
        // Key uses string serialization
        template.setKeySerializer(stringRedisSerializer);
        //value uses Jackson's serialization
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // Hashkey uses string serialization
        template.setHashKeySerializer(stringRedisSerializer);
        // HashValue uses Jackson's serialization
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        returntemplate; }}Copy the code

4.3 Configuring SpringSession in the Configuration File

Add session storage mode and REDis IP port to application.yml:

spring:
  redis:
    host: 192.16878.128.
    port: 6379
  session:
    store-type: redis
Copy the code

4.4 open springsession

Create a new configuration class RedisHttpSessionConfiguration, and set a maximum time of survival, and here is set to 1 hour

@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600)
public class RedisHttpSessionConfiguration {}Copy the code

4.5 validation

With a single login, you can now access the corresponding INDEX interface directly on both clusters. And you can already see the session we plugged in in Redis.

(5) Summary

With current technology, there are many ways to implement distributed sessions. Basically, the idea is to store session data in a unified location. See you next time!