Shiro を用いた Spring Boot アプリケーションでの認証・認可制御の実装

Web アプリケーションにおけるアクセス制御は、ユーザーのログイン状態やロール・権限に基づいて URL や UI 要素へのアクセスを制限する仕組みです。Apache Shiro は Java 向けの軽量かつ柔軟なセキュリティフレームワークであり、認証(Authentication)、認可(Authorization)、セッション管理などをシンプルに実現できます。本稿では、Spring Boot と Thymeleaf を使用したアプリケーションに Shiro を統合し、ログイン処理および URL・ページ内ボタンレベルでのアクセス制御を実装する方法を紹介します。

1. 必要な依存関係の追加

Maven プロジェクトでは、以下のように pom.xml に Shiro と Thymeleaf の連携モジュールを追加します。

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.4.0</version>
</dependency>
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

2. Shiro の設定クラス

Shiro の動作に必要なフィルターチェーン、セキュリティマネージャ、カスタム Realm、Thymeleaf 拡張を定義します。

package com.example.config;

import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;

import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroSecurityConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean factory = new ShiroFilterFactoryBean();
        factory.setSecurityManager(securityManager);

        Map<String, String> rules = new LinkedHashMap<>();
        rules.put("/static/**", "anon");
        rules.put("/login", "anon");
        rules.put("/logout", "logout");
        rules.put("/**", "authc");

        factory.setLoginUrl("/login");
        factory.setSuccessUrl("/index");
        factory.setUnauthorizedUrl("/403");
        factory.setFilterChainDefinitionMap(rules);
        return factory;
    }

    @Bean
    public DefaultWebSecurityManager securityManager(MyCustomRealm realm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(realm);
        return manager;
    }

    @Bean
    public MyCustomRealm myCustomRealm() {
        return new MyCustomRealm();
    }

    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }
}

3. カスタム Realm の実装

ユーザー認証と権限付与のロジックを実装するため、AuthorizingRealm を継承したクラスを作成します。

package com.example.config;

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.example.pojo.User;
import com.example.pojo.Role;
import com.example.pojo.Permission;
import com.example.service.UserService;
import com.example.service.RoleService;

@Component("authorizer")
public class MyCustomRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    @Autowired
    private RoleService roleService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        User user = (User) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        for (Role role : user.getRoles()) {
            info.addRole(role.getId());
            Role loadedRole = roleService.getRoleById(role.getId());
            for (Permission perm : loadedRole.getPermissions()) {
                info.addStringPermission(perm.getCode());
            }
        }
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        String username = (String) token.getPrincipal();
        User user = userService.getUserById(username);
        if (user == null) {
            return null;
        }
        return new SimpleAuthenticationInfo(user, "123456", getName());
    }
}

4. ログイン処理の実装

ログインフォームとコントローラーを実装します。

<form th:action="@{/login}" method="post">
    <label>ユーザーID:</label>
    <input type="text" name="id" /><br/>
    <label>パスワード:</label>
    <input type="password" name="pwd" /><br/>
    <button type="submit">ログイン</button>
</form>
package com.example.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

@Controller
public class AuthController {

    @GetMapping("/login")
    public String showLoginForm() {
        return "login";
    }

    @PostMapping("/login")
    public String handleLogin(@RequestParam String id, Model model) {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(id, "123456");

        try {
            subject.login(token);
            return "redirect:/index";
        } catch (UnknownAccountException e) {
            model.addAttribute("msg", "ユーザーが存在しません");
        } catch (IncorrectCredentialsException e) {
            model.addAttribute("msg", "パスワードが違います");
        }
        return "login";
    }

    @GetMapping("/index")
    public String index() {
        return "home";
    }
}

5. コントローラーレベルでのアクセス制御

Shiro のアノテーションを使って、特定のロールや権限を持つユーザーのみがアクセスできるようにします。

@GetMapping("/edit")
@RequiresRoles("002")
public String editUser(@RequestParam String id, Model model) {
    model.addAttribute("id", id);
    return "/user/edit";
}

@GetMapping("/selrole")
@RequiresPermissions("002")
public String selectRole(@RequestParam String id, @RequestParam Integer type, Model model) {
    model.addAttribute("id", id);
    model.addAttribute("type", type);
    return "/user/selrole";
}

6. フロントエンドでのボタン表示制御

Thymeleaf で Shiro の Dialect を利用し、ボタンの表示・非表示を動的に制御します。

<html xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<div class="layui-inline">
    <a shiro:hasAnyRoles="002,003" class="layui-btn" onclick="addUser('')">ユーザー追加</a>
</div>
<div class="layui-inline">
    <a shiro:hasAnyRoles="002,003" class="layui-btn" onclick="batchDelete()">一括削除</a>
</div>

これにより、ユーザーのロールに応じて UI 要素が条件付きでレンダリングされます。

タグ: Shiro Spring Boot Thymeleaf 認証 認可

7月31日 08:08 投稿