0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Web 開発再入門 #10 ― コントローラー処理(Web API もどき)

Last updated at Posted at 2024-04-28

Web 開発再入門 #10 ― コントローラー処理(Web API もどき)

fmockup

はじめに

Web アプリケーションのサーバー・サイドを開発します。
MVC の Controller 部分となります。

MVC:Model View Controller

画面ショット

ログイン画面で認証 Web API を呼び出したとき、Web API の Controller のバリデーションでエラーを出しています。

Java-fmockup_14-loginValidationError.jpg

フォルダー・ファイル構成

D:\
└ Developments\
    └ Workspace\
         └ fmockup\
             ├ build\
             ├ sql\
             ├ src\
             │ └ main\
             │     ├ java\
             │     │ └ cn\
             │     │     └ com\
             │     │         └ xxxx\
             │     │             └ fmockup\
             │     │                  ├ action\
             │     │                  │ └ SessionAuthAction.java ← コレ
             │     │                  ├ controller\
             │     │                  ├ customizer\
             │     │                  ├ entity\
             │     │                  ├ mapper\
             │     │                  ├ response\
             │     │                  │ └ StatusResponse.java ← コレ
             │     │                  ├ service\
             │     │                  ├ util\
             │     │                  ├ validator\
             │     │                  │ └ SessionAuthValidator.java ← コレ
             │     │                  └ validator_order\
             │     │                       ├ ValidatorOrder.java ← コレ
             │     │                       ├ ValidatorOrder1.java ← コレ
             │     │                       ├ ValidatorOrder2.java ← コレ
             │     │                       └ ValidatorOrder3.java ← コレ
             │     └ resources\
             ├ vue-vite\
             └ WinSW.NET-nnn\

Web API もどき

GET リクエストと POST リクエストのみを実装します。なので、ここでは “Web API もどき” と呼んでいます。
簡単にするために、以下を実装していません。

  • PUT、DELETE リクエストは使用していません。
  • リクエストにおいて JSON 渡しを使用していません。

ファイルの文字コード

基本的に Eclipse でファイルを作成するので、あまり意識したことがありません。
多分、Unix 改行(LF)なのだと思います。

