Implementing a Web Blog System with SSM Framework and JWT Authentication

Building a Blog Platform with Spring MVC, Spring, and MyBatis

A web-based blog system is implemented using the SSM stack (Spring MVC, Spring, MyBatis). This platform consists of five core pages: user authentication, blog creation, blog editing, article listing, and post details. The backend is designed to fulfill the following functional requirements:

  • Authentication: Validate user credentials (username and password).
  • User Profile: Retrieve and display user information based on the user ID.
  • Article List: Fetch and present a list of blog posts.
  • Author Profile: Display author details by resolving the author ID from a blog post.
  • Post Details: Show comprehensive content of a specific blog article using its unique ID.
  • Edit Function: Load existing post data for modification and persist updates.
  • Delete Operation: Remove a blog post through a logical delete operation.
  • Create Post: Insert a new blog entry with user-provided data.

Service Endpoint Architecture

User Services:

  • Validate credentials.
  • Retrieve user profile data.

Blog Services:

  • Fetch paginated or full article lists.
  • Resolve author information via blog ID (process: Blog ID -> Author ID -> Author Details).
  • Get detailed content for a single post.
  • Update post information.
  • Delete a post (logical deletion).
  • Create a new post.

Core Data Models

  • User Entity
  • Blog Entity
  • Operation Result Entity

Database Schema Design

1. Table Structures

A database java_blog_spring is created with two primary tables.

User Table (user)

  • id (INT, Primary Key, Auto Increment)
  • user_name (VARCHAR(128), Unique)
  • password (VARCHAR(128))
  • github_url (VARCHAR(128))
  • delete_flag (TINYINT, default 0)
  • create_time (DATETIME)
  • update_time (DATETIME)

Blog Table (blog)

  • id (INT, Primary Key, Auto Increment)
  • title (VARCHAR(200))
  • content (TEXT)
  • user_id (INT(11))
  • delete_flag (TINYINT, default 0)
  • create_time (DATETIME)
  • update_time (DATETIME)

2. Initialization Script

CREATE DATABASE IF NOT EXISTS java_blog_spring CHARACTER SET utf8mb4;

