1, the preface

In the project, the same account will sometimes log in from multiple places. In this case, it is necessary to limit an account to log in from only one client/browser, and send messages to the browser/client that is kicked out.

For example, after user A successfully logs in to browser A, user A logs in to the same account in browser B. In this case, the message “The current account has been logged in elsewhere” is displayed in browser A and user A logs out.

Message notification is based on Websocket, and the mutual kick function is based on Redis+Redisson message queue.

So how do you do that?

2. Implementation ideas

Train of thought

alerts

Message notification is based on WebSocket, the specific code can refer to the previous article: Spring Boot Hand teaching (17) : WebSocket analysis and how to access webSocket front and back end

Each play implementation

Redisson message queue

When a user logs in, an API authentication interceptor will be used. After passing the authentication, the current user information will be stored in redis with token as key. At the same time, a Redisson queue will be stored in Redis. The token generated by the same login account is stored in the queue. When the number of clients that can log in exceeds the limit, that is, the queue length, the kicking logic is performed.

For example

Account name: TEST Log in to browser A and browser B

When the user

When browser A logs in, the token is generated as Token_A after passing the API permission interceptor.

Then store it in Redis as a Token_A key and UserInfo value.

At the same time, a queue uni_token_deque_TEST is generated and stored in the current Token_A.

When browser B logs in, the token is generated as Token_B after passing the API permission interceptor.

Then store it in Redis as a Token_B key and UserInfo value.

At the same time, a queue uni_token_deque_TEST is generated and stored in the current Token_B.

Each subsequent client interface request passes through this layer of logic. If the current user queue uni_token_deque_TEST exceeds the specified length (limiting the number of clients to log in), browser A can execute related logic such as notification of kicking.

3. Redisson message queue

Redisson is a Java in-memory Data Grid based on Redis. Full use of Redis key value database to provide a series of advantages, based on the Common interface in the Java utility kit, for the user to provide a series of common tools with distributed characteristics. As a result, the toolkit which was originally used to coordinate single multithreaded concurrent program has obtained the ability of coordinating distributed multi-machine multithreaded concurrent system, which greatly reduces the difficulty of designing and developing large-scale distributed system. At the same time, it further simplifies the cooperation between programs in the distributed environment by combining various distributed services

Mevan

<! -- redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.10.7</version>
</dependency>
Copy the code

com.scaffold.test.config.RedissonConfig

package com.scaffold.test.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.codec.JsonJacksonCodec;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {

    /** * Single-machine mode */
    @Bean
    RedissonClient RedissonSingle(a) {
        Config config = new Config();
        config.setCodec(new JsonJacksonCodec())
                .useSingleServer()
                .setAddress("redis://localhost:6379");
        returnRedisson.create(config); }}Copy the code

Kick out the logic

