首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

Spring MVC 快速整合 Apache Shiro 實戰指南

     編輯:Java 綜合教學

Spring MVC 快速整合 Apache Shiro 實戰指南

Apache Shiro 是輕量級安全框架,提供認證、授權、加密、會話管理等功能。本文記錄 Spring MVC 整合 Shiro 的最小化配置步驟,涵蓋 Realm 自定義、權限註解、登錄流程、攔截器鏈等核心環節。

一、依賴坐標

<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring</artifactId>
  <version>1.7.1</version> <!-- 當前穩定版 -->
</dependency>
<!-- 需額外引入 shiro-ehcache 若用 EhCache -->

二、web.xml 配置 ShiroFilter

<filter>
  <filter-name>shiroFilter</filter-name>
  <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  <init-param>
    <param-name>targetFilterLifecycle</param-name>
    <param-value>true</param-value> <!-- 委托 Spring 管理生命週期 -->
  </init-param>
</filter>
<filter-mapping>
  <filter-name>shiroFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

關鍵:DelegatingFilterProxy 從 Spring 容器查找名為 shiroFilter 的 Bean。

三、Shiro Spring 配置 (shiro-spring.xml)

<beans xmlns="https://www.springframework.org/schema/beans"
       xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="https://www.springframework.org/schema/beans
       https://www.springframework.org/schema/beans/spring-beans.xsd">

  <!-- 1. 密碼匹配器 (MD5) -->
  <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.Md5CredentialsMatcher"/>

  <!-- 2. 自定義 Realm -->
  <bean id="myRealm" class="com.example.shiro.MyRealm"/>

  <!-- 3. JDBC Realm (可選,直接查表) -->
  <bean id="jdbcRealm" class="org.apache.shiro.realm.jdbc.JdbcRealm">
    <property name="credentialsMatcher" ref="credentialsMatcher"/>
    <property name="dataSource" ref="dataSource"/>
    <property name="authenticationQuery" value="SELECT password FROM users WHERE username = ?"/>
    <property name="userRolesQuery" value="SELECT role_name FROM user_roles WHERE username = ?"/>
    <property name="permissionsQuery" value="SELECT perm FROM roles_perms WHERE role_name = ?"/>
    <property name="permissionsLookupEnabled" value="true"/>
  </bean>

  <!-- 4. 安全管理器 -->
  <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realms">
      <list>
        <ref bean="myRealm"/> <!-- 多 Realm 按順序嘗試 -->
        <ref bean="jdbcRealm"/>
      </list>
    </property>
  </bean>

  <!-- 5. ShiroFilterFactoryBean -->
  <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login.jsp"/>
    <property name="unauthorizedUrl" value="/403.jsp"/>
    <property name="filterChainDefinitions">
      <value>
        /static/** = anon
        /login.jsp = anon
        /dologin = anon
        /logout = logout
        /admin/** = roles[admin]
        /user/** = authc
        /** = authc
      </value>
    </property>
  </bean>

  <!-- 6. 註解支持 (AOP) -->
  <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
  <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
    <property name="securityManager" ref="securityManager"/>
  </bean>

</beans>

四、自定義 Realm 實現

public class MyRealm extends AuthorizingRealm {
    private static final Logger log = LoggerFactory.getLogger(MyRealm.class);
    
    @Autowired private UserService userService; // 注入業務服務
    
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = (String) getAvailablePrincipal(principals);
        log.info("授權查詢: {}", username);
        
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        User user = userService.findByUsername(username);
        if (user != null) {
            info.addRoles(user.getRoleNames());       // 角色集合
            info.addStringPermissions(user.getPerms()); // 權限字符串集合
        }
        return info;
    }
    
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) 
            throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        log.info("認證用戶: {}", username);
        
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new UnknownAccountException("用戶不存在");
        }
        // 返回 Principal + Credentials (加密後密碼) + RealmName
        return new SimpleAuthenticationInfo(username, user.getPassword(), getName());
    }
}

五、登錄 Controller

@Controller
@RequestMapping("/")
public class LoginController {
    @PostMapping("/dologin")
    @ResponseBody
    public Map login(HttpServletRequest request, 
                                     @RequestParam String username,
                                     @RequestParam String password,
                                     @RequestParam(required = false) Boolean rememberMe) {
        Map result = new HashMap<>();
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        token.setRememberMe(rememberMe != null && rememberMe);
        
        try {
            subject.login(token); // 觸發 Realm.doGetAuthenticationInfo
            result.put("success", true);
            result.put("msg", "登錄成功");
        } catch (UnknownAccountException e) {
            result.put("msg", "用戶不存在");
        } catch (IncorrectCredentialsException e) {
            result.put("msg", "密碼錯誤");
        } catch (LockedAccountException e) {
            result.put("msg", "賬號已鎖定");
        } catch (DisabledAccountException e) {
            result.put("msg", "賬號已禁用");
        } catch (ExcessiveAttemptsException e) {
            result.put("msg", "嘗試次數過多,請稍後再試");
        } catch (AuthenticationException e) {
            result.put("msg", "登錄失敗: " + e.getMessage());
        }
        return result;
    }
    
    @GetMapping("/logout")
    public String logout() {
        SecurityUtils.getSubject().logout();
        return "redirect:/login.jsp";
    }
}

六、註解式授權使用

@Service
public class UserServiceImpl implements UserService {
    @RequiresPermissions("user:create")
    public void createUser(User user) { ... }
    
    @RequiresRoles("admin")
    public void deleteUser(Long id) { ... }
    
    @RequiresPermissions(value = {"user:view", "user:export"}, logical = Logical.OR)
    public List listUsers() { ... }
}

七、JSP 頁面整合標籤庫

<%@ taglib prefix="shiro" uri="https://shiro.apache.org/tags" %>

<shiro:guest>
  <a href="/login.jsp">登錄</a>
</shiro:guest>

<shiro:user>
  歡迎, <shiro:principal/> | <a href="/logout">退出</a>
</shiro:user>

<shiro:hasRole name="admin">
  <a href="/admin/dashboard">管理後台</a>
</shiro:hasRole>

<shiro:hasPermission name="user:export">
  <button>導出用戶</button>
</shiro:hasPermission>

八、常見坑點避雷

  1. Realm 注入 Service 循環依賴:用 @Lazy 或在 doGetAuthenticationInfo 內手動 getBean
  2. 靜態資源被攔截:filterChainDefinitions 首行配置 /static/** = anon
  3. 註解不生效:確保 DefaultAdvisorAutoProxyCreator 和 AuthorizationAttributeSourceAdvisor 都已註冊
  4. Session 共享:集群環境需配置 shiro-redis 或 shiro-spring-session
  5. 密碼加密:註冊/修改密碼時用 new Md5Hash(password, salt, 1024).toHex(),Realm 配置 Md5CredentialsMatcher 並 setStoredCredentialsHexEncoded(true)
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved