くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Spring BootとSpring Securityでユーザ認証(インメモリ&独自のユーザテーブル)

Spring BootとSpring Securityのユーザ認証について、忘れがちになるので、備忘録φ(..)メモメモ

認証の方法はいくつかあるけど、今回は、

  • 暫定対応時のインメモリDB
  • 本格対応時の独自ユーザテーブル

の2つをメモφ(..)メモメモ

まずはbuild.gradleにDependencyに追加する

Spring Securityを使うには、spring-boot-starter-securityを追加

ロール/権限によるthymeleafのテンプレートの制御のため、thymeleaf-extras-springsecurity4も追加

(この記事では登場しません。。。まだこんど。。。)

dependencies {
  //SpringBootのDependency
  compile('org.springframework.boot:spring-boot-starter-data-jpa')
  compile('org.springframework.boot:spring-boot-starter-thymeleaf')
  compile('org.springframework.boot:spring-boot-starter-web')

  //SpringSecurityを使うため、追加
  compile('org.springframework.boot:spring-boot-starter-security')
  compile('org.thymeleaf.extras:thymeleaf-extras-springsecurity4')

  ・・・
}

ログインフォームはこんな感じ

usernameにユーザIDが、passwordにパスワードが詰まって、/loginに遷移する感じ

認証が失敗したら、アラートが出るようにメッセージとth:ifを設定

<html lang="ja" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
  xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<meta charset=" utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>タイトル</title>
</head>
<body onload="document.login.username.focus();">
  <!-- ログインが失敗した時のメッセージ -->
  <div class="alert alert-danger" role="alert"
    th:if="${session['SPRING_SECURITY_LAST_EXCEPTION']} != null">
    <strong>ログインに失敗しました: </strong>ユーザが存在しないか、パスワードが間違っています。
  </div>

  <!-- ログインのフォーム -->
  <form class="form-signin" action="" th:action="@{/login}" method="post" name="login">
    <h2 class="form-signin-heading">ログイン画面</h2>
    <input type="text" class="form-control" placeholder="ユーザID" name="username" />
    <input type="password" class="form-control" placeholder="パスワード" name="password" />
    <button class="btn btn-lg btn-primary btn-block" type="submit">ログイン</button>
  </form>
</body>
</html>

どちらの認証でも必要な下準備

まずは、Spring Securityを有効にするためのConfigクラスを作成

WebSecurityConfigurerAdapterを継承したクラスを用意

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
          // アクセス権限の設定
          // staticディレクトリにある、'/css/','fonts','/js/'は制限なし
          .antMatchers("/css/**", "/fonts/**", "/js/**").permitAll()
          // '/admin/'で始まるURLには、'ADMIN'ロールのみアクセス可
          .antMatchers("/admin/**").hasRole("ADMIN")
          // 他は制限なし
          .anyRequest().authenticated()
        .and()
          // ログイン処理の設定
          .formLogin()
            // ログイン処理のURL
            .loginPage("/login")
            // usernameのパラメタ名
            .usernameParameter("username")
            // passwordのパラメタ名
            .passwordParameter("password")
            .permitAll()
        .and()
          // ログアウト処理の設定
          .logout()
            // ログアウト処理のURL
            .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
            // ログアウト成功時の遷移先URL
            .logoutSuccessUrl("/login")
            // ログアウト時に削除するクッキー名
            .deleteCookies("JSESSIONID")
            // ログアウト時のセッション破棄を有効化
            .invalidateHttpSession(true)
            .permitAll()
        ;
    }

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //TODO インメモリDBと独自ユーザテーブルで処理が変わる部分
  }
}

インメモリDBでユーザ認証を実装するには

インメモリDBでの認証は、超簡単!!

これだけ!!

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 ・・・

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
      // ユーザ名'user', パスワード'user',ロール'USER'のユーザを追加
      .withUser("user").password("user").roles("USER")
      .and()
      // ユーザ名'admin', パスワード'admin',ロール'ADMIN'のユーザを追加
      .withUser("admin").password("admin").roles("ADMIN");
  }
}

独自のユーザテーブルでユーザ認証を実装するには

独自のユーザテーブルは、認証だけではなく、ユーザ情報などを自由に追加できるので、柔軟に設定できる。

でも、その分、やらなければいけないことが多い。。。

  1. ユーザテーブルのEntity
  2. ユーザテーブルのRepository
  3. 認証用のService

細かいことは、Serviceに任されるので、WebSecurityConfigはとても簡潔

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  @Autowired
  private UserInfoService userInfoService;

  (中略)

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userInfoService);
  }
}

ユーザテーブルのEntityをつくる

認証に利用するEntityには、UserDetailsの実装が必要

Entityはこんな感じ。@Columnのgetter/setterはLombokで生成!

@Setter
@Getter
@Entity
@Table(name = "user_info")
public class UserInfoEntity implements UserDetails {
  private static final long serialVersionUID = 1L;
  public enum Authority {ROLE_USER, ROLE_ADMIN};

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long userId;

  @Column(nullable = false, unique = true)
  private String username;
  @Column(nullable = false)
  private String password;
  @Column
  private String email;

  @Column(nullable = false)
  @Enumerated(EnumType.STRING)
  private Authority authority;

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    List<GrantedAuthority> authorities = new ArrayList<>();
    authorities.add(new SimpleGrantedAuthority(authority.toString()));
    return authorities;
  }

  @Override
  public boolean isAccountNonExpired() {
    return true;
  }

  @Override
  public boolean isAccountNonLocked() {
    return true;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return true;
  }

  @Override
  public boolean isEnabled() {
    return true;
  }
}

OverrideのisAccountNonExpired,isAccountNonLocked,isCredentialsNonExpired,isEnabledは、期限切れやLock状態、有効無効などのフラグ。

これらも認証に使いたい場合は、ここらへんもテーブルのカラムに追加すればOK。

今回は使わないので、常にtrueを返してます。

ユーザテーブルのRepositoryを作る

UserInfoEntityのRepositoryを用意

認証にusernameでのselectが必要なので、それだけ追加。クエリはJPAにおまかせ

public interface UserInfoRepository extends JpaRepository<UserInfoEntity, Long> {
    public UserInfoEntity findByUsername(String username);
}

認証用のServiceをつくる

認証用のサービスとして扱うために、UserDetailsServiceの実装が必要

中身は、必須チェックと存在チェックして、Entityの返却している感じ

@Service
public class UserInfoService implements UserDetailsService {
  @Autowired
  private UserInfoRepository userInfoRepo;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    if (username == null || "".equals(username)) {
      throw new UsernameNotFoundException("Username is empty");
    }

    UserInfoEntity userInfo = userInfoRepo.findByUsername(username);
    if (userInfo == null) {
      throw new UsernameNotFoundException("User not found for name: " + username);
    }

    return userInfo;
  }
}

(おまけ) 認証されたユーザをコントローラで使う

認証されたユーザは、Principal principalから取得できる

こんな感じ

@Controller
public class HogeController {
  @RequestMapping(value = "/hoge", method = RequestMethod.GET)
  public String index(Principal principal, Model model) {
    Authentication authentication = (Authentication) principal;
    UserInfoEntity user = (UserInfoEntity) authentication.getPrincipal();

    model.addAttribute("user", user);
    return "hoge/index";
  }
}

以上!!

参考になる書籍

Spring Boot 2 プログラミング入門

Spring Boot 2 プログラミング入門

参考にしたサイト様