/** * The user logs in only ** the queue is kicked out **@paramToken tokens * /
public Boolean kickOut(String token, HttpServletResponse response) {
    // Kick out the previously logged in user or the later logged in user
    boolean KICKOUT_AFTER = false;

    // The default maximum number of sessions for an account is 1
    int MAX_SESSION = 1;

    String PREFIX = "uni_token_";

    String PREFIX_LOCK = "uni_token_lock_";

    // Get user information from Redis
    RBucket<User> redisBucket = redissonClient.getBucket(token);
    User currentUser = redisBucket.get();
    String username = currentUser.getUserName();
    String userKey = PREFIX + "deque_" + username;

    / / lock
    String lockKey = PREFIX_LOCK + username;
    RLock lock = redissonClient.getLock(lockKey);
    lock.lock(2, TimeUnit.SECONDS);

    try {
        RDeque<String> deque = redissonClient.getDeque(userKey);
        // If the token does not exist in the queue and the user is not kicked out; In the queue
        if(! deque.contains(token) && ! currentUser.getKickout().equals(Boolean.TRUE)) {// Queue is first in, last out
            deque.push(token);
        }

        // If the number of session ids in the queue exceeds the maximum number of sessions, start kicking people
        while (deque.size() > MAX_SESSION) {

            String kickoutSessionId;
            if (KICKOUT_AFTER) {
                // Kick out the last one
                kickoutSessionId = deque.removeFirst();
            } else {
                // Kick out the first one
                kickoutSessionId = deque.removeLast();
            }

            try {
                RBucket<User> bucket = redissonClient.getBucket(kickoutSessionId);
                User kickOutUser = bucket.get();

                if(kickOutUser ! =null) {
                    // Set the kickout attribute of the session to indicate that it is kicked out
                    kickOutUser.setKickout(true);
                    bucket.set(kickOutUser);

                    // Get the updated data from Redis
                    currentUser = redisBucket.get();
                    token = kickoutSessionId;

                    // Push a message
                    Map<Object, Object> wsResult = new HashMap<>();
                    wsResult.put("message"."Your account has been logged in on another device.");
                    wsResult.put("code"."1001");
                    log.info("User kick notification"); WebSocketServer.sendInfo(JSONObject.toJSONString(wsResult), kickOutUser.getUuid()); }}catch(Exception e) { e.printStackTrace(); }}// If kicked out, prompt exit

        if (currentUser.getKickout()) {
            try {
                / / logout
                userService.logout(token);
                Result result = ResultGenerator.setFailResult(ResultCode.ALREADY_EXIST, "Your account has been logged in on another device.");
                response.getWriter().write(getJSONObject(result));
            } catch (Exception e) {
                e.printStackTrace();
            }
            return false; }}finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
            log.info("User" + currentUser.getUserName() + " unlock");

        } else {
            log.info("User" + currentUser.getUserName() + " already automatically release lock"); }}return true;

}
Copy the code

The interceptor

com.scaffold.test.config.interceptor.AuthenticationInterceptor

package com.scaffold.test.config.interceptor;

import com.alibaba.fastjson.JSONObject;
import com.scaffold.test.base.Result;
import com.scaffold.test.base.ResultCode;
import com.scaffold.test.base.ResultGenerator;
import com.scaffold.test.config.annotation.PassToken;
import com.scaffold.test.entity.User;
import com.scaffold.test.service.UserService;
import com.scaffold.test.utils.BaseUtils;
import com.scaffold.test.utils.JWTUtils;
import com.scaffold.test.websocket.WebSocketServer;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/** * interceptor **@author alex
 */


@Slf4j
public class AuthenticationInterceptor implements HandlerInterceptor {

    @Autowired
    UserService userService;

    @Autowired
    public RedissonClient redissonClient;

    @Autowired
    WebSocketServer webSocketServer;


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // token
        String token = BaseUtils.getToken();

        // If it is not a response method, static resources are released directly
        if(! (handlerinstanceof HandlerMethod)) {
            return true;
        }

        // pass with @passtoken
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        if (method.isAnnotationPresent(PassToken.class)) {
            PassToken annotation = method.getAnnotation(PassToken.class);
            if (annotation.required()) {
                return true; }}// Set the response format
        response.setContentType("application/json; charset=UTF-8");

        // Verify that the token is not empty
        if (token == null || token.equals("null")) {
            Result result = ResultGenerator.setFailResult(ResultCode.UNAUTHORIZED, "No token, please log in again");
            response.getWriter().write(getJSONObject(result));
            return false;
        }

        // Check if it exists in redis
        RBucket<User> bucket = redissonClient.getBucket(token);
        User rUser = bucket.get();
        if (rUser == null) {
            Result result = ResultGenerator.setFailResult(ResultCode.UNAUTHORIZED, "Invalid token, please log in again");
            response.getWriter().write(getJSONObject(result));
            return false;
        }

        // Verify that the TOKEN is valid
        String currentUserId = BaseUtils.getCurrentUserId();
        if (currentUserId == null || currentUserId.equals("null")) {
            Result result = ResultGenerator.setFailResult(ResultCode.UNAUTHORIZED, "Access exception, token incorrect, please log in again");
            response.getWriter().write(getJSONObject(result));
            return false;
        }

        // Verify that the user exists
        User userQuery = new User();
        userQuery.setUserId(currentUserId);
        User user = userService.findUser(userQuery);
        if (user == null) {
            Result result = ResultGenerator.setFailResult(ResultCode.UNAUTHORIZED, "User does not exist, token is incorrect, please log in again");
            response.getWriter().write(getJSONObject(result));
            return false;
        }

        // JWT validates again
        Boolean verify = JWTUtils.verify(token, user);
        if(! verify) { Result result = ResultGenerator.setFailResult(ResultCode.UNAUTHORIZED,"Illegal access, please log in again.");
            response.getWriter().write(getJSONObject(result));
            return false;
        }

