Web 開発再入門 #10 ― コントローラー処理(Web API もどき)
fmockup
はじめに
Web アプリケーションのサーバー・サイドを開発します。
MVC の Controller 部分となります。
MVC:Model View Controller
画面ショット
ログイン画面で認証 Web API を呼び出したとき、Web API の Controller のバリデーションでエラーを出しています。
フォルダー・ファイル構成
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)なのだと思います。
ファイルの作成
- 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 の使い分け