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 要素が条件付きでレンダリングされます。