        // Verify whether the user is logging in from multiple locations
        return kickOut(token, response);
    }


    /** * The user logs in only ** the queue is kicked out **@paramToken tokens * /
    public Boolean kickOut(String token, HttpServletResponse response) {
        / / play before login | | after login user default kicked out before the logged in user
        boolean KICKOUT_AFTER = false;

        // The default maximum number of sessions for an account is 1
        int MAX_SESSION = 1;

        String PREFIX = "uni_token_";

        String PREFIX_LOCK = "uni_token_lock_";

        // Get user information from Redis
        RBucket<User> redisBucket = redissonClient.getBucket(token);
        User currentUser = redisBucket.get();
        String username = currentUser.getUserName();
        String userKey = PREFIX + "deque_" + username;

        / / lock
        String lockKey = PREFIX_LOCK + username;
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(2, TimeUnit.SECONDS);

        try {
            RDeque<String> deque = redissonClient.getDeque(userKey);
            // If the token does not exist in the queue and the user is not kicked out; In the queue
            if(! deque.contains(token) && ! currentUser.getKickout().equals(Boolean.TRUE)) {// Queue is first in, last out
                deque.push(token);
            }

            // If the number of session ids in the queue exceeds the maximum number of sessions, start kicking people
            while (deque.size() > MAX_SESSION) {

                String kickoutSessionId;
                if (KICKOUT_AFTER) {
                    // Kick out the last one
                    kickoutSessionId = deque.removeFirst();
                } else {
                    // Kick out the first one
                    kickoutSessionId = deque.removeLast();
                }

                try {
                    RBucket<User> bucket = redissonClient.getBucket(kickoutSessionId);
                    User kickOutUser = bucket.get();

                    if(kickOutUser ! =null) {
                        // Set the kickout attribute of the session to indicate that it is kicked out
                        kickOutUser.setKickout(true);
                        bucket.set(kickOutUser);

                        // Get the updated data from Redis
                        currentUser = redisBucket.get();
                        token = kickoutSessionId;

                        // Push a message
                        Map<Object, Object> wsResult = new HashMap<>();
                        wsResult.put("message"."Your account has been logged in on another device.");
                        wsResult.put("code"."1001");
                        log.info("User kick notification"); WebSocketServer.sendInfo(JSONObject.toJSONString(wsResult), kickOutUser.getUuid()); }}catch(Exception e) { e.printStackTrace(); }}// If kicked out, prompt exit

            if (currentUser.getKickout()) {
                try {
                    / / logout
                    userService.logout(token);
                    Result result = ResultGenerator.setFailResult(ResultCode.ALREADY_EXIST, "Your account has been logged in on another device.");
                    response.getWriter().write(getJSONObject(result));
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return false; }}finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
                log.info("User" + currentUser.getUserName() + " unlock");

            } else {
                log.info("User" + currentUser.getUserName() + " already automatically release lock"); }}return true;

    }


    /** * Response result conversion format **@param result
     * @return* /
    private static String getJSONObject(Result result) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code", result.getCode());
        jsonObject.put("message", result.getMessage());
        returnjsonObject.toJSONString(); }}Copy the code

The API authorization interceptor has been explained in the previous article. If you don’t understand it, please refer to the previous article.

Spring Boot Teaching (15) : How do I enable login authentication interception and release

The login

com.scaffold.test.controller.UserController

 /** * login *@paramUser User information *@return Result
     */
    @PassToken
    @PostMapping("/login")
    public Result userLogin(User user) {
        // Verification code verification
        if(! userService.checkCode(user.getCode())) {return ResultGenerator.setFailResult("Login failed. Incorrect verification code");
        }
        User userInfo = userService.findUser(user);
        if(userInfo ! =null) {
            HashMap<Object, Object> result = new HashMap<>();
            String uuid = UUIDUtils.getUUID();
            String token = JWTUtils.createToken(userInfo);
            result.put("token", token);
            result.put("uuid", uuid);
            // Store to Redis
            RBucket<User> bucket = redissonClient.getBucket(token);
            user.setUserId(userInfo.getUserId());
            user.setUuid(uuid);
            bucket.set(user, JWTUtils.EXPIRATION_DATE, TimeUnit.SECONDS);

            return ResultGenerator.setSuccessResult(result);
        } else {
            return ResultGenerator.setFailResult("Login failed. Please check username and password."); }}/** * Get user information *@return Result
     */
    @GetMapping("/info")
    public Result getUserInfo(a){
        // Get from redis
        HttpServletRequest request = HttpUtils.getRequest();
        String token = request.getHeader("token");
        // The client needs the UUID generated by login to initiate a Websocket request, so it directly reads the redis cache data
        RBucket<User> bucket = redissonClient.getBucket(token);
        User currentUser = bucket.get();
        currentUser.setPassword(null);
        return ResultGenerator.setSuccessResult(currentUser);
    }