CREATE TABLE IF NOT EXISTS java_blog_spring.user (
    `id` INT NOT NULL AUTO_INCREMENT,
    `user_name` VARCHAR(128) NOT NULL,
    `password` VARCHAR(128) NOT NULL,
    `github_url` VARCHAR(128) NULL,
    `delete_flag` TINYINT(4) DEFAULT 0,
    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE INDEX `user_name_UNIQUE` (`user_name` ASC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='User accounts';

CREATE TABLE IF NOT EXISTS java_blog_spring.blog (
    `id` INT NOT NULL AUTO_INCREMENT,
    `title` VARCHAR(200) NULL,
    `content` TEXT NULL,
    `user_id` INT(11) NULL,
    `delete_flag` TINYINT(4) DEFAULT 0,
    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Blog posts';

-- Sample Data
INSERT INTO java_blog_spring.user (user_name, password, github_url) VALUES
('zhangsan', '123456', 'https://github.com/sample1'),
('lisi', '123456', 'https://github.com/sample2');

INSERT INTO java_blog_spring.blog (title, content, user_id) VALUES
('First Post', 'Initial blog entry', 1),
('Second Post', 'Follow-up article', 2);

3. Project Setup and Configuration

Create a Spring Boot project with dependencies for Spring MVC and MyBatis. Configure application.yml:

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/java_blog_spring?characterEncoding=utf8&useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:mapper/**Mapper.xml

logging:
  file:
    name: application.log

Core Application Layers

The architecture follows a standard layered pattern: Controller (presentation), Service (business logic), and Mapper (data persistence).

1. Domain Models

Utilize Lombok annotations to reduce boilerplate code for getters, setters, and constructors.

User Model

@Data
public class UserAccount {
    private Integer id;
    private String userName;
    private String password;
    private String githubUrl;
    private Integer deleteFlag;
    private Date createTime;
    private Date updateTime;
}

Blog Model

@Data
public class BlogPost {
    private Integer id;
    private String title;
    private String content;
    private Integer userId;
    private boolean isOwner;
    private Integer deleteFlag;
    private Date createTime;
    private Date updateTime;

    public String getFormattedCreateTime() {
        return DateUtil.format(createTime);
    }
}

2. Shared Componnets

Standardized Response Wrapper Define an enumeration for operation status codes and a generic result container.

public enum OperationStatus {
    SUCCESS, FAILURE, UNAUTHENTICATED;
}

@Data
public class ApiResponse<T> {
    private OperationStatus status;
    private String message;
    private T payload;

    public static <T> ApiResponse<T> ok(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setStatus(OperationStatus.SUCCESS);
        response.setMessage("");
        response.setPayload(data);
        return response;
    }

    public static <T> ApiResponse<T> error(String errorMessage) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setStatus(OperationStatus.FAILURE);
        response.setMessage(errorMessage);
        response.setPayload(null);
        return response;
    }
}

Response Normalization Implement a global advice to wrap all controller responses.

@ControllerAdvice
public class ResponseNormalizer implements ResponseBodyAdvice<Object> {
    @Autowired
    private ObjectMapper jsonMapper;

    @Override
    public boolean supports(MethodParameter returnType, Class<?> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
                                  MediaType mediaType, Class<?> converterType,
                                  ServerHttpRequest req, ServerHttpResponse res) {
        if (body instanceof ApiResponse) {
            return body;
        }
        if (body instanceof String) {
            try {
                return jsonMapper.writeValueAsString(ApiResponse.ok(body));
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }
        return ApiResponse.ok(body);
    }
}

Exception Handler Centralize error handling across the application.

@Slf4j
@ResponseBody
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public ApiResponse<?> handleGeneric(Exception ex) {
        log.error("Unhandled exception", ex);
        return ApiResponse.error("Internal server error");
    }

    @ExceptionHandler(NoResourceFoundException.class)
    public ApiResponse<?> handleMissingResource(NoResourceFoundException ex) {
        log.warn("Resource not found: {}", ex.getResourcePath());
        return ApiResponse.error("Requested resource unavailable");
    }
}

Implementing Blog Listing

Data Access Layer

@Mapper
public interface ArticleMapper {
    @Select("SELECT id, title, content, user_id, delete_flag, create_time, update_time " +
            "FROM blog WHERE delete_flag = 0")
    List<BlogPost> fetchAllArticles();
}

Service Layer

@Service
@Slf4j
public class ArticleService {
    @Autowired
    private ArticleMapper articleMapper;

    public List<BlogPost> retrieveAllPosts() {
        return articleMapper.fetchAllArticles();
    }
}

Controller Endpoint

@RestController
@RequestMapping("/api/articles")
@Slf4j
public class ArticleController {
    @Autowired
    private ArticleService articleService;

    @GetMapping
    public List<BlogPost> listArticles() {
        log.info("Fetching article list");
        return articleService.retrieveAllPosts();
    }
}

Client-Side Rendering (JavaScript)

$.ajax({
    method: "GET",
    url: "/api/articles",
    success: function(response) {
        if (response.status === "SUCCESS") {
            const articles = response.payload;
            let htmlContent = '';
            articles.forEach(post => {
                htmlContent += `<div class="post">
                    <h3>${post.title}</h3>
                    <span class="timestamp">${post.formattedCreateTime}</span>
                    <p>${post.content.substring(0, 150)}...</p>
                    <a href="/post.html?id=${post.id}">Read full article</a>
                </div>`;
            });
            $(".article-container").html(htmlContent);
        }
    },
    error: function(xhr) {
        if (xhr.status === 401) {
            window.location.href = "/login.html";
        }
    }
});

Date Formatting Utility

Create a utility class to standardize date presentation.

public class DateUtil {
    private static final SimpleDateFormat FORMATTER = 
        new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
    public static String format(Date date) {
        return FORMATTER.format(date);
    }
}

Retrieving Individual Blog Posts

Data Access Method

@Select("SELECT id, title, content, user_id, delete_flag, create_time, update_time " +
        "FROM blog WHERE delete_flag = 0 AND id = #{postId}")
BlogPost fetchById(Integer postId);

Service Method

public BlogPost getPostDetail(Integer postId) {
    log.info("Retrieving details for post ID: {}", postId);
    return articleMapper.fetchById(postId);
}

Controller Endpoint

@GetMapping("/{postId}")
public BlogPost getPost(@PathVariable Integer postId, HttpServletRequest req) {
    log.info("Request for post details, ID: {}", postId);
    BlogPost post = articleService.getPostDetail(postId);
    
    // Check if current user is the author
    String authToken = req.getHeader("Authorization");
    Integer currentUserId = TokenUtil.extractUserId(authToken);
    if (currentUserId != null && currentUserId.equals(post.getUserId())) {
        post.setOwner(true);
    }
    return post;
}

Authentication with JWT Tokens

To overcome session limitations in distributed environments, implement JSON Web Token authentication.

Dependencies

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

Token Utility

@Slf4j
public class TokenUtil {
    private static final long EXPIRATION_MS = 3600000; // 1 hour
    private static final String SECRET_KEY = "YourSecretKeyHere";
    private static final Key KEY = Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8));

    public static String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_MS))
                .signWith(KEY)
                .compact();
    }

    public static Claims parseToken(String token) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(KEY)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            log.warn("Token validation failed: {}", token);
            return null;
        }
    }

    public static Integer extractUserId(String token) {
        Claims claims = parseToken(token);
        return claims != null ? (Integer) claims.get("userId") : null;
    }
}

Authentication Controller

@RestController
@RequestMapping("/api/auth")
@Slf4j
public class AuthController {
    @Autowired
    private UserService userService;

    @PostMapping("/login")
    public ApiResponse<String> authenticate(@RequestParam String username, 
                                           @RequestParam String password) {
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
            return ApiResponse.error("Credentials required");
        }

        UserAccount user = userService.findByUsername(username);
        if (user == null) {
            return ApiResponse.error("User not found");
        }

        if (!SecurityUtil.verifyPassword(password, user.getPassword())) {
            return ApiResponse.error("Invalid credentials");
        }

        Map<String, Object> tokenData = new HashMap<>();
        tokenData.put("userId", user.getId());
        tokenData.put("username", user.getUserName());
        String token = TokenUtil.generateToken(tokenData);
        return ApiResponse.ok(token);
    }
}

Client-Side Token Storage

function performLogin() {
    $.ajax({
        method: "POST",
        url: "/api/auth/login",
        data: {
            username: $("#username").val(),
            password: $("#password").val()
        },
        success: function(response) {
            if (response.status === "SUCCESS" && response.payload) {
                localStorage.setItem("auth_token", response.payload);
                window.location.href = "/dashboard.html";
            } else {
                alert(response.message);
            }
        }
    });
}

Mandatory Authentication Interceptor

Create an interceptor to validate tokens on protected endpoints.

@Component
@Slf4j
public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, 
                             Object handler) throws Exception {
        String token = req.getHeader("Authorization");
        log.debug("Token from header: {}", token);

        if (TokenUtil.parseToken(token) == null) {
            res.setStatus(HttpStatus.UNAUTHORIZED.value());
            return false;
        }
        return true;
    }
}

Register the interceptor, excluding public resources.

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private AuthInterceptor authInterceptor;

    private final List<String> EXCLUDED_PATHS = Arrays.asList(
        "/static/**",
        "/api/auth/**",
        "/**.html"
    );

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/api/**")
                .excludePathPatterns(EXCLUDED_PATHS);
    }
}

Client-Side Header Injection

Add token to all AJAX requests.

$(document).ajaxSend(function(event, xhr, options) {
    const token = localStorage.getItem("auth_token");
    if (token) {
        xhr.setRequestHeader("Authorization", token);
    }
});

Add error handling for unauthorized requests.

error: function(xhr) {
    if (xhr.status === 401) {
        window.location.href = "/login.html";
    }
}

User Information Display

Endpoints for User Data

@GetMapping("/profile")
public UserAccount getCurrentUser(HttpServletRequest req) {
    String token = req.getHeader("Authorization");
    Integer userId = TokenUtil.extractUserId(token);
    return userService.getUserById(userId);
}

@GetMapping("/author/{postId}")
public UserAccount getPostAuthor(@PathVariable Integer postId) {
    BlogPost post = articleMapper.fetchById(postId);
    if (post == null || post.getUserId() == null) {
        return null;
    }
    return userService.getUserById(post.getUserId());
}

Service Implementation

public UserAccount getPostAuthor(Integer postId) {
    BlogPost post = articleMapper.fetchById(postId);
    if (post == null || post.getUserId() <= 0) {
        log.error("Invalid post or author ID for post: {}", postId);
        return null;
    }
    return userMapper.findUserById(post.getUserId());
}

Client-Side User Display

function loadUserProfile(apiEndpoint) {
    $.ajax({
        method: "GET",
        url: apiEndpoint,
        success: function(response) {
            if (response.status === "SUCCESS" && response.payload) {
                const user = response.payload;
                $(".user-name").text(user.userName);
                $(".user-github").attr("href", user.githubUrl);
            }
        },
        error: function(xhr) {
            if (xhr.status === 401) {
                window.location.href = "/login.html";
            }
        }
    });
}

User Logout

Simply remove the token from client storage.

function logoutUser() {
    localStorage.removeItem("auth_token");
    window.location.href = "/login.html";
}

Creating Blog Posts

Controller Endpoint

@PostMapping
public ApiResponse<Boolean> createPost(@RequestBody BlogPost postData, 
                                      HttpServletRequest req) {
    if (StringUtils.isEmpty(postData.getTitle()) || 
        StringUtils.isEmpty(postData.getContent())) {
        return ApiResponse.error("Title and content required");
    }

    String token = req.getHeader("Authorization");
    Integer authorId = TokenUtil.extractUserId(token);
    if (authorId == null) {
        return ApiResponse.error("Authentication required");
    }

    postData.setUserId(authorId);
    boolean created = articleService.addPost(postData);
    return ApiResponse.ok(created);
}

Data Access Method

@Insert("INSERT INTO blog (title, content, user_id) " +
        "VALUES (#{title}, #{content}, #{userId})")
Integer insertPost(BlogPost post);

Service Method

public boolean addPost(BlogPost post) {
    try {
        int rows = articleMapper.insertPost(post);
        return rows == 1;
    } catch (Exception e) {
        log.error("Failed to create post", e);
        return false;
    }
}

Markdown Editor Integration

Include the editor in the post creation form.

<link rel="stylesheet" href="/assets/editormd/css/editormd.min.css">

<div id="editor">
    <textarea style="display:none;" id="content" name="content">
        Start writing your post here...
    </textarea>
</div>

<script src="/assets/editormd/editormd.min.js"></script>
<script>
    $(function() {
        editormd("editor", {
            width: "100%",
            height: "500px",
            path: "/assets/editormd/lib/"
        });
    });
</script>

Client-Side Submission

function submitPost() {
    $.ajax({
        method: "POST",
        url: "/api/articles",
        contentType: "application/json",
        data: JSON.stringify({
            title: $("#title").val(),
            content: $("#content").val()
        }),
        success: function(response) {
            if (response.status === "SUCCESS" && response.payload) {
                window.location.href = "/posts.html";
            } else {
                alert(response.message);
            }
        },
        error: function(xhr) {
            if (xhr.status === 401) {
                window.location.href = "/login.html";
            }
        }
    });
}

Rendering Markdown Content

// In post detail page
editormd.markdownToHTML("content-display", {
    markdown: post.content
});

Post Editing and Deletion

Authorization Check

Modify the post detail endpoint to include ownership flag.

@GetMapping("/{postId}")
public BlogPost getPost(@PathVariable Integer postId, HttpServletRequest req) {
    BlogPost post = articleService.getPostDetail(postId);
    String token = req.getHeader("Authorization");
    Integer currentUserId = TokenUtil.extractUserId(token);
    post.setOwner(currentUserId != null && currentUserId.equals(post.getUserId()));
    return post;
}

Client-Side Conditional UI

if (post.owner) {
    const controls = `<div class="post-controls">
        <button onclick="editPost(${post.id})">Edit</button>
        <button onclick="deletePost(${post.id})">Delete</button>
    </div>`;
    $(".post-header").append(controls);
}

Update Endpoint

@PutMapping("/{postId}")
public ApiResponse<Boolean> updatePost(@PathVariable Integer postId, 
                                      @RequestBody BlogPost updates) {
    if (postId == null || StringUtils.isEmpty(updates.getTitle()) || 
        StringUtils.isEmpty(updates.getContent())) {
        return ApiResponse.error("Invalid update data");
    }
    updates.setId(postId);
    articleService.modifyPost(updates);
    return ApiResponse.ok(true);
}

XML Mapper for Dynamic Updates

<mapper namespace="com.example.blog.mapper.ArticleMapper">
    <update id="updatePost">
        UPDATE blog
        <set>
            <if test="title != null">title = #{title},</if>
            <if test="content != null">content = #{content},</if>
            <if test="userId != null">user_id = #{userId},</if>
            <if test="deleteFlag != null">delete_flag = #{deleteFlag},</if>
        </set>
        WHERE id = #{id}
    </update>
</mapper>

Client-Side Update

function savePost() {
    $.ajax({
        method: "PUT",
        url: `/api/articles/${$("#postId").val()}`,
        data: {
            id: $("#postId").val(),
            title: $("#title").val(),
            content: $("#content").val()
        },
        success: function(response) {
            if (response.status === "SUCCESS" && response.payload) {
                window.location.href = `/post.html?id=${$("#postId").val()}`;
            } else {
                alert(response.message);
            }
        },
        error: function(xhr) {
            if (xhr.status === 401) {
                window.location.href = "/login.html";
            }
        }
    });
}

Loading Existing Content for Editing

function loadPostForEditing() {
    const postId = new URLSearchParams(window.location.search).get('id');
    $.ajax({
        method: "GET",
        url: `/api/articles/${postId}`,
        success: function(response) {
            if (response.status === "SUCCESS" && response.payload) {
                const post = response.payload;
                $("#postId").val(post.id);
                $("#title").val(post.title);
                
                editormd("editor", {
                    width: "100%",
                    height: "500px",
                    path: "/assets/editormd/lib/",
                    onload: function() {
                        this.watch();
                        this.setMarkdown(post.content);
                    }
                });
            }
        }
    });
}

Logical Deletion

Controller Endpoint

@DeleteMapping("/{postId}")
public boolean deletePost(@PathVariable Integer postId) {
    log.info("Soft deleting post: {}", postId);
    if (postId <= 0) return false;
    
    BlogPost updates = new BlogPost();
    updates.setId(postId);
    updates.setDeleteFlag(1);
    articleService.modifyPost(updates);
    return true;
}

Client-Side Deletion

function deletePost(postId) {
    if (!confirm("Confirm deletion?")) return;
    
    $.ajax({
        method: "DELETE",
        url: `/api/articles/${postId}`,
        success: function(response) {
            if (response.status === "SUCCESS" && response.payload) {
                window.location.href = "/posts.html";
            } else {
                alert(response.message);
            }
        },
        error: function(xhr) {
            if (xhr.status === 401) {
                window.location.href = "/login.html";
            }
        }
    });
}

Password Security with MD5 Hashing

Security Utility

public class SecurityUtil {
    public static String hashPassword(String plainPassword) {
        String salt = UUID.randomUUID().toString().replace("-", "");
        String hashed = DigestUtils.md5DigestAsHex((plainPassword + salt).getBytes());
        return salt + hashed;
    }

    public static boolean verifyPassword(String input, String stored) {
        if (StringUtils.isEmpty(input) || stored == null || stored.length() != 64) {
            return false;
        }
        String salt = stored.substring(0, 32);
        String hashedInput = DigestUtils.md5DigestAsHex((input + salt).getBytes());
        return (salt + hashedInput).equals(stored);
    }
}

Updated Authentication

@PostMapping("/login")
public ApiResponse<String> authenticate(@RequestParam String username, 
                                       @RequestParam String password) {
    UserAccount user = userService.findByUsername(username);
    if (user == null) {
        return ApiResponse.error("User not found");
    }

    if (!SecurityUtil.verifyPassword(password, user.getPassword())) {
        return ApiResponse.error("Invalid credentials");
    }

    Map<String, Object> tokenData = new HashMap<>();
    tokenData.put("userId", user.getId());
    tokenData.put("username", user.getUserName());
    String token = TokenUtil.generateToken(tokenData);
    return ApiResponse.ok(token);
}

Database Password Update

Update existing passwords to use hashed format.

UPDATE user SET password = 
CONCAT(SUBSTRING(MD5(RAND()), 1, 32), 
       MD5(CONCAT('plain_password', SUBSTRING(MD5(RAND()), 1, 32))))
WHERE user_name IN ('zhangsan', 'lisi');

Tags: Spring Boot MyBatis JWT Authentication Blog System Java Web Development

Posted on Fri, 08 May 2026 02:54:07 +0000 by cuongvt