ファイルの作成

  1. Eclipse で、それぞれのソース・コード・フォルダーにて、[ファイル (F)] - [新規 (N)] - [クラス] で、“SessionAuthAction.java”、“SessionAuthValidator.java”、“GroupOrder.java”、“GroupOrder1.java”、“GroupOrder2.java”、“GroupOrder3.java”、“StatusResponse.java” を作成する。
    SessionAuthAction.java
    ・・・
    
    package cn.com.xxxx.fmockup.action;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.validation.BindingResult;
    import org.springframework.validation.ObjectError;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.ModelAttribute;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.RestController;
    
    import cn.com.xxxx.fmockup.entity.SessionAuthEntity;
    import cn.com.xxxx.fmockup.response.StatusResponse;
    import cn.com.xxxx.fmockup.service.SessionAuthService;
    import cn.com.xxxx.fmockup.util.LogUtil;
    import cn.com.xxxx.fmockup.util.MessageUtil;
    import cn.com.xxxx.fmockup.util.PropertyUtil;
    import cn.com.xxxx.fmockup.util.SessionUtil;
    import cn.com.xxxx.fmockup.validator.SessionAuthValidator;
    import cn.com.xxxx.fmockup.validator_order.GroupOrder;
    import jakarta.servlet.http.HttpSession;
    
    /**
     * Session Auth Action
     *   It processes the Web-API POST request from browser.
     */
    
    @RestController
    public class SessionAuthAction {
    
    	@Autowired
    	private HttpSession session;
    
    	@Autowired
    	private SessionAuthService service;
    
    	/**
    	 * Session Auth Action
    	 *   It processes the Web-API POST request from browser.
    	 */
    	@PostMapping("/session/auth")
    	@ResponseBody
    	public StatusResponse response(@Validated(GroupOrder.class) @ModelAttribute SessionAuthValidator validator, BindingResult result) throws Exception {
        // ★★★ ↑ SessionAuthValidator.java ★★★
    
    		MessageUtil.setLocale();
    
    		LogUtil.access("Login started (" + Thread.currentThread().getStackTrace()[1].getClassName() + ")");
    		StatusResponse response = new StatusResponse();
    
    		// Drop Session
    		session.invalidate();
    
    		// Check Bean Validator
    		if (result.hasErrors()) {
    			LogUtil.access("Login faild : '" + validator.getUserName() + "'");
    			response.setStatus("action-ng");
    			response.setMessage(MessageUtil.getMessage(MessageUtil.COMMON_ERROR_TITLE_PLEASE_MODIFY_VALUE));
    			for (ObjectError error : result.getAllErrors()) {
    				String[] keyValue = error.getDefaultMessage().split(":", 2);
    				if (keyValue[0].equals("ambiguous")) {
    					response.setMessage(keyValue[1]);
    				} else {
    					response.addMessageMap(keyValue[0], keyValue[1]);
    				}
    			}
    			return response;
                // ★★★ ↑ StatusResponse.java ★★★
    		}
    
    		// Check User Exists or Not
    		SessionAuthEntity entity;
    		try {
    			entity = service.searchOneUser(validator);
                // ★★★ ↑ SessionAuthService ★★★
    		} catch (Exception ex) {
    			LogUtil.access("Login failed (Service) : '" + validator.getUserName() + "', '" + ex.getMessage() + "'");
    			response.setStatus("action-ng");
    			response.setMessage(ex.getMessage());
    			return response;
                // ★★★ ↑ StatusResponse.java ★★★
    		}
    
    		// Set Session Values
    		SessionUtil sessionUtil = new SessionUtil(session, entity.getUserId(), validator.getUserName());
    
    		// Check Account Password Is Expired
    		if (entity.getDurationDays() > PropertyUtil.getLoginExpireDay()) {
    			LogUtil.access("Login succeed (expired) : '" + sessionUtil.getUserName() + "'");
    			response.setStatus("action-ok-expired");    // Go to Change Password Screen
    			response.setMessage("");
    			return response;
                // ★★★ ↑ StatusResponse.java ★★★
    		}
    
    		// Check Account Is Locked or Not
    		if (entity.getUserLockedF().equals("Y")) {
    			LogUtil.access("Login succeed (locked) : '" + sessionUtil.getUserName() + "'");
    			response.setStatus("action-ok-expired");    // Go to Change Password Screen
    			response.setMessage("");
    			return response;
                // ★★★ ↑ StatusResponse.java ★★★
    		}
    
    		// Set Response Values
    		sessionUtil.setLoggedIn();
    		LogUtil.access("Login succeed : '" + sessionUtil.getUserName() + "'");
    		response.setStatus("action-ok");
    		response.setMessage("");
    		return response;
            // ★★★ ↑ StatusResponse.java ★★★
    	}
    }
    
    説明:
    POST リクエスト( POST http://localhost:8080/session/auth )を受け付けて、リクエストデータをチェックして、メイン処理を行って、レスポンスデータ(JSON)を返却するまでの処理を行います。
    SessionAuthValidator.java
    ・・・
    
    package cn.com.xxxx.fmockup.validator;
    
    import cn.com.xxxx.fmockup.validator_order.GroupOrder1;
    import cn.com.xxxx.fmockup.validator_order.GroupOrder2;
    import cn.com.xxxx.fmockup.validator_order.GroupOrder3;
    import jakarta.validation.constraints.NotEmpty;
    import jakarta.validation.constraints.Pattern;
    import jakarta.validation.constraints.Size;
    import lombok.Data;
    
    /*
     * Notice:
     * Do not write detail message in Login Screen.
     * (Need to write ambiguous message in Login Screen)
     */
    
    /**
     * Session Authorization Validator
     *   It checks values from browser.
     */
    
    @Data
    public class SessionAuthValidator {
    
    	// Screen Input Values
    
    	@NotEmpty(message = "userName:{LOGIN_ERROR_FORM_NO_USER_ID_IS_SPECIFIED}", groups = GroupOrder1.class)
    	@Size(max = 32, message = "ambiguous:{LOGIN_ERROR_TITLE_USER_ID_OR_PASSWORD_IS_NOT_CORRECT}", groups = GroupOrder2.class)
    	@Pattern(regexp = "[a-zA-Z0-9\\_\\-]*", message = "ambiguous:{LOGIN_ERROR_TITLE_USER_ID_OR_PASSWORD_IS_NOT_CORRECT}", groups = GroupOrder3.class)
    	private String userName;
    
    	// Password is checked at SessionAuthAction
    	@NotEmpty(message = "password:{LOGIN_ERROR_FORM_NO_PASSWORD_IS_SPECIFIED}", groups = GroupOrder1.class)
    //	@Size(min = 8, max = 32, message = "ambiguous:{LOGIN_ERROR_TITLE_USER_ID_OR_PASSWORD_IS_NOT_CORRECT}", groups = GroupOrder2.class)
    //	@Pattern(regexp = "[a-zA-Z0-9\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\-\\=\\^\\~\\\\\\|\\@\\`\\[\\{\\;\\+\\:\\*\\]\\}\\,\\<\\.\\>\\/\\?\\_]*", message = "ambiguous:{LOGIN_ERROR_TITLE_USER_ID_OR_PASSWORD_IS_NOT_CORRECT}", groups = GroupOrder3.class)
    	private String password;
    }
    
    説明:
    リクエストデータの値チェックを行います。またデータベースに検索条件を指定する値にもなります。
    GroupOrder.java
    ・・・
    
    package cn.com.xxxx.fmockup.validator_order;
    
    import jakarta.validation.GroupSequence;
    
    @GroupSequence({ GroupOrder1.class, GroupOrder2.class, GroupOrder3.class })
    public interface GroupOrder {
    }
    
    GroupOrder1.java
    ・・・
    
    package cn.com.xxxx.fmockup.validator_order;
    
    public interface GroupOrder1 {
    }
    
    GroupOrder2.java
    ・・・
    
    package cn.com.xxxx.fmockup.validator_order;
    
    public interface GroupOrder2 {
    }
    
    GroupOrder3.java
    ・・・
    
    package cn.com.xxxx.fmockup.validator_order;
    
    public interface GroupOrder3 {
    }
    
    説明:
    上記の 4つは、リクエストデータのチェック順を定義するためのものです。
    SessionAuthService.java
    ・・・
    
    package cn.com.xxxx.fmockup.service;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import cn.com.xxxx.fmockup.entity.SessionAuthEntity;
    import cn.com.xxxx.fmockup.mapper.SessionAuthMapper;
    import cn.com.xxxx.fmockup.util.CryptoUtil;
    import cn.com.xxxx.fmockup.util.LogUtil;
    import cn.com.xxxx.fmockup.util.MessageUtil;
    import cn.com.xxxx.fmockup.util.PropertyUtil;
    import cn.com.xxxx.fmockup.validator.SessionAuthValidator;
    
    /**
     * Session Auth Service
     *   It processes login data.
     */
    
    @Service
    public class SessionAuthService {
    
    	@Autowired
    	private SessionAuthMapper mapper;
    
    	/**
    	 * Session Auth Service
    	 *   It processes login data.
    	 */
    	public SessionAuthEntity searchOneUser(SessionAuthValidator validator) throws Exception {
    
    		//
    		// If an error occurs in this process, it must output a blurred explanation in error message.
    		//
    
    		// Get Values from Database
    		SessionAuthEntity entity;
    		try {
    			entity = mapper.searchOneUser(validator);
    		} catch (Exception ex) {
    			LogUtil.info("searchOneUser : '" + ex.getMessage() + "'");
    			throw new Exception(MessageUtil.COMMON_ERROR_STRING_UNKNOWN_TROUBLE_HAS_OCCURRED);
    		}
    		if (entity == null) {
    			LogUtil.info("searchOneUser : 'User does not exists.'");															// A true problem explanation
    			throw new Exception(MessageUtil.getMessage(MessageUtil.LOGIN_ERROR_TITLE_USER_ID_OR_PASSWORD_IS_NOT_CORRECT));		// A blurred explanation
    		}
    
    		// Check Retry Count
    		if (entity.getFailedCount() >= PropertyUtil.getLoginRetryCount()) {
    			LogUtil.info("searchOneUser : 'Retry Count is overed by maximum count.'");											// A true problem explanation
    			throw new Exception(MessageUtil.getMessage(MessageUtil.LOGIN_ERROR_TITLE_USER_ID_OR_PASSWORD_IS_NOT_CORRECT));		// A blurred explanation
    		}
    
    		// Check Match Password
    		String password;
    		try {
    			password = CryptoUtil.decrypt(entity.getPasswordAes());
                // ★★★ ↑ CryptoUtil.java ★★★
    		} catch (Exception ex) {
    			LogUtil.info("searchOneUser : '" + ex.getMessage() + "'");		// A true problem explanation
    			throw new Exception(MessageUtil.getMessage(MessageUtil.LOGIN_ERROR_TITLE_USER_ID_OR_PASSWORD_IS_NOT_CORRECT));	// A blurred explanation
    		}
    		if (! password.equals(validator.getPassword())) {
    
    			try {
    				mapper.updateUserFailedCount(entity.getUserId());
    			} catch (Exception ex) {
    				LogUtil.info("searchOneUser : '" + ex.getMessage() + "'");
    				throw new Exception(MessageUtil.COMMON_ERROR_STRING_UNKNOWN_TROUBLE_HAS_OCCURRED);
    			}
    
    			LogUtil.info("searchOneUser : 'Password is not correct.'");														// A true problem explanation
    			throw new Exception(MessageUtil.getMessage(MessageUtil.LOGIN_ERROR_TITLE_USER_ID_OR_PASSWORD_IS_NOT_CORRECT));	// A blurred explanation
    		}
    
    		//
    		// Because authorization process is already ended,
    		// so if an error occurs in this process, it can output a true problem explanation in error message.
    		//
    
    		// Set Values to Database
    		int count;
    		try {
    			count = mapper.resetUserFailedCount(entity.getUserId());
    		} catch (Exception ex) {
    			LogUtil.info("searchOneUser : '" + ex.getMessage() + "'");
    			throw new Exception(MessageUtil.COMMON_ERROR_STRING_UNKNOWN_TROUBLE_HAS_OCCURRED);
    		}
    		if (count == 0) {
    			LogUtil.info("searchOneUser : 'This Account does not exist.'");
    			throw new Exception(MessageUtil.getMessage(MessageUtil.LOGIN_ERROR_TITLE_THIS_ACCOUNT_DOES_NOT_EXIST)); 
    		} else if (count > 1) {
    			LogUtil.info("searchOneUser : 'Assertion : Invalid count of manipulate data.'");
    			throw new Exception(MessageUtil.COMMON_ERROR_STRING_UNKNOWN_TROUBLE_HAS_OCCURRED);
    		}
    
    		return entity;
    	}
    
    	public void searchOneUserForSession(String userId, String tranName) throws Exception {
    
    		//
    		// Because authorization process is already ended,
    		// so if an error occurs in this process, it can output a true problem explanation in error message.
    		//
    
    		// Get Values from Database
    		SessionAuthEntity entity;
    		try {
    			entity = mapper.searchOneUserForSession(userId);
    		} catch (Exception ex) {
    			LogUtil.info("searchOneUserForSession : '" + ex.getMessage() + "'");
    			throw new Exception(MessageUtil.COMMON_ERROR_STRING_UNKNOWN_TROUBLE_HAS_OCCURRED);
    		}
    		if (entity == null) {
    			LogUtil.info("searchOneUserForSession : 'Yout Account does not exist.'");
    			throw new Exception(MessageUtil.getMessage(MessageUtil.USER_ERROR_TITLE_THIS_USER_DOES_NOT_EXIST));
    		}
    
    		// Check Account Is Locked or Not
    		if (entity.getUserLockedF().equals("Y")) {
    			LogUtil.info("searchOneUserForSession : 'Your Account was locked. Please logout, and try to login again.'");
    			throw new Exception(MessageUtil.getMessage(MessageUtil.ACCOUNT_ERROR_TITLE_YOUR_ACCOUNT_WAS_LOCKED_PLEASE_LOGOUT_AND_TRY_TO_LOGIN_AGAIN));
    		}
    
    		// Check Access Control
    		int count;
    		try {
    			count = mapper.searchOneUserTranCount(userId, tranName);
    		} catch (Exception ex) {
    			LogUtil.info("searchOneUserForSession : '" + ex.getMessage() + "'");
    			throw new Exception(MessageUtil.COMMON_ERROR_STRING_UNKNOWN_TROUBLE_HAS_OCCURRED);
    		}
    		if (count == 0) {
    			LogUtil.info("searchOneUserForSession : 'Your Account access is restricted.'");
    			throw new Exception(MessageUtil.getMessage(MessageUtil.ACCOUNT_ERROR_TITLE_YOUR_ACCOUNT_IS_RESTRICTED));
    		}
    
    		return;
    	}
    }
    
    説明:
    メインの処理を行います。データベースをアクセスしたり、エラーを発行したり。
    CryptoUtil.java
    ・・・
    
    package cn.com.xxxx.fmockup.util;
    
    import javax.crypto.Cipher;
    import javax.crypto.spec.SecretKeySpec;
    
    /**
     * Cripto Utility
     *   It encrypts or decrypts data.
     */
    
    public class CryptoUtil {
    
    	private static String charset = "utf-8";
    
    	/**
    	 * Cripto Utility
    	 *   It encrypts data.
    	 */
    	public static String encrypt(String inString, int charLength) throws Exception {
    
    		// Feature TODO : Change from "AES" to "AES/GCM/NoPadding".
    
    		String outString;
    		try {
    			SecretKeySpec key = new SecretKeySpec(PropertyUtil.getCryptoKey().getBytes(charset), "AES");
    			Cipher cipher = Cipher.getInstance("AES");
    			cipher.init(Cipher.ENCRYPT_MODE, key);
    			byte[] in = inString.getBytes(charset);
    			byte[] out = cipher.doFinal(in);
    			outString = new String(bin2hex(out));
    		} catch (Exception e) {
    			throw new Exception("Eecrypt trouble has occurred.");
    		}
    
    		int length = 16 * ( ( charLength * 6 / 16 ) + 1 ) * 2;
    
    		// Assertion
    		if (outString.length() > length) {
    			throw new Exception("Assertion : Invalid encrypt length (" + String.valueOf(outString.length()) + " > " + String.valueOf(length) + ")");
    		}
    
    		return outString;
    	}
    
    	/**
    	 * Cripto Utility
    	 *   It decrypts data.
    	 */
    	public static String decrypt(String inString) throws Exception {
    
    		String outString;
    		try {
    			SecretKeySpec key = new SecretKeySpec(PropertyUtil.getCryptoKey().getBytes(charset), "AES");
    			Cipher cipher = Cipher.getInstance("AES");
    			cipher.init(Cipher.DECRYPT_MODE, key);
    			byte[] in = hex2bin(inString);
    			byte[] out = cipher.doFinal(in);
    			outString = new String(out, charset);
    		} catch (Exception ex) {
    				throw new Exception("Decrypt trouble has occurred.");
    		}
    
    		return outString;
    	}
    
    	// bin to hex
    	private static String bin2hex(byte[] data) {
    		StringBuffer sb = new StringBuffer();
    		for (byte b : data) {
    			String s = Integer.toHexString(0xff & b);
    			if (s.length() == 1) {
    				sb.append("0");
    			}
    			sb.append(s);
    		}
    	    return sb.toString();
    	}
    
    	// hex to bin
    	private static byte[] hex2bin(String hex) {
    		byte[] bytes = new byte[hex.length() / 2];
    		for (int index = 0; index < bytes.length; index++) {
    			bytes[index] = (byte) Integer.parseInt(hex.substring(index * 2, (index + 1) * 2), 16);
    		}
    		return bytes;
    	}
    }
    
    説明:
    パスワードのデクリプト(復号化)やエンクリプト(暗号化)の処理を行います。ログイン時においては、データベースの暗号化されたパスワードをデクリプト(復号化)して、ログイン画面のパスワードと一致するか否かを確認します。
    StatusResponse.java
    ・・・
    
    package cn.com.xxxx.fmockup.response;
    
    import java.util.HashMap;
    import java.util.Map;
    
    // JSON format:
    //
    // {
    //   "status"     : "status-string",
    //   "message"    : "message-string",
    //   "messageMap" : {
    //       "key"    : "value",
    //       ...
    //   }
    // }
    
    /**
     * JSON Response Structure
     *   All Web-API return this JSON.
     */
    
    public class StatusResponse {
    
    	private String status;
    	private String message;
    	private Map<String, String> messageMap = new HashMap<>();
    
    	public void setStatus(String status) {
    		this.status = status;
    	}
    
    	public String getStatus() {
    		return this.status;
    	}
    
    	public void setMessage(String message) {
    		this.message = message;
    	}
    
    	public String getMessage() {
    		return this.message;
    	}
    
    	public void addMessageMap(String key, String message) {
    		this.messageMap.put(key, message);
    	}
    
    	public Map<String, String> getMessageMap() {
    		return this.messageMap;
    	}
    }
    
    説明:
    レスポンスデータ(JSON)を返却するためのものです。

参考

GET と POST

Controller

Validation

Controller、Service、Model の使い分け

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?