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
token
The renewal of- Server active failure specified
token
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…