preface

This article will introduce an idea of integrating JWT based on SpringBoot and Vue’s back-end separation project, as well as the Token refresh strategy in the case of remembering passwords. This article will assume that you have some knowledge of the following, if not, it is recommended to take a look at the content of the recommended link:

  • JWTBasic knowledge of:Introduction to JWT
  • Cross-domain problem: Solve the cross-domain problem of front – and back-end separated projects
  • SpringBootintegrationredis:Learn about redis automatic configuration through source code

The following figure is the basic idea of implementing Token verification and refreshing in this paper. This paper shows the core code to achieve the final effect. The complete code (including front-end and back-end code) has been synchronized to GitHub:

The effect

Here is the final look, showing only the front end of the interface:

implementation

Since the complete code has been uploaded to GitHub, this article will only show the implementation ideas of the core-related code, starting with the back-end part:

The backend implementation

In order to refresh the Token when the Token is invalid when the password is remembered, the last login time of the user is stored in Redis, of course, it can also be stored in the database. If the refresh conditions are met, Update the last login time and Token expiration in Redis and refresh the Token.

The following shows the specific configuration. First, the configuration file is displayed:

spring:
  # Cancel the banner icon
  main:
    banner-mode: off

  Mysql database connection setup
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/blog? serverTimezone=GMT%2B8&charset=utf8mb4&useSSL=false
    username: root
    password: root

  # jPA related Settings
  jpa:
    show-sql: true
    properties:
      hibernate:
      dialect: org.hibernate.dialect.MySQL5InnoDBDialect
    open-in-view: false

  # redis configuration
  redis:
    database: 0
    host: localhost
    port: 6379
    lettuce:
      pool:
        max-active: 8
        max-wait: - 1
        max-idle: 10
        min-idle: 5
      shutdown-timeout: 100ms

# set port
server:
  port: 8888

# set JWT in seconds
jwt:
  # Set the Token expiration time
  expiration: 10
  # set JWT key
  secret: zjw1221
  Set the time to remember your password
  remember-time: 1296000
  # Set to less than a certain time to automatically renew the Token
  validate-time: 300
Copy the code

Then the WebMvc configuration:

@Configuration
public class WebConfig extends WebMvcConfigurationSupport {

	private final HttpInterceptor httpInterceptor;

	@Autowired
	public WebConfig(HttpInterceptor httpInterceptor) {
		this.httpInterceptor = httpInterceptor;
	}

	@Override
	public void addCorsMappings(CorsRegistry registry) {
		// Configure cross-domain dependencies and allow authorization to appear in the response header
		registry.addMapping("/ * *")
				.allowedOrigins("*")
				.allowedMethods("POST"."GET"."PUT"."PATCH"."OPTIONS"."DELETE")
				.exposedHeaders("authorization")
				.allowedHeaders("*")
				.maxAge(3600);
	}

	@Override
	protected void addInterceptors(InterceptorRegistry registry) {
		// Set up a custom interceptor to block all interfaces
		// Exclude /login requests without preventing /login invalidation
		registry.addInterceptor(httpInterceptor)
				.addPathPatterns("/ * *")
				.excludePathPatterns("/login"."/error");
		super.addInterceptors(registry); }}Copy the code

Then there’s the interceptor setup:

@Configuration
public class HttpInterceptor implements HandlerInterceptor {

    private final Gson gson;
    private final UserSecurityUtil userSecurityUtil;

    @Autowired
    public HttpInterceptor(Gson gson, UserSecurityUtil userSecurityUtil) {
        this.gson = gson;
        this.userSecurityUtil = userSecurityUtil;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // To handle cross-domain requests, return normal if OPTIONS are sent
        if (HttpMethod.OPTIONS.matches(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return true;
        }

        // Set the encoding format of the request and response headers
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");

        // Call the validation method to verify that the Token in the request header is valid
        boolean isOk = userSecurityUtil.verifyWebToken(request, response);

        // Return false if illegal, true otherwise
        if(! isOk) { ResultEntity<String> resultEntity =new ResultEntity<>();
            resultEntity.setErrMsg("Please log in again");
            resultEntity.setStatus(false);
            response.getWriter().write(gson.toJson(resultEntity));
            return false;
        }
        return true; }}Copy the code

After that, the verification method is set:

@Component
public class UserSecurityUtil {

    private final RedisUtil redis;
    @Value("${jwt.validate-time}")
    private long validateTime;

