開発環境
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>
