開発環境
OS:Win10
IDE:Eclipse
Java8
Spring Boot 2.2.1
依存:spring security,thymeleaf,spring web,devtools,lombok,mysql connector
DB:MySQL
参考URL:spring security reference
Springスターター・プロジェクト依存関係
プロジェクトを作成する際に、Spring Securityを選択してください。
Entity Classの作成
作成物の想定としては、簡易的なCRM(顧客管理システム)です。
CRMにログインするために、USER権限とADMIN権限の2種類を作成しました。
DBには「アカウントユーザーが有効か無効か」と「Admin権限が付与されているかいないか」を判断するカラムをBoolean型で持たせています。
※DB側では TINYNT型 で設定。
デフォルトコンストラクタ以外に、アカウント新規登録用のコンストラクタも作成しております。
package com.example.image.entity; import java.util.List;... @Entity @Table(name = "accounts") @Setter @Getter public class EmployeeEntity { @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; // login accountID @NotBlank(message = "必須項目です") @Column(name = "employee_number", nullable = false, unique = true) private String employeeNumber; @NotBlank(message = "必須項目です") @Column(name = "employee_name", nullable = false) private String employeeName; // ユーザーのアイコン @Column(name = "employee_photo") private String employeePhoto; @NotBlank(message = "必須項目です") @Column(name = "password", nullable = false) private String password; // Userが有効かのチェック @Column(name = "enabled", nullable = false) private boolean enabled; // admin権限保有者かのチェック @Column(name = "admin", nullable = false) private boolean admin; // デフォルトコンストラクタ protected EmployeeEntity() { } // UserDetails用コンストラクタ public EmployeeEntity(String employeeNumber, String employeeName, String password, boolean isAdmin) { setEmployeeNumber(employeeNumber); setEmployeeName(employeeName); setPassword(password); setAdmin(isAdmin); setEnabled(true); setEmployeePhoto("noimage.jpg"); } }
Repositoryの作成
Repositoryは、JpaRepositoryかCrudRepositoryのいずれかをextendsする。JPAの方が使いなれているので、今回はJpaRepositoryを継承しました。
継承したら、社員番号からユーザー情報を探すメソッドを定義する。
package com.example.image.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.stereotype.Repository; import com.example.image.entity.EmployeeEntity; @Repository public interface EmployeeRepository extends JpaRepository<EmployeeEntity,Integer>,JpaSpecificationExecutor<EmployeeEntity> { public EmployeeEntity findByEmployeeNumber(String employeeNumber); }
UserDetailsを継承するClassの作成
UserDetailsは認証情報を保持する場所で、すでに用意されているUserDetailsInterfaceをオーバーライドしていく。
ログインIDがgetUsername()にデフォルト設定されているが、今回は、社員番号をログインIDとして使用したいため、戻り値をEntity classのアクセッサーで指定します。
public class LoginAccount implements UserDetails { private static final long serialVersionUID = 1L; private EmployeeEntity employeeEntity; private Collection<GrantedAuthority> authorities; // デフォルトコンストラクタ protected LoginAccount() {} // アカウントと認証情報を処理するコンストラクタ public LoginAccount(EmployeeEntity employeeEntity,Collection<GrantedAuthority> authorities) { this.employeeEntity = employeeEntity; this.authorities = authorities; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } @Override public String getPassword() { return employeeEntity.getPassword(); } // 社員番号をログインIDにする @Override public String getUsername() { return employeeEntity.getEmployeeNumber(); } // 使用しないのでtrue @Override public boolean isAccountNonExpired() { return true; } // 使用しないのでtrue @Override public boolean isAccountNonLocked() { return true; } // 使用しないのでtrue @Override public boolean isCredentialsNonExpired() { return true; } // ユーザーが有効かどうか @Override public boolean isEnabled() { return employeeEntity.isEnabled(); } }
UserDetailsServiceを実装する
service classでは、ログイン許可を判断し、ROLE_権限を付与してUserDetailsに受け渡す処理を記載していきます。
権限はROLE_で始まるという暗黙条件がありますので、ルールに従いましょう。
@Service public class EmployeeService implements UserDetailsService { @Autowired private EmployeeRepository repository; @Override public UserDetails loadUserByUsername(String employeeNumber) throws UsernameNotFoundException { // ユーザー入力がnullの場合 if (employeeNumber == null || "".equals(employeeNumber)) { System.out.println("Username is empty"); throw new UsernameNotFoundException("Username is empty"); } // Entityに格納されている(EmployeeRepository)ユーザー情報と突合 EmployeeEntity employeeEntity = repository.findByEmployeeNumber(employeeNumber); if (employeeEntity == null) { System.out.println("Username not found:" + employeeNumber); throw new UsernameNotFoundException("Username not found:" + employeeNumber); } // ユーザーが有効でないとき if (!employeeEntity.isEnabled()) { System.out.println("Username not anabled:" + employeeNumber); throw new UsernameNotFoundException("Username not anabled:" + employeeNumber); } LoginAccount account = new LoginAccount(employeeEntity, getAuthrities(employeeEntity)); return account; } // 権限の付与 private Collection<GrantedAuthority> getAuthrities(EmployeeEntity employeeEntity) { if (employeeEntity.isAdmin()) { // admin権限がtrueのとき return AuthorityUtils.createAuthorityList("ROLE_ADMIN", "ROLE_USER"); } else { // admin権限がfalseの時、USER権限を付与する return AuthorityUtils.createAuthorityList("ROLE_USER"); } } }
spring securityを有効にする
securityを発動させるには、configクラスを作成して認証に関わる各種パラメータの設定を行う必要があります。
パスワードのハッシュ化にはBCryptを使用。
Configクラスの設定はこちらのサイトを参考にしました。
@Configuration @EnableWebSecurity public class WebSecurityConfigurerAdapter extends org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; // ハッシュ化をBeanに登録 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // 認証を除外するもの @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/favicon.ico", "/css/**", "/js/**", "/images/**", "/fonts/**","/upload/**"); } // 認証設定 @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/login").permitAll(). antMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated(); // 認証にかかわるパラメータを指定 // .loginPageには、htmlのform object名を入力 http.formLogin().loginProcessingUrl("/login").loginPage("/login") .failureUrl("/login?error").defaultSuccessUrl("/top",true).usernameParameter("employeeNumber").passwordParameter("password"); // logoutしたらcookieを削除し、セッションを無効にする。 http.logout().logoutUrl("/logout").logoutSuccessUrl("/login") .deleteCookies("JESSIONID").invalidateHttpSession(true); } // 独自認証の設定 @Autowired protected void congigureAuthenticationManager(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } }
Controller Classの作成
保有している権限をViewで表示したい時は、@AuthenticationPrincipalを使用する。
@RequestMapping("/top") public String top(@AuthenticationPrincipal LoginAccount account, Model model) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth.getPrincipal() instanceof LoginAccount) { account = LoginAccount.class.cast(auth.getPrincipal()); model.addAttribute("authorities", account.getAuthorities().toString()); } else { model.addAttribute("authorities", ""); } // employeNumberを利用してemployee nameを検索。そしてviewへ受け渡す EmployeeEntity user = repository.findByEmployeeNumber(account.getUsername()); model.addAttribute("userName", user.getEmployeeName()); return "top"; }
HTMLでのコンテンツ切り替え
thymeleafにはspring securityを使用できるがうまくいかなかったのでif文でコンテンツの表示・非表示を実装しました。
UserとAdminで見ることのできるnavメニューを制限しました。
User権限>アカウント編集とアカウント追加は見れない
Admin権限>すべてのメニューが選択可能
※参考
thymeleafのsecを使用する場合は、htmlタグ内に、
xmlns:sec=”http://www.thymeleaf.org/thymeleaf-extras-springsecurity5″
を追加します。
<nav> <ul class="menu"> <li><a href="clientlist">顧客検索・一覧</a></li> <li><a href="registerclient">新規顧客追加</a></li> <li th:if="${authorities} == '[ROLE_ADMIN, ROLE_USER]'"><a href="admin">CRMアカウント編集</a></li> <li th:if="${authorities} == '[ROLE_ADMIN, ROLE_USER]'"><a href="/admin/register">CRMアカウント追加</a></li> </ul> </nav>