Spring Securityでログイン認証と権限によるコンテンツ表示・非表示を実装してみた

開発環境
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>
タイトルとURLをコピーしました