The advantage of using JWT is that the server does not need to maintain and store the state of tokens. The server only needs to verify that the Token is valid. It does save a lot of work. However, the disadvantages are also obvious. The server cannot actively invalidate a Token, and the Token cannot be modified after an expiring time is specified.

With Redis, the above two problems can be easily solved

  • tokenThe renewal of
  • Server active failure specifiedtoken

Next, I’ll show you an implementation Demo

Initialize a project

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.3.2. RELEASE</version>
</parent>

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
	
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
		<exclusions>
			<exclusion>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-starter-tomcat</artifactId>
			</exclusion>
		</exclusions>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-freemarker</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-websocket</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-undertow</artifactId>
	</dependency>
	<! -- redis -->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-redis</artifactId>
	</dependency>
	<dependency>
		<groupId>org.apache.commons</groupId>
		<artifactId>commons-pool2</artifactId>
	</dependency>
	<! -- jwt -->
	<dependency>
		<groupId>com.auth0</groupId>
		<artifactId>java-jwt</artifactId>
		<version>3.10.3</version>
	</dependency>
</dependencies>

<build>
	<plugins>
		<plugin>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-maven-plugin</artifactId>
			<configuration>
				<executable>true</executable>
			</configuration>
		</plugin>
	</plugins>
</build>
Copy the code

Core configuration

spring:
  redis:
    database: 0
    host: 127.0. 01.
    port: 6379
    timeout: 2000
    lettuce:
      pool:
        max-active: 8
        max-wait: - 1
        max-idle: 8
        min-idle: 0

jwt:
  key: "springboot"
Copy the code

Redis configuration, we are familiar with. Jwt. key is a user-defined configuration item that configures the key used by JWT for signature.

How tokens are stored in Redis

The user’s Token needs to be cached in Redis and a custom object is used to describe the information. And the JDK serialization is used here. Not JSON.

Create a description object for the Token: UserToken

import java.io.Serializable;
import java.time.LocalDateTime;

public class UserToken implements Serializable {

	private static final long serialVersionUID = 8798594496773855969L;
	// token id
	private String id;
	/ / user id
	private Integer userId;
	// ip
	private String ip;
	/ / the client
	private String userAgent;
	// Authorization time
	private LocalDateTime issuedAt;
	// Expiration time
	private LocalDateTime expiresAt;
	// Remember me
	private boolean remember;
    / / ignore getter/setter
}
Copy the code

Create: ObjectRedisTemplate

import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

public class ObjectRedisTemplate extends RedisTemplate<String.Object> {
	
	public ObjectRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
		
		this.setConnectionFactory(redisConnectionFactory);
		
		this.setKeySerializer(StringRedisSerializer.UTF_8);
		this.setValueSerializer(newJdkSerializationRedisSerializer()); }}Copy the code

Need to configure to IOC

import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

public class ObjectRedisTemplate extends RedisTemplate<String.Object> {
	
	public ObjectRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
		this.setConnectionFactory(redisConnectionFactory);
		// key uses a string
		this.setKeySerializer(StringRedisSerializer.UTF_8);
		// value uses JDK serialization
		this.setValueSerializer(newJdkSerializationRedisSerializer()); }}Copy the code

There’s not much Redis involved here, but if you’re not familiar with it, just remember that the value of ObjectRedisTemplate is the Java object stored.

Implementation logic for login

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;

import io.springboot.jwt.domain.User;
import io.springboot.jwt.redis.ObjectRedisTemplate;
import io.springboot.jwt.web.support.UserToken;

@RestController
@RequestMapping("/login")
public class LoginController {
	
	@Autowired
	private ObjectRedisTemplate objectRedisTemplate;
	
	@Value("${jwt.key}")
	private String jwtKey;		// Read the JWT key from the configuration