    @Autowired
    public UserSecurityUtil(RedisUtil redis) {
        this.redis = redis;
    }

    public boolean verifyWebToken(HttpServletRequest req, HttpServletResponse resp) {
        // Get the authorization information in the request header
        String token = req.getHeader("authorization");
        // Return false if null
        if (token == null) {
            return false;
        }
        // Decode Token message, return false if null
        DecodedJWT jwtToken = JwtUtil.decode(token);
        if (jwtToken == null) {
            return false;
        }
        // Obtain the user ID in the Token information
        // Check in redis, return false if no relevant information exists
        long uid = Long.parseLong(jwtToken.getSubject());
        if (redis.getExpire(uid) == -2) {
            return false;
        }
        // Obtain JwtEntity entity information from Redis according to the uid
        JwtEntity jwtEntity = (JwtEntity) redis.get(uid);
        try {
            // Continue the verification
            JwtUtil.verifyToken(token);
        } catch (SignatureVerificationException e) {
            // If a signature verification exception occurs, return false
            return false;
        } catch (TokenExpiredException e) {
            // If it expires, determine whether the criteria for obtaining the refresh Token are met
            // If null is returned, Token expired, delete the information in redis and return false
            String newToken = JwtUtil.getRefreshToken(jwtToken, jwtEntity);
            if (newToken == null) {
                redis.del(uid);
                return false;
            }
            // If the token refresh condition is met, set the return header authorization and return true
            resp.setHeader("authorization", newToken);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        // Check whether the password is not remembered and the remaining validity period of the Token is smaller than a specified value
        // If less than a certain time, the Token is refreshed
        if(! jwtEntity.getIsRemember()) { Instant exp = jwtEntity.getLastLoginTime().atZone(ZoneId.systemDefault()).toInstant(); Instant now = Instant.now();if(now.getEpochSecond() - exp.getEpochSecond() <= validateTime) { token = JwtUtil.getRefreshToken(jwtToken); }}// Set the token in the return header
        resp.setHeader("authorization", token);
        return true; }}Copy the code

And then the JwtEntity class setting:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class JwtEntity {

    private String token;
    // Prevent LocalDateTime from failing in serialization
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    private LocalDateTime lastLoginTime;
    private Boolean isRemember;

}
Copy the code

Then there is the JwtUtil setup:

@Component
public class JwtUtil {

    private static String secret;
    private static long expiration;
    private static long rememberTime;
    private static RedisUtil redis;

    @Autowired
    public JwtUtil(RedisUtil redis) {
        JwtUtil.redis = redis;
    }

    @Value("${jwt.secret}")
    public void setSecret(String secret) {
        JwtUtil.secret = secret;
    }

    @Value("${jwt.expiration}")
    public void setExpiration(long expiration) {
        JwtUtil.expiration = expiration;
    }

    @Value("${jwt.remember-time}")
    public void setRememberTime(long rememberTime) {
        JwtUtil.rememberTime = rememberTime;
    }

    public static String createToken(Long uid, Instant issueAt) {
        / / Token is generated
        Instant exp = issueAt.plusSeconds(expiration);
        return createToken(uid.toString(), issueAt, exp);
    }

    public static DecodedJWT decode(String token){
        try {
            // Return Token decoding information
            return JWT.decode(token);
        } catch (Exception e) {
            e.printStackTrace();
            return null; }}public static void verifyToken(String token) {
        / / validation Token
        JWTVerifier verifier = JWT.require(Algorithm.HMAC256(JwtUtil.secret)).build();
        verifier.verify(token);
    }

    public static String getRefreshToken(DecodedJWT jwtToken, JwtEntity jwtEntity) {
        // This refresh Token class is used to handle Token invalidation
        Instant exp = jwtEntity.getLastLoginTime().atZone(ZoneId.systemDefault()).toInstant();
        Instant now = Instant.now();
        // If the password retention period is exceeded or the password is not remembered, null is returned
        if(! jwtEntity.getIsRemember() || (now.getEpochSecond() - exp.getEpochSecond()) > rememberTime) {return null;
        }
        // Otherwise generate the refreshed Token and reset the information stored in redis
        Instant newExp = exp.plusSeconds(expiration);
        String token = createToken(jwtToken.getSubject(), now, newExp);
        LocalDateTime lastLoginTime = getLastLoginTime(newExp);
        redis.set(jwtToken.getSubject(), new JwtEntity(token, lastLoginTime, true));
        return token;
    }