Copy the code

Each login of the same user will be a different UUID, which is used to distinguish different clients logged in by the same account. At that time, this UUID will also be used as the sessionID of websocket to initiate requests and get notifications. Please refer to the previous article Spring Boot Hand-in-hand Teaching (17) : WebSocket analysis and how to access webSocket at the front and back ends;

I set to jump to the home page to obtain user information after successful login, that is, to initiate the websocket when the /login/info request is initiated, so I need to read the user cache data of Redis.

4. Front-end implementation

src/main/resources/static/home.html

<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=0"> <title>home</title> <! <link rel="stylesheet" href=" CSS /regist.css"/> <link rel="stylesheet" Href = "https://res.wx.qq.com/open/libs/weui/2.1.3/weui.min.css" > < / head > < body > < div class = "container" > < div class = "page form_page js_show"> <div class="weui-form"> <div class="weui-form__text-area"> <h2 class="weui-form__title">Hello</h2> </div> <div id="result"></div> </div> </div> </div> </body> <script src="js/md5.js"></script> <script src="js/utils.js"></script> <script src="js/dataService.js"></script> <script type="text/javascript" SRC = "https://res.wx.qq.com/open/libs/weuijs/1.2.1/weui.min.js" > < / script > < script SRC = "js/home. Js" > < / script > < / HTML >Copy the code

src/main/resources/static/js/home.js

// Get relevant user information

const titleDom = document.querySelector(".weui-form__title");
const result = document.getElementById("result");

// getUserInfo
function getUserInfo() {
    dataService.getUserInfo().then(res= > {
        const {code, data} = res;
        if (code === 401) {
            location.href = location.origin + "/login.html";
            return;
        } else if (code == 1001) {
            alert("Your account is already logged in on another device.");
            location.href = location.origin + "/login.html";
            return;
        }
        if (data) {
            titleDom.innerHTML = 'Hello ' + data.userName + 'welcome to Dreamchaser';
            createSocket({
                sessionId: data.uuid,
                userId: data.userId
            })
        }

    })
}


/ / connection

let socket;

const createSocket = (params) = > {
    if (typeof WebSocket == 'undefined') {
        console.log("Browser does not support Websocket");
    } else {
        const paramsArr = [];
        Object.keys(params).forEach(m= > {
            paramsArr.push(`${m}=${params[m]}`);
        });
        const sessionId = params['sessionId'];
        const userId = params['userId'];
        let socketUrl = location.origin + "/message?" + paramsArr.join("&");
        socketUrl = socketUrl.replace(/http|https/g.'ws');
        console.log(socketUrl);
        if(socket ! =null) {
            socket.close();
            socket = null;
        }
        socket = new WebSocket(socketUrl);
        // Establish a connection
        socket.onopen = () = > {
            console.log("Establish a connection", sessionId);

            socket.send(JSON.stringify({
                sessionId: sessionId,
                query: 'onLineNumber'
            }));

        };
        // Get the message
        socket.onmessage = message= > {
            console.log(sessionId, message);
            const data = JSON.parse(message.data);
            if (data.code == 1001) {
                // Kick out the notification
                alert(data.message);
                location.href = location.origin + "/login.html";
            } else if (data.code == 200) { result.innerText = data.message; }}; }};// Close the connection
function closeWebSocket() {
    socket.close();
}

getUserInfo();
Copy the code

5, summary

This is the code logic kicked out for all user queues, only the main code logic is posted here.

Because the current code is not posted, most of the existing in the previous article, so, go to the corresponding article, to see the corresponding code.