JWT Login Authentication

📢 This article was translated by gemini-2.5-flash

Session Technology

Session: When a user opens a browser and accesses web server resources, a session is established. It ends when either side disconnects. A single session can include multiple requests and responses.

Session Tracking: A method to maintain browser state. The server needs to identify if multiple requests come from the same browser to share data across these requests within the same session.

Session Tracking Solutions:

  • Client-side Session Tracking: Cookie
  • Server-side Session Tracking: Session
  • Token-based Solutions

Cookie is an HTTP-supported tech. The first time a browser visits, the server sets a Cookie in the response header: Set-Cookie: your_cookie. The browser automatically stores this Cookie locally and sends it in the request header (Cookie: your_cookie) on subsequent visits.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
public class SessionController {
    // Set Cookie
    @GetMapping("/c1")
    public Result cookie1(HttpServletResponse response){
        response.addCookie(new Cookie("login_name", "yexca"));
        return Result.success();
    }

    // Get Cookie
    @GetMapping("/c2")
    public Result cookie2(HttpServletRequest request){
        Cookie[] cookies = request.getCookies();
        for (Cookie c : cookies){
            if(c.getName().equals("login_name")){
                System.out.println("login_name:"+c.getValue());
            }
        }
        return Result.success();
    }
}

But mobile clients can’t use Cookies, and Cookies don’t work cross-domain.

Same Domain: Same protocol, IP/domain, port.

Session

Implemented based on Cookies. The first browser request generates a Session, and the response header returns the Session ID Set-Cookie: JSESSIONID=session_id. The browser then automatically includes this ID in subsequent requests.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
public class SessionController {
    // Store value in HttpSession
    @GetMapping("/s1")
    public Result session1(HttpSession session){
        log.info("HttpSession_set:{}", session.hashCode());
        session.setAttribute("login_user","yexca");
        return Result.success();
    }

    // Get value from HttpSession
    @GetMapping("/s2")
    public Result session2(HttpServletRequest request){
        HttpSession session = request.getSession();
        log.info("HttpSession_get:{}", session.hashCode());

        Object loginUser = session.getAttribute("login_user");
        log.info("loginUser:{}", loginUser);
        return Result.success();
    }
}

The issue with Session arises when using load balancing with multiple servers. If server one handles the first response and server two handles the second, server two won’t have the Session, making it unusable.

JWT Tokens

Token-based tech supports PCs and mobile devices. It solves authentication issues in clustered environments, reduces server storage pressure, but requires custom implementation.

JWT, or JSON Web Token. Official website: https://jwt.io/ . It defines a compact, self-contained format for securely transmitting information between parties as JSON objects. Thanks to digital signatures, this information is reliable.

Structure:

  • First Part: Header, records token type, signing algorithm, etc.
  • Second Part: Payload, records custom information, default information, etc.
  • Third Part: Signature, prevents Token tampering and ensures security. It’s calculated from the Header, Payload, and a secret key using a specified signing algorithm.

Add Dependency

1
2
3
4
5
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

Generation and Parsing

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class WebApplicationTests {
    // JWT Generation
    @Test
    public void jwtGenTest(){

        Map<String,Object> claims = new HashMap<>();
        claims.put("id", "233");
        claims.put("user", "yexca");
        String jwt = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, "yexca") // Signing algorithm
                .setClaims(claims) // Custom content (payload)
                .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) // Valid for 1h
                .compact();
        System.out.println(jwt);
    }

// Output
// eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIzMyIsImV4cCI6MTcwMjc5NzA4OCwidXNlciI6InlleGNhIn0.BqZDxEddGN4g4GyyfvkOtKYv7DVlIF6cWY9PGW2RbUU

    // JWT Parsing
    @Test
    public void jwtParseTest(){
        Claims claims = Jwts.parser()
                .setSigningKey("yexca") // Secret key
                .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIzMyIsImV4cCI6MTcwMjc5NzA4OCwidXNlciI6InlleGNhIn0.BqZDxEddGN4g4GyyfvkOtKYv7DVlIF6cWY9PGW2RbUU")
                .getBody(); // Get the second part
        System.out.println(claims);
    }

// Output
// {id=233, exp=1702797088, user=yexca}
}

Login Authentication

Filter

Filter is one of the early JavaWeb “Big Three” components (Servlet, Filter, Listener). Filters can intercept resource requests to implement special functions like login authentication, unified encoding, or sensitive character handling.

Quick Start

Create Filter Class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@WebFilter(urlPatterns = "/*") // Intercept path
public class DemoFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
//        Filter.super.init(filterConfig);
        System.out.println("Filter initialized");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("Filter intercepted");

        // Release
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
//        Filter.super.destroy();
        System.out.println("Filter destroyed");
    }
}

Add Annotation to Application

1
2
@ServletComponentScan // Enables support for servlet components
@SpringBootApplication

Intercept Paths

Adjust as needed, examples below:

