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型 で設定。
デフォルトコンストラクタ以外に、アカウント新規登録用のコンストラクタも作成しております。

EmployeeEntity.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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を継承しました。
継承したら、社員番号からユーザー情報を探すメソッドを定義する。

EmployeeRepository.java
01
02
03
04
05
06
07
08
09
10
11
12
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のアクセッサーで指定します。

LoginAccount.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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_で始まるという暗黙条件がありますので、ルールに従いましょう。

EmployeeService.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@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クラスの設定はこちらのサイトを参考にしました。

WebSecurityConfigurerAdapter.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@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を使用する。

LoginControllerのアカウント情報受け渡し
01
02
03
04
05
06
07
08
09
10
11
12
13
14
@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″
を追加します。

top.html
1
2
3
4
5
6
7
8
9
<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をコピーしました