前提
Javaの学習のため、Spring Bootを使用してWebアプリケーションを作成しています。
同一ユーザの多重ログインを禁止したく、webで色々と調べたのですが、上手く出来ず困っています。
※2つのブラウザから同一ユーザ名とパスワードでログイン出来てしまう状態。
環境
Java 8
Spring Boot 2.3.0
JPA
H2 Database
Thymeleaf
BootStrap 4.5.0
試したこと
まず、WebSecurityConfigurerAdapterを継承したクラスで、
以下のように最大セッション数を1にしました。
Java
1@EnableWebSecurity 2public class SecurityConfig extends WebSecurityConfigurerAdapter { 3 (省略) 4 @Override 5 protected void configure(HttpSecurity http) throws Exception { 6 (省略) 7 http.sessionManagement().maximumSessions(1); 8 (省略) 9}
その上でセッション作成を検知するために、
AbstractSecurityWebApplicationInitializerを継承したクラスで、
enableHttpSessionEventPublisher()がtrueを返すようにオーバーライドしました。
Java
1package com.example; 2 3import org.springframework.core.annotation.Order; 4import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; 5 6@Order(1) 7public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer { 8 9 public SecurityWebApplicationInitializer() { 10 super(SecurityConfig.class); 11 } 12 13 protected boolean enableHttpSessionEventPublisher() { 14 return true; 15 } 16 17}
問題
上記のとおり実装しましたが、
2つのブラウザから同一ユーザ名とパスワードでログイン出来てしまう状態です。
そこで、デバッグしたり、カバレッジを確認したりしたところ、
新規に作成したSecurityWebApplicationInitializerクラスが呼ばれていないことが分かりました。
(正確に書くと実装したコンストラクタやメソッドが呼び出されていない状態)
サーブレット初期化時にAbstractSecurityWebApplicationInitializerを継承したクラスがあれば自動でフックされるという記事があったので、
上記の実装で呼び出されると思っていましたが、そうでもないようです。
何か気づくことがあればアドバイスをお願い致します。
追記
問題のプロジェクトとは別に、
多重ログイン禁止処理を確かめるためのプロジェクトを新規に作成して試してみました。
すると、WebSecurityConfigurerAdapterを継承したクラスに、
上記と同様、最大セッション数を1とするコードを書くと2重ログインを禁止出来ました。
そこから少しずつ、問題のプロジェクトに近づけていくと、
ログインに使用しているUSERテーブルのNAMEカラムを外部キーにもつMUTTERテーブルを作成し、
USERテーブルと関連性をもたせたタイミングで2重ログインが禁止できなくなりました。
ここからは予想なのですが、この記事によると、
多重ログインチェックを機能させるためには、UserDetailsを実装したクラスの中で、
equalsとhashCodeメソッドのオーバーライドを適切に行うことが必要らしく、
それが出来ていないのではないかと考えています。
その観点から考えたのですが、結局何が問題か分からず、アドバイスをお願い致します。
なお、DB操作にはJPAを利用しています。
[Userエンティティクラス]
Java
1package sample.spring.domain; 2 3import java.io.Serializable; 4import java.util.List; 5 6import javax.persistence.CascadeType; 7import javax.persistence.Column; 8import javax.persistence.Entity; 9import javax.persistence.FetchType; 10import javax.persistence.GeneratedValue; 11import javax.persistence.GenerationType; 12import javax.persistence.Id; 13import javax.persistence.OneToMany; 14import javax.persistence.Table; 15 16import com.fasterxml.jackson.annotation.JsonIgnore; 17 18import lombok.AllArgsConstructor; 19import lombok.Data; 20import lombok.NoArgsConstructor; 21import lombok.ToString; 22 23@Entity 24@Table(name = "USER") 25@Data 26@NoArgsConstructor 27@AllArgsConstructor 28@ToString(exclude = "mutters") 29public class User implements Serializable { 30 private static final long serialVersionUID = 1L; 31 32 @Id 33 @GeneratedValue(strategy = GenerationType.IDENTITY) 34 @Column(name = "USER_ID") 35 private Integer Id; 36 37 @Column(unique = true, nullable = false, name = "NAME", length = 64) 38 private String name; 39 40 @Column(nullable = false, name = "PASS", length = 80) 41 @JsonIgnore 42 private String pass; 43 44 // 以下をコメントアウトすると2重ログインが禁止出来る 45 @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "user") 46 private List<Mutter> mutters; 47 48}
[UserDetailsを実装したUserクラスを継承したクラス]
Java
1package sample.spring.service; 2 3import java.util.Collection; 4 5import org.springframework.security.core.GrantedAuthority; 6 7import sample.spring.domain.User; 8 9import lombok.Data; 10import lombok.EqualsAndHashCode; 11 12@Data 13@EqualsAndHashCode(callSuper = true) 14public class LoginUserDetails extends org.springframework.security.core.userdetails.User { 15 private static final long serialVersionUID = 1L; 16 private final User user; 17 18 public LoginUserDetails(User user, Collection<GrantedAuthority> authorities) { 19 super(user.getName(), user.getPass(), authorities); 20 this.user = user; 21 } 22 23 /* lombokを使わずに実装してみた 24 * 25 * public User getUser() { 26 * return user; 27 * } 28 * 29 * @Override 30 * public int hashCode() { 31 * return user.getName().hashCode(); 32 * } 33 * 34 * @Override 35 * public boolean equals(Object rhs) { 36 * if (rhs instanceof User) { 37 * return user.getName().equals(((LoginUserDetails) rhs).user.getName()); } 38 * return false; 39 * } 40 * 41 * @Override 42 * public String toString(){ 43 * StringBuilder sb = new StringBuilder(); 44 * sb.append("Username: ").append(this.user.getName()).append("; "); 45 * sb.append("Password: [PROTECTED]; "); 46 * 47 * return sb.toString(); 48 * } 49 */ 50 51}
その他のクラスはGitHubにソースを配置したので、
必要であればご確認をお願い致します。
ウェブアプリのユーザ名とパスワードはそれぞれuser1とdemoです。
https://github.com/uekiGityuto/test-session-control
補足
上記で省略していた問題のあるプロジェクトで使用しているSecurityConfigの全量です。
Java
1package com.example; 2 3import org.springframework.beans.factory.annotation.Autowired; 4import org.springframework.context.annotation.Bean; 5import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 6import org.springframework.security.config.annotation.web.builders.HttpSecurity; 7import org.springframework.security.config.annotation.web.builders.WebSecurity; 8import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 9import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 10import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 11import org.springframework.security.crypto.password.PasswordEncoder; 12 13import com.example.service.CustomAuthenticationFailurehandler; 14import com.example.service.LoginUserDetailsService; 15 16//アプリ起動時に読み込まれるコンフィグファイル 17@EnableWebSecurity 18public class SecurityConfig extends WebSecurityConfigurerAdapter { 19 @Autowired 20 LoginUserDetailsService userDetailsService; 21 22 // アクセスフィルター(AuthenticationFilter)のカスタマイズ 23 @Override 24 public void configure(WebSecurity web) throws Exception { 25 web.ignoring().antMatchers("/webjars/**", "/css/**"); 26 } 27 28 // アクセスフィルター(AuthenticationFilter)のカスタマイズ 29 @Override 30 protected void configure(HttpSecurity http) throws Exception { 31 // ログインページを指定 32 // ログインページへのアクセスは全員許可する 33 http.formLogin().loginPage("/index").loginProcessingUrl("/authenticate").usernameParameter("name") 34 .passwordParameter("pass").defaultSuccessUrl("/main", true) 35 // failureHandlerを呼ばない場合は認証エラー時に"/index?error"にリダイレクトする 36 .failureHandler(new CustomAuthenticationFailurehandler()).permitAll(); 37 38 // ユーザ登録ページへのアクセスは全員許可する。 39 // それ以外は認証が必要とする 40 // "/management/"以下はROLE_ADMINのユーザのみ認可する 41 http.authorizeRequests().antMatchers("/index**").permitAll().antMatchers("/user/registration").permitAll() 42 .antMatchers("/user/registerResult").permitAll().antMatchers("/user/gotoTop").permitAll() 43 .antMatchers("/management/**").hasRole("ADMIN")// 'ROLE_'はつけない 44 .anyRequest().authenticated(); 45 46 // ログアウト後に遷移するページを指定 47 http.logout().logoutSuccessUrl("/index"); 48 49 // 二重ログインを禁止 50 http.sessionManagement().maximumSessions(1); 51 } 52 53 // パスワードのエンコード方式を指定 54 @Bean 55 PasswordEncoder passwordEncoder() { 56 return new BCryptPasswordEncoder(); 57 } 58 59 // 認証処理(AuthenticationProvider)のカスタマイズ 60 @Autowired 61 void configureAuthenticationManager(AuthenticationManagerBuilder auth) throws Exception { 62 auth.userDetailsService(userDetailsService)// DaoAuthenticationProviderが使用するUserDetailsServiceを指定 63 .passwordEncoder(passwordEncoder());// DaoAuthenticationProviderが使用するPasswordEncoderを指定 64 } 65 66}
以上、宜しくお願い致します。
回答1件
あなたの回答
tips
プレビュー