フロント側をFlutter(スマホ)Thymeleaf(PC)、バックエンド側SpringBootの自動作成勉強中
10:03
2022年2月9日で中断していたので、過去どんなことやったか、この日記で思い出している。
2月9日に「現在自動作成できているPC側WEBのThymeleafを残して、そのSpringBootロジックをそのまま使って、スマホ(Flutter)を作れないかな?するとPC・WEBからもスマホからも同じロジックで処理できる。ので、管理が楽になるはず。」と書いてあるので、これに向かって調査していたことになる。
MouseComputerのノートPCのjavaSpringBootプログラムを、どの辺まで作っていたか確認している。
■Controllerはこんな感じに書いていた。
①package com.kaz02u.demo.controller.adminComm;にFlutter向け、PC向けの共通ロジックをabstract classにする。←abstractはいらない気がするけど。
/** * ユーザー情報管理の共通コントローラー * */ @Controller ////リクエストマッピングURL指定 //@RequestMapping(value = "/members/admin/user/userA") //log出力用 @Slf4j public abstract class UserCommController { /** * 一覧表示時の、ソート項目の項目名(Entityの項目名。テーブル項目名ではない) */ protected static String[] sortItemNames = {"id", "name", "email", "roles", "enableFlag"}; /** * PageableDefaultのsize(1画面中の表示レコード数)を指定 */ public static final int pageableDefaultSize = 10; @Autowired protected AppProperties appProperties; @Autowired protected UserService userService; @Autowired protected SessionUserSrchForm sessionUserSrchForm; @Autowired protected SessionUserSrchOrderForm sessionUserSrchOrderForm; @Autowired protected MessageSource messageSource; //======================================================= // ユーザー情報一覧登録 //======================================================= //======================================================= // ユーザー情報登録 //======================================================= /** * リターン共通処理(Flutter、PC・WEB共用) * リターン共通処理 * * @param url 遷移先 * @param model モデル * @param result チェック結果 * @param flutterFlg true:Flutter用 false:PC・WEB用 * @return Flutter用String:jsonデータ PC・WEB用 Map<String, Object>:遷移先 */ protected Object returnComm(String url ,Model model,BindingResult result, boolean flutterFlg) { if (flutterFlg) { ResData resData = new ResData(model, result); return resData.getResDataMap(messageSource); } else { return url; } } /** * userInsSubメソッド * ユーザー情報登録を表示する処理のサブモジュール * * @param model モード */ protected void userInsSub(Model model) { UserForm userForm = new UserForm(); model.addAttribute("userForm", userForm); model.addAttribute("mode", "ins"); } /** * ユーザー情報登録共通処理(Flutter、PC・WEB共用) * ユーザー情報登録する共通処理 * * @param userForm ユーザー情報登録 * @param result チェック結果 * @param model モデル * @param flutterFlg true:Flutter用 false:PC・WEB用 * @return Flutter用:jsonデータ PC・WEB用:遷移先 */ @SuppressWarnings("unchecked") protected Object userInsDoComm(UserForm userForm, BindingResult result, Model model, boolean flutterFlg) { if (flutterFlg) { model.addAttribute("userForm", userForm); } //エラーになったときのモードを設定 model.addAttribute("mode", "ins"); if (result.hasErrors()) { model.addAttribute("errorMessage", "エラーが発生しました"); model.addAttribute("itemErrorMessages", result.toString()); return returnComm("/members/admin/user/userRegister", model, result, flutterFlg); } try { //ロールを文字列にする String[] roles = userForm.getRolesArray(); userService.register(userForm.getName(), userForm.getEmail(), userForm.getPassword(), roles, userForm.getEnableFlag()); } catch(Exception e){ if (e.getMessage() != null && e.getMessage().equals("invalid email")) { result.rejectValue("email", "validation.already-registered", new String[] {"『メールアドレス』"}, ""); model.addAttribute("itemErrorMessages", result.toString()); } else { e.printStackTrace(); log.error("エラーが発生しました", e); model.addAttribute("errorMessage", "エラーが発生しました"); } return returnComm("/members/admin/user/userRegister", model, result, flutterFlg); } userInsSub(model); model.addAttribute("successMessage", "ユーザー情報登録が完了しました"); return returnComm("/members/admin/user/userRegister", model, result, flutterFlg); } }
②package com.kaz02u.demo.controller.adminPc;にPC向けのロジックを登録する。
/** * ユーザー情報管理のコントローラー * */ @Controller //リクエストマッピングURL指定 @RequestMapping(value = "/members/admin/user/userA") //log出力用 @Slf4j public class UserPcController extends UserCommController { //======================================================= // ユーザー情報一覧登録 //======================================================= //======================================================= // ユーザー情報登録 //======================================================= /** * ユーザー情報登録表示処理 (PC・WEB用) * ユーザー情報登録を表示する処理 * * @param userForm ユーザー情報登録 * @param mode モード * @param model モデル * @return 遷移先 */ @PostMapping(params="mode=ins") public String userIns(@RequestParam("mode") String mode, Model model) { userInsSub(model); return (String)returnComm("/members/admin/user/userRegister", model, null, false); } /** * ユーザー情報登録処理(PC・WEB用) * ユーザー情報登録する処理 * * @param userForm ユーザー情報登録 * @param result チェック結果 * @param mode モード * @param model モデル * @return 遷移先 */ @PostMapping(params="mode=ins_do") public String userInsDo(@Validated(GroupOrder.class) UserForm userForm, BindingResult result, @RequestParam("mode") String mode, Model model) { return (String)userInsDoComm(userForm, result, model, false); } }
③package com.kaz02u.demo.controller.adminFlutter;にFlutter向けのロジックを登録する。
|/** * ユーザー情報管理のコントローラー * */ @Controller //log出力用 @Slf4j public class UserFlutterController extends UserCommController { //======================================================= // ユーザー情報一覧登録 //======================================================= //======================================================= // ユーザー情報登録 //======================================================= /** * ユーザー情報登録表示処理(Flutter用) * ユーザー情報登録を表示する処理 * * @param userForm ユーザー情報登録 * @param mode モード * @param model モデル * @return jsonデータ */ @SuppressWarnings("unchecked") @PostMapping("/members/admin/user/userA/ins") @ResponseBody public Map<String, Object> userInsFlutter(@RequestParam("mode") String mode, Model model) { userInsSub(model); return (Map<String, Object>)returnComm("/members/admin/user/userRegister", model, null, true); } /** * ユーザー情報登録処理(Flutter用) * ユーザー情報登録する処理 * * @param userForm ユーザー情報登録 * @param result チェック結果 * @param mode モード * @param model モデル * @return jsonデータ */ @SuppressWarnings("unchecked") @PostMapping("/members/admin/user/userA/ins_do") @ResponseBody public Map<String, Object> userInsDoFlutter(@RequestBody @Validated(GroupOrder.class) UserForm userForm, BindingResult result, Model model) { return (Map<String, Object>)userInsDoComm(userForm, result, model, true); } }
④そして、Flutter用共通例外
/** * すべてのコントローラー(Flutter用)に共通する例外処理クラス(ControllerAdviceクラス) * 共通例外(”システムに問題が発生しました”)を戻す。 * */ @ControllerAdvice(basePackages="com.kaz02u.demo.controller.adminFlutter") //log出力用 @Slf4j public class CustomControllerAdviceAdminFlutter { @ResponseBody @ExceptionHandler public Map<String, Object> handleException(Throwable e) { log.error("System Error occurred.", e); e.printStackTrace(); Map<String, Object> map = new HashMap<String, Object>(); map.put("errorMessage", "システムに問題が発生しました"); map.put("mode", "SystemError"); return map; } }
⑤そして、Pc用共通例外
/** * すべてのコントローラー(PC・WEB用)に共通する例外処理クラス(ControllerAdviceクラス) * 共通例外(”システムに問題が発生しました”)を戻す。 * */ @ControllerAdvice(basePackages="com.kaz02u.demo.controller.adminPc") //log出力用 @Slf4j public class CustomControllerAdviceAdminPc { @ExceptionHandler public String handleException(Throwable e) { log.error("System Error occurred.", e); e.printStackTrace(); return "error/error.html"; } }
■WebSecurityConfigはこんな感じに書いていた。
①package com.kaz02u.demo;のWebSecurityConfig.java
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private SimpleAuthenticationEntryPoint authenticationEntryPoint; @Autowired private SimpleAccessDeniedHandler accessDeniedHandler; @Autowired private SimpleAuthenticationSuccessHandler authenticationSuccessHandler; @Autowired private SimpleAuthenticationFailureHandler authenticationFailureHandler; @Autowired private SimpleLogoutSuccessHandler logoutSuccessHandler; // アカウント登録時のパスワードエンコードで利用するためDI管理する。 @Bean PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } @Override public void configure(WebSecurity web) throws Exception { web.debug(false).ignoring().antMatchers("/js/**", "/css/**", "/img/**") // 静的リソース(js、css、img)に対するアクセスはセキュリティ設定を無視する ; } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().mvcMatchers("/", "/login", "/logout", "/members/index", "/error/**").permitAll() // 全ユーザーアクセス許可 .mvcMatchers("/members/user/**", "/images/user/**", "/upload/**", "/elements").hasRole("USER") // ユーザーロール(※1)がアクセス許可 .mvcMatchers("/members/admin/**", "/images/admin/**", "/upload/**", "/elements").hasRole("ADMIN") // 管理者ロール(※1)がアクセス許可 .anyRequest().authenticated() // それ以外は全て認証無しの場合アクセス不許可 .and() .exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler) .and() .formLogin() .loginProcessingUrl("/login") // 認証処理のパス .loginPage("/login").permitAll() // ログイン画面のURL .successHandler(authenticationSuccessHandler) .failureHandler(authenticationFailureHandler) .and() .logout() .logoutUrl("/logout") .invalidateHttpSession(true) // ログアウトしたらセッションを無効にする .deleteCookies("JSESSIONID") // ログアウトしたら cookieの JSESSIONID を削除 .logoutSuccessHandler(logoutSuccessHandler) .and() .csrf() .ignoringAntMatchers("/login") // ログインAPIはCSRF対策不要にする .csrfTokenRepository(new CookieCsrfTokenRepository()) // CSRFトークンをcookieに保持する標準実装クラスCookieCsrfTokenRepository ; //※1:Userテーブルのrolesのカラムに入っている"ROLE_USER"、"ROLE_ADMIN"の"ROLE_"部分を取り除いたもので認証される。 } @Bean public SimpleAuthenticationFailureHandler simpleAuthenticationFailureHandler() { return new SimpleAuthenticationFailureHandler(); } // //ログアウトが正常終了した時の処理を実装したハンドラ // //HTTPステータスを返すだけのSpring Securityの標準実装クラスHttpStatusReturningLogoutSuccessHandlerを使う。 // //this.httpStatusToReturnに、HttpStatus.OKを戻す。 // @Bean // public LogoutSuccessHandler logoutSuccessHandler() { // return new HttpStatusReturningLogoutSuccessHandler(); // } @Bean public SimpleAccessDeniedHandler simpleAccessDeniedHandler() { return new SimpleAccessDeniedHandler(); } }
②package com.kaz02u.demo.auth;のSimpleAuthenticationEntryPoint.java
@Component //log出力用 @Slf4j /* * 未認証のユーザーが認証の必要なAPIにアクセスしたときの処理 * * デフォルトや用意されている標準実装クラスは利用せず、 * HTTPステータス403を返すだけの処理を実装。補足:PC・WEB側は自動的に/error/403.htmlが表示される。 * */ public class SimpleAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { if (response.isCommitted()) { log.info("Response has already been committed."); return; } // HTTPステータス403を戻す response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase()); } }
③package com.kaz02u.demo.auth;のSimpleAccessDeniedHandler.java
//log出力用 @Slf4j /* * ユーザーは認証済みだが未認可のリソースへアクセスしたときの処理 * * デフォルトや用意されている標準実装クラスは利用せず、 * HTTPステータス403を返すだけの処理を実装。補足:PC・WEB側は自動的に/error/403.htmlが表示される。 * */ public class SimpleAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException, ServletException { if (response.isCommitted()) { log.info("Response has already been committed."); return; } // HTTPステータス403を戻す response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase()); } }
④package com.kaz02u.demo.auth;のSimpleAuthenticationSuccessHandler.java
@Component //log出力用 @Slf4j /* * 認証が成功した時の処理を実装したハンドラを設定 * * デフォルトや用意されている標準実装クラスは利用せず、HTTPステータス200を返すだけのハンドラを実装 */ public class SimpleAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { if (response.isCommitted()) { log.info("Response has already been committed."); return; } // クッキーにfromFlutterFlgが入っている場合(Flutter画面) if (Functions.isFromFlutter(request)) { response.setStatus(HttpStatus.OK.value()); clearAuthenticationAttributes(request); } else { response.setStatus(HttpServletResponse.SC_OK); response.sendRedirect("/members/index"); } } /** * Removes temporary authentication-related data which may have been stored in the * session during the authentication process. * このメソッドは、SimpleUrlAuthenticationSuccessHandler.javaからコピーしました。 */ private void clearAuthenticationAttributes(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session == null) { return; } session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); } }
⑤package com.kaz02u.demo.auth;のSimpleAuthenticationFailureHandler.java
/* * 認証が失敗した時の処理を実装したハンドラを設定 * * デフォルトや用意されている標準実装クラスは利用せず、HTTPステータス401と * デフォルトのメッセージを返すだけのハンドラを実装 */ public class SimpleAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { // クッキーにfromFlutterFlgが入っている場合(Flutter画面) if (Functions.isFromFlutter(request)) { // HTTPステータス401を戻す response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); } else { response.setStatus(HttpServletResponse.SC_OK); response.sendRedirect("/login?error=true"); } } }
⑥package com.kaz02u.demo.auth;のSimpleLogoutSuccessHandler.java
@Component //log出力用 @Slf4j public class SimpleLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler implements LogoutSuccessHandler{ @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth) throws IOException, ServletException { if (auth != null && auth.getDetails() != null) { log.info("ログアウト: " + auth.getDetails().toString()); } else { log.info("ログアウト"); } // クッキーにfromFlutterFlgが入っている場合(Flutter画面) if (Functions.isFromFlutter(request)) { // デフォルト処理 super.onLogoutSuccess(request, response, auth); } else { response.setStatus(HttpServletResponse.SC_OK); //redirect to login response.sendRedirect("/login"); } } }
⑦package com.kaz02u.demo.utils;のFunctions.java ←Flutterからの時、requestのクッキーに"fromFlutter"を忍ばせておく方法。ログオン確認はこれで切り分ける。補足:コントローラーはURLで切り分ける。
/** * リクエストのFlutter、PC・WEBチェック。 * @param request リクエスト * @return true:Flutter、false:PC・WEB */ public static boolean isFromFlutter(HttpServletRequest request) { boolean flg = true; Cookie[] cookies = request.getCookies(); String fromFlutter = ""; if (cookies != null) { for (Cookie cook : cookies) { if (cook.getName().equals("fromFlutter")) { fromFlutter = cook.getValue(); } } } // fromFlutterFlgが入っていない場合(PC・WEB画面) if (ObjectUtils.isEmpty(fromFlutter)) { flg = false; } return flg; }