    public static String getRefreshToken(DecodedJWT jwtToken) {
        // This refresh Token class is used to handle cases where the Token validity time is less than a certain value
        // Generate refreshed tokens and reset the information stored in redis
        Instant now = Instant.now();
        Instant newExp = now.plusSeconds(expiration);
        String token = createToken(jwtToken.getSubject(), now, newExp);
        redis.set(jwtToken.getSubject(), new JwtEntity(token, getLastLoginTime(now), false));
        return token;
    }

    private static String createToken(String sub, Instant iat, Instant exp) {
        // Generate tokens, including user uid, effective and expiry dates
        return JWT.create()
                .withClaim("sub", sub)
                .withClaim("iat", Date.from(iat))
                .withClaim("exp", Date.from(exp))
                .sign(Algorithm.HMAC256(JwtUtil.secret));
    }

    private static LocalDateTime getLastLoginTime(Instant newExp) {
        // Get the LocalDateTime format for the current time
        returnLocalDateTime.ofInstant(newExp, ZoneId.systemDefault()); }}Copy the code

Then there are the Settings for logging in to the controller:

@RestController
public class LoginInController {

    private final UserService userService;

    @Autowired
    public LoginInController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping("/login")
    public ResultEntity<Long> login(@RequestBody LoginEntity loginEntity, HttpServletResponse response) {
        ResultEntity<Long> resultEntity = new ResultEntity<>();
        // Verify the username and password in the login information
        User user = userService.findByUsername(loginEntity.getUsername());
        booleanisOk = user ! =null && user.getPassword().equals(loginEntity.getPassword());
        // If not, return directly
        if(! isOk) { resultEntity.setErrMsg("Wrong username or password");
            resultEntity.setStatus(false);
            return resultEntity;
        }
        // Otherwise, the Token is generated and the authorization information of the header is set
        String token = userService.createWebToken(user.getId(), loginEntity.getIsRemember());
        response.setHeader("authorization", token);
        resultEntity.setData(user.getId());
        returnresultEntity; }}Copy the code

The login entity class is as follows:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginEntity {

    private String username;
    private String password;
    private Boolean isRemember;
}
Copy the code

The contents of UserService are as follows:

@Service
public class UserService {

    private final UserDao dao;
    private final RedisUtil redis;

    @Autowired
    public UserService(UserDao dao, RedisUtil redis) {
        this.dao = dao;
        this.redis = redis;
    }

    public User findById(Long uid) {
        return dao.findById(uid).orElse(null);
    }

    public User findByUsername(String username) {
        return dao.findByUsername(username);
    }

    @Transactional
    public String createWebToken(Long uid, Boolean isRemember) {
        // Call the Token generation method of the JwtUtil utility class
        // Store the token of the user in redis, the last login time, whether to remember the password and so on
        Instant now = Instant.now();
        String token = JwtUtil.createToken(uid, now);
        LocalDateTime lastLoginTime = LocalDateTime.ofInstant(now, ZoneId.systemDefault());
        redis.set(uid, newJwtEntity(token, lastLoginTime, isRemember ! =null && isRemember));
        return token;
    }