	@PostMapping
	public Object login(HttpServletRequest request,
						HttpServletResponse response,
						@RequestParam("account") String account,
						@RequestParam("password") String password,
						@RequestParam(value = "remember", required = false) boolean remember) {

		/** * Ignore the authentication logic and assume that the user is already logged in successfully and his ID is 1 */
		User user = new User(); 
		user.setId(1);
		
		/** * Login information */
		String ip = request.getRemoteAddr();							// Client IP address (if it is a reverse proxy, obtain the actual IP address based on the situation)
    	String userAgent = request.getHeader(HttpHeaders.USER_AGENT);	// UserAgent
    	
    	// Login time
    	LocalDateTime issuedAt = LocalDateTime.now();
    	
    	
    	// The Token is valid for 7 days if it is "remember me" and half an hour if it is not
    	LocalDateTime expiresAt = issuedAt.plusSeconds(remember 
    								? TimeUnit.DAYS.toSeconds(7)
    								: TimeUnit.MINUTES.toSeconds(30));
    	
    	// The number of seconds left before the expiration time
    	int expiresSeconds = (int) Duration.between(issuedAt, expiresAt).getSeconds();
    	
    	/** * Store Session */
    	UserToken userToken = new UserToken();
    	// Randomly generate uuid as token ID
    	userToken.setId(UUID.randomUUID().toString().replace("-".""));
    	userToken.setUserId(user.getId());
    	userToken.setIssuedAt(issuedAt);
    	userToken.setExpiresAt(expiresAt);
    	userToken.setRemember(remember);
    	userToken.setUserAgent(userAgent);
    	userToken.setIp(ip);
    	
    	// Serialize Token objects to Redis
    	this.objectRedisTemplate.opsForValue().set("token:" + user.getId(), userToken, expiresSeconds, TimeUnit.SECONDS);
    	
    	/** * Generates Token information */
    	Map<String, Object> jwtHeader = new HashMap <>();
    	jwtHeader.put("alg"."alg");
    	jwtHeader.put("JWT"."JWT");
    	String token = JWT.create()
    			.withHeader(jwtHeader)
    			// Write the user id to the token
    			.withClaim("id", user.getId())
    			.withIssuedAt(new Date(issuedAt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()))
    			/** * The expiration time is maintained by Redis */
    			// .withExpiresAt(new Date(expiresAt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()))
    			// Use the generated tokenId as the ID of the JWT
    			.withJWTId(userToken.getId())
    			.sign(Algorithm.HMAC256(this.jwtKey));

    	/** * responds to the client with a Cookie */
    	Cookie cookie = new Cookie("_token", token);
    	cookie.setSecure(request.isSecure());
    	cookie.setHttpOnly(true);
    	// The cookie life cycle is set to -1 and will be deleted immediately after the browser closes
    	cookie.setMaxAge(remember ? expiresSeconds : -1);
    	cookie.setPath("/");
    	response.addCookie(cookie);
    	
		return Collections.singletonMap("success".true); }}Copy the code

Allow multiple logins for the same user

In the above code, the Token is stored and the user’s ID is used as the key, so the user can only have one legitimate Token at any time. However, some scenarios allow users to have multiple tokens at the same time. In this case, you can add the Token ID to the key of the Redis.

this.objectRedisTemplate.opsForValue().set("token:" + user.getId() + ":" + userToken.getId(), userToken, expiresSeconds, TimeUnit.SECONDS);
Copy the code

If you need to retrieve all the tokens of the user, you can use Redis sacnner for scanning.

token:{userId}:*
Copy the code

Validation logic in interceptors

import java.util.concurrent.TimeUnit;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.util.WebUtils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;

import io.springboot.jwt.redis.ObjectRedisTemplate;
import io.springboot.jwt.web.support.UserToken;

public class TokenValidateInterceptor extends HandlerInterceptorAdapter {

	private static final Logger LOGGER = LoggerFactory.getLogger(TokenValidateInterceptor.class);

	@Autowired
	private ObjectRedisTemplate objectRedisTemplate;

	@Value("${jwt.key}")
	private String jwtKey;

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

		/ / cookie
		Cookie cookie = WebUtils.getCookie(request, "_token");

		if(cookie ! =null) {

			DecodedJWT decodedJWT = null;

			Integer userId = null;

			try {
				decodedJWT = JWT.require(Algorithm.HMAC256(this.jwtKey)).build().verify(cookie.getValue());
				userId = decodedJWT.getClaim("id").asInt();
			} catch (JWTVerificationException e) {
				LOGGER.warn({}, Token ={}, e.getMessage(), cookie.getValue());
			}

			if(userId ! =null) {

				String tokenKey = "token:" + userId;

				UserToken userToken = (UserToken) objectRedisTemplate.opsForValue().get(tokenKey);

				if(userToken ! =null && userToken.getId().equals(decodedJWT.getId()) && userId.equals(userToken.getUserId())) {

					/** * The Token is a valid Token and needs to be renewed */
					this.objectRedisTemplate.expire(tokenKey, userToken.getRemember() 
							? TimeUnit.DAYS.toSeconds(7) 
							: TimeUnit.MINUTES.toSeconds(30),
								TimeUnit.SECONDS);

					//TODO stores the identity of the current user into the context of the current request for retrieval in the Controller (e.g. : ThreadLocal)
					return true; }}}/** * Verification failed. Token does not exist/Invalid Token/Token has expired */
		
		// TODO throws an unlogged exception and responds to the client in the global handler (you can also respond directly here via Response)
		return false;
	}
	
	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)	throws Exception {
		// TODO, need to remember to clean up the user identity information stored in the context of the current request}}Copy the code

Very simple logic, read token through Cookie, try to read cached data from Redis. Verify. If verification succeeds. The expiration time of the Token is updated to complete the Token renewal.

Management of Token

It is very simple, just according to the user ID, you can delete/renew the operation. The server can proactively revoke the authorization of a Token. If you need to obtain all the tokens, you can use SACN to scan the tokens with the specified prefix.

With the Redis expiration key notification event, you can also listen in the program to see which tokens have expired.


Original text: springboot. IO/topic / 234 / t…