SSMフレームワークによる大学卒業生就職管理プラットフォームの構築と実装

システム設計と技術構成

現代の教育機関および就職支援機関では、膨大な学生データと求人情報を効率的に統合管理する仕組みが求められています。本プラットフォームは、卒業生のキャリア支援業務をデジタル化し、データの一括処理とセキュリティ強化を実現することを目的としています。SSM(Spring Framework + Spring MVC + MyBatis)アーキテクチャを中核とし、Java言語とMySQLリレーショナルデータベースを組み合わせて構築されています。システムは管理者用のバックエンド制御画面と、学生が利用するフロントエンド情報ポータルに明確に分離されており、操作フローの最適化と情報アクセスの高速化を実現しています。

使用技術スタック

  • 開発言語: Java (JDK 8 推奨)
  • バックエンド: Spring, Spring MVC, MyBatis
  • フロントエンド: JSP, JavaScript, CSS3
  • データベース: MySQL 5.7 以降
  • アプリケーションサーバー: Apache Tomcat (7.x ~ 10.x)
  • ビルド管理: Apache Maven
  • IDE: IntelliJ IDEA / Eclipse

機能モジュール概要

管理者用バックエンド

システム管理者は以下の機能を通じてデータ運用と構成管理を行います。

  • 学生情報制御: 学籍データの入力、プロフィール編集、在籍ステータスの更新、および統計レポートの出力。
  • バナー通知管理: ホームページ上部に表示されるスライド画像の追加、更新、撤去。
  • キャリア相談員管理: 就職指導を担当する教職員および外部アドバイザー情報の登録。
  • ガイダンス分類設定: 進路支援コンテンツのタグ管理および公開範囲の指定。

学生向けフロントエンド

在籍学生および卒業生は、以下のポータル機能を利用して就職活動支援サービスにアクセスします。

  • 情報ダッシュボード: キャリアガイダンス資料、合同説明会スケジュール、求人票、関連法規、相談員プロフィールの一覧表示。
  • アカウント認証: メールアドレスおよびパスワードによる新規登録とログインセッションの確立。
  • プロフィール管理: 氏名、連絡先、希望職種などの個人情報閲覧および編集。
  • 求人詳細参照: 企業概要の閲覧、募集要項の確認、および応募リンクへの遷移。

コアロジック実装

リクエストフィルタとCORS制御

SpringのHandlerInterceptorを用いて、クロスオリジンリソース共有(CORS)の許可設定とトークンベースのセッション検証を行います。不要なエンドポイントへのアクセスをフィルタリングし、システムのセキュリティレイヤーを構築します。

package jp.example.platform.security;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
public class RequestAuthInterceptor implements HandlerInterceptor {
    private static final String AUTH_HEADER_KEY = "X-App-Token";
    private static final String SESSION_USER_ATTR = "CURRENT_USER_CONTEXT";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String origin = request.getHeader("Origin");
        if (StringUtils.hasText(origin)) {
            response.setHeader("Access-Control-Allow-Origin", origin);
        }
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type, X-App-Token, Authorization");
        response.setHeader("Access-Control-Allow-Credentials", "true");

        if (handler instanceof HandlerMethod) {
            HandlerMethod hm = (HandlerMethod) handler;
            if (hm.hasMethodAnnotation(AnonymousAccess.class)) {
                return true;
            }
        }

        String clientToken = request.getHeader(AUTH_HEADER_KEY);
        if (!StringUtils.hasText(clientToken)) {
            sendErrorResponse(response, 401, "認証トークンが不足しています");
            return false;
        }

        if (!validateSessionToken(clientToken)) {
            sendErrorResponse(response, 401, "無効または期限切れのトークンです");
            return false;
        }

        request.getSession().setAttribute(SESSION_USER_ATTR, clientToken);
        return true;
    }

    private void sendErrorResponse(HttpServletResponse resp, int status, String message) throws IOException {
        resp.setStatus(status);
        resp.setContentType("application/json;charset=UTF-8");
        Map<String, Object> payload = new HashMap<>();
        payload.put("code", status);
        payload.put("msg", message);
        new ObjectMapper().writeValue(resp.getOutputStream(), payload);
    }

    private boolean validateSessionToken(String token) {
        return token != null && token.length() > 10;
    }
}

ユーザー認証とアカウント管理

コントローラ層では、ログイン、新規登録、パスワード初期化、およびCRUD処理を担います。データベースアクセスはサービスレイヤを通じてMyBatisマッパーと連携し、ビジネスロジックとデータアクセスを分離しています。

package jp.example.platform.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@RestController
@RequestMapping("/api/accounts")
public class AccountController {

    @Autowired
    private AccountService service;

    @PostMapping("/signup")
    public ResponseEntity<Map<String, Object>> create(@RequestBody AppUser newUser) {
        if (service.findByUsername(newUser.getUsername()).isPresent()) {
            return ResponseEntity.badRequest().body(buildResponse(400, "指定のユーザー名は既に登録されています"));
        }
        newUser.encodeSecret();
        service.insert(newUser);
        return ResponseEntity.ok(buildResponse(200, "登録が完了しました"));
    }

    @PostMapping("/signin")
    public ResponseEntity<Map<String, Object>> authenticate(@RequestParam String username, @RequestParam String password) {
        Optional<AppUser> found = service.findByUsername(username);
        if (!found.isPresent() || !found.get().matchSecret(password)) {
            return ResponseEntity.status(401).body(buildResponse(401, "認証に失敗しました"));
        }
        String sessionKey = service.generateSessionId(found.get().getId());
        Map<String, Object> result = buildResponse(200, "ログイン成功");
        result.put("token", sessionKey);
        return ResponseEntity.ok(result);
    }

    @PostMapping("/reset")
    public ResponseEntity<Map<String, Object>> resetCredentials(@RequestParam String account) {
        AppUser target = service.findByUsername(account).orElse(null);
        if (target == null) return ResponseEntity.notFound().build();
        target.setSecret("DEFAULT_123");
        target.encodeSecret();
        service.update(target);
        return ResponseEntity.ok(buildResponse(200, "パスワードを初期化しました"));
    }

    @GetMapping("/listing")
    public ResponseEntity<Map<String, Object>> fetchPage(@RequestParam(required = false) String query,
                                                           @RequestParam(defaultValue = "1") int pageNum,
                                                           @RequestParam(defaultValue = "10") int pageSize) {
        Map<String, Object> pageData = service.retrievePaged(query, pageNum, pageSize);
        Map<String, Object> result = buildResponse(200, "取得成功");
        result.put("records", pageData);
        return ResponseEntity.ok(result);
    }

    @PutMapping("/modify")
    public ResponseEntity<Map<String, Object>> editProfile(@RequestBody AppUser updated) {
        AppUser existing = service.getById(updated.getId());
        if (existing == null) return ResponseEntity.notFound().build();
        existing.applyChanges(updated);
        service.update(existing);
        return ResponseEntity.ok(buildResponse(200, "更新完了"));
    }

    @DeleteMapping("/bulkRemove")
    public ResponseEntity<Map<String, Object>> removeMultiple(@RequestBody List<Long> targetIds) {
        service.deleteBatch(targetIds);
        return ResponseEntity.ok(buildResponse(200, "削除処理が完了しました"));
    }

    private Map<String, Object> buildResponse(int code, String msg) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", code);
        map.put("msg", msg);
        return map;
    }
}

タグ: SSM Spring MVC MyBatis Java MySQL

5月30日 14:33 投稿