    @Transactional
    public void deleteWebToken(Long uid) {
        // Delete the user's Jwt information from redisredis.del(uid); }}Copy the code

The deregistered controller Settings are as follows:

@RestController
public class LoginOutController {

    private final UserService userService;

    @Autowired
    public LoginOutController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/exit")
    public String login(Long uid) {
        // When a user logs out, the information stored in Redis is deleted according to the user ID
        // There are some security issues because the foreground UID is localStorage
        // This article is an introduction to Jwt
        userService.deleteWebToken(uid);
        return "Logout successful"; }}Copy the code

The front-end implementation

The first is the login interface:

<template> <el-row type="flex" class="login"> < EL-col :span="6"> <h1 class="title">JWT test </h1> < EL-form :model="loginForm" :rules="rules" status-icon ref="ruleForm" class="demo-ruleForm"> <el-form-item prop="username"> <el-input V-model ="loginForm. Username "autocomplete="off" placeholder=" placeholder" prefix ="el-icon-user-solid" ></el-input> </el-form-item> <el-form-item prop="password"> <el-input type="password" v-model="loginForm.password" Autocomplete ="off" placeholder=" please input password "prefix-icon="el-icon-lock" ></el-input> </el-form-item> <el-form-item Prop ="isRemember"> <el-checkbox V-model ="loginForm. IsRemember "class="remember"> </el-checkbox> <el-button Type ="primary" @click="submitForm" class="login-btn"> </el-button> </el-form> </el-col> </el-row> </template> <script> import { Row, Col, Form, Input, Button, Message, Checkbox, FormItem, Loading } from 'element-ui' import {request} from '.. /network/request' export default { name: 'Login', components: { 'el-row': Row, 'el-col': Col, 'el-form': Form, 'el-checkbox': Checkbox, 'el-input': Input, 'el-button': Button, 'el-form-item': FormItem }, data() { return { loginForm: { username: '', password: '', isRemember: false }, rules: { username: [{required: true, message: 'Please enter user name ', trigger: 'blur'}], Password: [{required: true, message:' Please enter password ', trigger: 'blur' }] } } }, methods: { submitForm() { const loading = Loading.service({ fullscreen: true }) request({ method: 'post', url: '/login', data: { 'username': this.loginForm.username, 'password': this.loginForm.password, 'isRemember': Enclosing loginForm. IsRemember}}). Then (res = > {loading. The close () / / login successfully deposited the user uid in localStorage localStorage. SetItem (uid, This.$router.push('/home') Message(' login successful ')}). Catch (err => {console.log(err)})}} </script>Copy the code

Then there is the Home screen:

<template> <el-row type="flex" justify="center" align="middle" class="main"> <el-col :span="12"> <el-button @click="exit"> log off </el-button> <el-button @click="test"> test </el-button> </el-col> </el-row> </template> <script> import { Row, Col, Button, Message } from 'element-ui' import {request} from '.. /network/request' export default { name: 'Home', components: { 'el-row': Row, 'el-col': Col, 'el-button': Button }, methods: { exit() { request({ method: 'get', url: '/exit', params: { uid: Localstorage.getitem ('uid')}}).then(() => {// After the user logs out, the local Token localstorage.removeItem ('authorization') // is cleared and the login page is displayed This.$router.push('/login') Message(' log out ')}). Catch (err => {console.log(err)})}, test() {request({method: 'get', url: '/hello',}). Then (res => {if (res.data) {// If Token has not expired, then return 'hello, world! 'Message(res.data)} else {// Otherwise prompt user to log in Message(' please log in again ')}}). Catch (err => {console.log(err)})}} </script>Copy the code

The route configuration is as follows:

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
  {
    path: '/'.component() {
      return import('@/views/Home')}}, {path: '/home'.name: 'Home'.component() {
      return import('@/views/Home')}}, {path:'/login'.name:'Login'.component() {
      return import('@/views/Login')}}]const router = new VueRouter({
  mode: 'history'.base: process.env.BASE_URL,
  routes
})


// Set the global front navigation guard
router.beforeEach((to, from, next) = > {
  // If the target path is login page, no operation is performed
  if (to.path === '/login') {
    next()
  } else {
    /** * If the login page is not displayed, check whether the local Token exists. If yes, the login page is redirected to
    let token = localStorage.getItem('authorization')
    if(! token) { next('/login')}else {
      next()
    }
  }
})

export default router
Copy the code

Finally, network request-related Settings:

import axios from 'axios'
import router from '.. /router'

export function request(config) {

  const req = axios.create({
    baseURL: 'http://localhost:8888'.timeout: 5000
  })

  // All network request headers carry tokens stored locally
  req.interceptors.request.use(config= > {
    const token = localStorage.getItem('authorization')
    token && (config.headers.authorization = token)
    return config
  })

  /** * Obtain the Token in the response result of all requests. * If the return value is not empty, the local Token is valid. Reset the local Token
  req.interceptors.response.use(response= > {
    const token = response.headers.authorization
    if (token) {
      localStorage.setItem('authorization', token)
    } else {
      localStorage.removeItem('authorization')
      router.push('/login')}return response.data
  })

  return req(config)
}
Copy the code

conclusion

This article briefly introduces the mechanism of the integration of JWT and Token refresh in the back-end separation project. Due to lack of experience, there may be many mistakes in the idea, welcome comments.

The resources

The ideas for this implementation refer to the following blogs:

  • www.cnblogs.com/zxcoder/p/1…
  • Blog.csdn.net/qq_38345296…