Intercept TypeurlPatternsMeaning
Specific Path Interception/loginOnly /login access is intercepted
Directory Interception/emps/*All resources under /emps are intercepted, including /emps itself
Intercept All/*All resource access is intercepted

Single Filter Execution Logic

Browser sends request -> Request intercepted -> Pre-release logic executes -> Release -> Post-release logic executes -> Browser receives response

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@WebFilter(urlPatterns = "/*") // Intercept path
public class DemoFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
//        Filter.super.init(filterConfig);
        System.out.println("Filter initialized");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("Filter intercepted, pre-release logic");

        // Release
        filterChain.doFilter(servletRequest, servletResponse);

        System.out.println("Filter intercepted, post-release logic");
    }

    @Override
    public void destroy() {
//        Filter.super.destroy();
        System.out.println("Filter destroyed");
    }
}

Filter Chain

In a web application, you can configure multiple filters, which then form a filter chain.

Order: The priority of annotated filters is determined by the natural sorting of filter class names.

Logic: Browser sends request -> Request intercepted by A -> A’s pre-release logic executes -> A releases -> Request intercepted by B -> B’s pre-release logic executes -> B releases -> B’s post-release logic executes -> A’s post-release logic executes -> Browser receives response

Login Authentication

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        // Get request URL
        String requestURI = request.getRequestURI();
        log.info("Request URL:{}",requestURI);

        // Check if it contains "login"
        if(requestURI.contains("login")){
            log.info("Login operation, releasing");
            filterChain.doFilter(servletRequest, servletResponse);
            return; // Because login operation doesn't need subsequent logic
        }

        // Not login, get token
        String jwt = request.getHeader("token");

        // Check if token is valid
        if(!StringUtils.hasLength(jwt)){
            log.info("Request header token is empty");
            // Convert object to JSON
            Result error = Result.error("NOT_LOGIN");
            String notLogin = JSONObject.toJSONString(error);
            response.getWriter().write(notLogin);
            return;
        }

        // Validate token
        try {
            JwtUtils.parseJWT(jwt);
        }catch (Exception e){
            e.printStackTrace();
            log.info("Failed to parse token");
            // Convert object to JSON
            Result error = Result.error("NOT_LOGIN");
            String notLogin = JSONObject.toJSONString(error);
            response.getWriter().write(notLogin);
            return;
        }

        // Release
        log.info("Token is valid, releasing");
        filterChain.doFilter(servletRequest,servletResponse);
    }
}

Interceptor

Quick Start

Create Interceptor

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    @Override // Runs before target method, returns true to release, false to block
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        return HandlerInterceptor.super.preHandle(request, response, handler);
        System.out.println("preHandle");
        return true;
    }

    @Override // Runs after target method
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
        System.out.println("postHandle");
    }


    @Override // Runs after view rendering, last to run
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
        System.out.println("afterCompletion");
    }
}

Create Configuration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Configuration // Configuration class
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
//        WebMvcConfigurer.super.addInterceptors(registry);
        registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");

    }

}

Intercept Paths

PathMeaningExample
/*First-level Path/emps, /login don’t match /emps/1
/**Any-level Path/emps, /emps/1, /emps/1/2
/emps/*First-level paths under /emps/emps/1 doesn’t match /emps, /emps/1/2
/emps/**Any-level paths under /emps/emps, /emps/1, /emps/1/2

Interception Flow

If both Filter and Interceptor exist

Browser access -> Filter pre-release logic -> Filter release -> DispatcherServlet -> Interceptor preHandle -> Controller -> postHandle -> afterCompletion -> DispatcherServlet -> Filter post-release logic -> Respond to browser

Filters intercept all requests, while Interceptors only intercept resources within the Spring environment.

Login Authentication

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    @Override // Runs before target method, returns true to release, false to block
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        return HandlerInterceptor.super.preHandle(request, response, handler);
        System.out.println("preHandle");

        // Get request URL
        String requestURI = request.getRequestURI();
        log.info("Request URL:{}",requestURI);

        // Check if it contains "login"
        if(requestURI.contains("login")){
            log.info("Login operation, releasing");
            return true;
        }

        // Not login, get token
        String jwt = request.getHeader("token");

        // Check if token is valid
        if(!StringUtils.hasLength(jwt)){
            log.info("Request header token is empty");
            // Convert object to JSON
            Result error = Result.error("NOT_LOGIN");
            String notLogin = JSONObject.toJSONString(error);
            response.getWriter().write(notLogin);
            return false;
        }

        // Validate token
        try {
            JwtUtils.parseJWT(jwt);
        }catch (Exception e){
            e.printStackTrace();
            log.info("Failed to parse token");
            // Convert object to JSON
            Result error = Result.error("NOT_LOGIN");
            String notLogin = JSONObject.toJSONString(error);
            response.getWriter().write(notLogin);
            return false;
        }

        // Release
        log.info("Token is valid, releasing");
        return true;
    }

    @Override // Runs after target method
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
        System.out.println("postHandle");
    }


    @Override // Runs after view rendering, last to run
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
        System.out.println("afterCompletion");
    }
}