package sample.context.security; import java.io.IOException; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Lazy; import org.springframework.http.MediaType; import org.springframework.security.authentication.*; import org.springframework.security.config.annotation.web.builders.*; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.*; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.*; import org.springframework.security.web.authentication.logout.*; import org.springframework.web.filter.*; import lombok.*; import sample.ValidationException.ErrorKeys; import sample.context.actor.ActorSession; import sample.context.security.SecurityActorFinder.*; /** * Spring Security(認証/認可)全般の設定を行います。 * <p>認証はベーシック認証ではなく、HttpSessionを用いた従来型のアプローチで定義しています。 * <p>設定はパターンを決め打ちしている関係上、既存の定義ファイルをラップしています。 * securityプリフィックスではなくextension.securityプリフィックスのものを利用してください。 * <p>low: HttpSessionを利用しているため、横スケールする際に問題となります。その際は上位のL/Bで制御するか、 * SpringSession(HttpSessionの実装をRedis等でサポート)を利用する事でコード変更無しに対応が可能となります。 * <p>low: 本サンプルでは無効化していますが、CSRF対応はプロジェクト毎に適切な利用を検討してください。 */ @Setter @Getter public class SecurityConfigurer extends WebSecurityConfigurerAdapter { /** Spring Boot のサーバ情報 */ @Autowired private ServerProperties serverProps; /** 拡張セキュリティ情報 */ @Autowired private SecurityProperties props; /** 認証/認可利用者サービス */ @Autowired @Lazy private SecurityActorFinder actorFinder; /** カスタムエントリポイント(例外対応) */ @Autowired @Lazy private SecurityEntryPoint entryPoint; /** ログイン/ログアウト時の拡張ハンドラ */ @Autowired @Lazy private LoginHandler loginHandler; /** ThreadLocalスコープの利用者セッション */ @Autowired private ActorSession actorSession; /** CORS利用時のフィルタ */ @Autowired(required = false) private CorsFilter corsFilter; /** 認証配下に置くServletFilter */ @Autowired(required = false) private SecurityFilters filters; @Override public void configure(WebSecurity web) throws Exception { web.ignoring().mvcMatchers(serverProps.getPathsArray(props.auth().getIgnorePath())); } @Override protected void configure(HttpSecurity http) throws Exception { // Target URL http .authorizeRequests() .mvcMatchers(props.auth().getExcludesPath()).permitAll(); http .csrf().disable() .authorizeRequests() .mvcMatchers(props.auth().getPathAdmin()).hasRole("ADMIN") .mvcMatchers(props.auth().getPath()).hasRole("USER"); // Common http .exceptionHandling().authenticationEntryPoint(entryPoint); http .sessionManagement() .maximumSessions(props.auth().getMaximumSessions()) .and() .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED); http .addFilterAfter(new ActorSessionFilter(actorSession), UsernamePasswordAuthenticationFilter.class); if (corsFilter != null) { http.addFilterBefore(corsFilter, LogoutFilter.class); } if (filters != null) { for (Filter filter : filters.filters()) { http.addFilterAfter(filter, ActorSessionFilter.class); } } // login/logout http .formLogin().loginPage(props.auth().getLoginPath()) .usernameParameter(props.auth().getLoginKey()).passwordParameter(props.auth().getPasswordKey()) .successHandler(loginHandler).failureHandler(loginHandler) .permitAll() .and() .logout().logoutUrl(props.auth().getLogoutPath()) .logoutSuccessHandler(loginHandler) .permitAll(); } /** * Spring Securityのカスタム認証プロバイダ。 * <p>主にパスワード照合を行っています。 */ public static class SecurityProvider implements AuthenticationProvider { @Autowired private SecurityActorFinder actorFinder; @Autowired @Lazy private PasswordEncoder encoder; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { if (authentication.getPrincipal() == null || authentication.getCredentials() == null) { throw new BadCredentialsException("ログイン認証に失敗しました"); } SecurityActorService service = actorFinder.detailsService(); UserDetails details = service.loadUserByUsername(authentication.getPrincipal().toString()); String presentedPassword = authentication.getCredentials().toString(); if (!encoder.matches(presentedPassword, details.getPassword())) { throw new BadCredentialsException("ログイン認証に失敗しました"); } UsernamePasswordAuthenticationToken ret = new UsernamePasswordAuthenticationToken( authentication.getName(), "", details.getAuthorities()); ret.setDetails(details); return ret; } @Override public boolean supports(Class<?> authentication) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); } } /** * Spring Securityのカスタムエントリポイント。 * <p>API化を念頭に例外発生時の実装差込をしています。 */ public static class SecurityEntryPoint implements AuthenticationEntryPoint { @Autowired private MessageSource msg; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { if (response.isCommitted()) { return; } if (authException instanceof InsufficientAuthenticationException) { String message = msg.getMessage(ErrorKeys.AccessDenied, new Object[0], Locale.getDefault()); writeReponseEmpty(response, HttpServletResponse.SC_FORBIDDEN, message); } else { String message = msg.getMessage(ErrorKeys.Authentication, new Object[0], Locale.getDefault()); writeReponseEmpty(response, HttpServletResponse.SC_UNAUTHORIZED, message); } } private void writeReponseEmpty(HttpServletResponse response, int status, String message) throws IOException { response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(status); response.setCharacterEncoding("UTF-8"); response.getWriter().write("{\"message\": \"" + message + "\"}"); } } /** * SpringSecurityの認証情報(Authentication)とActorSessionを紐付けるServletFilter。 * <p>dummyLoginが有効な時は常にSecurityContextHolderへAuthenticationを紐付けます。 */ @AllArgsConstructor public static class ActorSessionFilter extends GenericFilterBean { private ActorSession actorSession; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { Optional<Authentication> authOpt = SecurityActorFinder.authentication(); if (authOpt.isPresent() && authOpt.get().getDetails() instanceof ActorDetails) { ActorDetails details = (ActorDetails) authOpt.get().getDetails(); actorSession.bind(details.actor()); try { chain.doFilter(request, response); } finally { actorSession.unbind(); } } else { actorSession.unbind(); chain.doFilter(request, response); } } } /** * Spring Securityにおけるログイン/ログアウト時の振る舞いを拡張するHandler。 */ @Getter @Setter public static class LoginHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler, LogoutSuccessHandler { @Autowired private SecurityProperties props; /** ログイン成功処理 */ @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { Optional.ofNullable((ActorDetails) authentication.getDetails()).ifPresent( (detail) -> detail.bindRequestInfo(request)); if (response.isCommitted()) { return; } writeReponseEmpty(response, HttpServletResponse.SC_OK); } /** ログイン失敗処理 */ @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { if (response.isCommitted()) { return; } writeReponseEmpty(response, HttpServletResponse.SC_BAD_REQUEST); } /** ログアウト成功処理 */ @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { if (response.isCommitted()) { return; } writeReponseEmpty(response, HttpServletResponse.SC_OK); } private void writeReponseEmpty(HttpServletResponse response, int status) throws IOException { response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setStatus(status); response.getWriter().write("{}"); } } }