kazpgmの日記

『プログラム自動作成@自動生成』作成の日記

フロント側を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;
	}