1. 程式人生 > 其它 >springboot整合shiro全流程

springboot整合shiro全流程

轉載自
王詩林-springboot整合shiro(完整版)
南風-超詳細 Spring Boot 整合 Shiro 教程!

1. Shiro是什麼?

Shiro是Apache下的一個開源專案。shiro屬於輕量級框架,相對於SpringSecurity簡單的多,也沒有SpringSecurity那麼複雜。

它的主要作用是用來做身份認證、授權、會話管理和加密等操作。

什麼意思?大白話就是判斷使用者是否登入、是否擁有某些操作的許可權等。

其實不用 Shiro,我們使用原生 Java API 就可以完成安全管理,很簡單,使用過濾器去攔截使用者的各種請求,然後判斷是否登入、是否擁有某些許可權即可。

我們完全可以完成這些操作,但是對於一個大型的系統,分散去管理編寫這些過濾器的邏輯會比較麻煩,不成體系,所以需要使用結構化、工程化、系統化的解決方案。

任何一個業務邏輯,一旦上升到企業級的體量,就必須考慮使用系統化的解決方案,也就是框架,否則後期的開發成本是相當巨大的,Shiro 就是來解決安全管理的系統化框架。

2. Shiro 核心元件

1、UsernamePasswordToken,Shiro 用來封裝使用者登入資訊,使用使用者的登入資訊建立令牌 Token,登入的過程即 Shiro 驗證令牌是否具有合法身份以及相關許可權。

2、 SecurityManager,Shiro 的核心部分,負責安全認證與授權。

3、Subject,Shiro 的一個抽象概念,包含了使用者資訊。

4、Realm,開發者自定義的模組,根據專案的需求,驗證和授權的邏輯在 Realm 中實現。

5、AuthenticationInfo,使用者的角色資訊集合,認證時使用。

6、AuthorizationInfo,角色的許可權資訊集合,授權時使用。

7、DefaultWebSecurityManager,安全管理器,開發者自定義的 Realm 需要注入到 DefaultWebSecurityManager 進行管理才能生效。

8、ShiroFilterFactoryBean,過濾器工廠,Shiro 的基本執行機制是開發者定製規則,Shiro 去執行,具體的執行操作就是由 ShiroFilterFactoryBean 建立一個個 Filter 物件來完成。

Shiro 的執行機制如下圖所示:

3. 具體實現

3.1 pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.rogn</groupId>
    <artifactId>mybp</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>mybp</name>
    <description>mybp</description>
    <properties>
        <java.version>18</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
            <scope>provided</scope>
        </dependency>

        <!-- shiro -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.9.0</version>
        </dependency>

        <!--熱部署依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

3.2 user.java(使用者實體類):

package com.rogn.mybp.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Set;

@Data
@NoArgsConstructor   // 無參建構函式
@AllArgsConstructor  // 有參建構函式
public class User {
    private Integer id;

    private String username;
    private String password;

    /**
     * 使用者對應的角色集合
     */
    private Set<Role> roles;
}

3.3 Role.java(角色對應實體類):

package com.rogn.mybp.entity;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Set;

@Data
@AllArgsConstructor
public class Role {
    private Integer id;
    private String roleName;
    /**
     * 角色對應許可權集合
     */
    private Set<Permissions> permissions;
}

3.4 Permissions.java(許可權對應實體類):

package com.rogn.mybp.entity;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Permissions {
    private Integer id;
    private String permissionsName;
}

3.5 LoginService.java

package com.rogn.mybp.service;

import com.rogn.mybp.entity.User;
import org.springframework.stereotype.Service;


public interface LoginService {
    User getUserByName(String userName);
}

3.6 LoginServiceImpl.java

package com.rogn.mybp.service.impl;

import com.rogn.mybp.entity.Permissions;
import com.rogn.mybp.entity.Role;
import com.rogn.mybp.entity.User;
import com.rogn.mybp.service.LoginService;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

@Service
public class LoginServiceImpl implements LoginService {
    @Override
    public User getUserByName(String getMapByName) {
        return getMapByName(getMapByName);
    }

    /**
     * 模擬資料庫查詢
     *
     * @param userName 使用者名稱
     * @return User
     */
    public User getMapByName(String userName) {
        Permissions permissions1 = new Permissions(1, "query");
        Permissions permissions2 = new Permissions(2, "add");
        Set<Permissions> permissionsSet = new HashSet<>();
        permissionsSet.add(permissions1);
        permissionsSet.add(permissions2);

        Role role = new Role(1, "admin", permissionsSet);
        Set<Role> roleSet = new HashSet<>();
        roleSet.add(role);

        User user = new User(1, "wsl", "123456", roleSet);
        Map<String, User> map = new HashMap<>();
        map.put(user.getUsername(), user);

        Set<Permissions> permissionsSet1 = new HashSet<>();
        permissionsSet1.add(permissions1);
        Role role1 = new Role(2, "user", permissionsSet1);
        Set<Role> roleSet1 = new HashSet<>();
        roleSet1.add(role1);
        User user1 = new User(2, "zhangsan", "123456", roleSet1);
        map.put(user1.getUsername(), user1);
        return map.get(userName);
    }
}

3.7 CustomRealm.java:

自定義Realm用於查詢使用者的角色和許可權資訊並儲存到許可權管理器

對 URL 進行攔截,沒有認證的需要認證,認證成功的則可以根據需要判斷角色及許可權。
這個過濾器需要開發者自定義,然後去指定認證和授權的邏輯,繼承抽象類 AuthorizingRealm,實現兩個抽象方法分別完成授權和認證的邏輯。

package com.rogn.mybp.shiro;

import com.rogn.mybp.entity.Permissions;
import com.rogn.mybp.entity.Role;
import com.rogn.mybp.entity.User;
import com.rogn.mybp.service.LoginService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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.util.StringUtils;

public class CustomRealm extends AuthorizingRealm {

    @Autowired
    private LoginService loginService;

    // 授權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //獲取登入使用者名稱
        String name = (String) principalCollection.getPrimaryPrincipal();
        //查詢使用者名稱稱
        User user = loginService.getUserByName(name);
        //新增角色和許可權
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        for (Role role : user.getRoles()) {
            //新增角色
            simpleAuthorizationInfo.addRole(role.getRoleName());
            //新增許可權
            for (Permissions permissions : role.getPermissions()) {
                simpleAuthorizationInfo.addStringPermission(permissions.getPermissionsName());
            }
        }
        return simpleAuthorizationInfo;
    }

    // 認證
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        if (StringUtils.isEmpty(authenticationToken.getPrincipal())) {
            return null;
        }
        //獲取使用者資訊
        String name = authenticationToken.getPrincipal().toString();
        User user = loginService.getUserByName(name);
        if (user == null) {
            //這裡返回後會報出對應異常
            return null;
        } else {
            //這裡驗證authenticationToken和simpleAuthenticationInfo的資訊
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, user.getPassword().toString(), getName());
            return simpleAuthenticationInfo;
        }
    }
}

3.8 ShiroConfig.java:

把CustomRealm和SecurityManager等注入到spring容器中,這個配置類中一共自動裝配了 5 個 bean 例項

  • CustomRealm:自定義過濾器 MyRealm,我們的業務邏輯全部定義在這個 bean 中
  • DefaultWebSecurityManager:將 MyRealm 注入到 DefaultWebSecurityManager bean 中,完成註冊
  • ShiroFilterFactoryBean:這是 Shiro 自帶的一個 Filter 工廠例項,所有的認證和授權判斷都是由這個 bean 生成的 Filter 物件來完成的,這就是 Shiro 框架的執行機制,開發者只需要定義規則,進行配置,具體的執行者全部由 Shiro 自己建立的 Filter 來完成。

自定義過濾器建立完成之後,需要進行配置才能生效,在 Spring Boot 應用中,不需要任何的 XML 配置,直接通過配置類進行裝配,也就是@Configuration

所以我們需要給 ShiroFilterFactoryBean 例項注入認證及授權規則,如下所示。

認證過濾器:

  • anon:無需認證即可訪問,遊客身份。
  • authc:必須認證(登入)才能訪問。
  • authcBasic:需要通過 httpBasic 認證。
  • user:不一定已通過認證,只要是曾經被 Shiro 記住過登入狀態的使用者就可以正常發起請求,比如 rememberMe。

授權過濾器:

  • perms:必須擁有對某個資源的訪問許可權(授權)才能訪問。
  • role:必須擁有某個角色許可權才能訪問。
  • port:請求的埠必須為指定值才可以訪問。
  • rest:請求必須是 RESTful,method 為 post、get、delete、put。
  • ssl:必須是安全的 URL 請求,協議為 HTTPS。
package com.rogn.mybp.config;

import com.rogn.mybp.shiro.CustomRealm;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.apache.shiro.mgt.SecurityManager;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }

    //將自己的驗證方式加入容器
    @Bean
    public CustomRealm myShiroRealm() {
        CustomRealm customRealm = new CustomRealm();
        return customRealm;
    } //許可權管理,配置主要是Realm的管理認證

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }
    //Filter工廠,設定對應的過濾條件和跳轉條件
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> map = new HashMap<>();
        //登出
        map.put("/logout", "logout");
        //對所有使用者認證
        map.put("/**", "authc");
        //登入
        shiroFilterFactoryBean.setLoginUrl("/login");
        //首頁
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //錯誤頁面,認證不通過跳轉
        shiroFilterFactoryBean.setUnauthorizedUrl("/error");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

3.9 LoginController.java

我們編寫一個簡單的登入方法,對應不同的角色或許可權攔截,一個admin方法(需要admin role),一個add方法(需要add permision),一個query方法(需要query permission)

package com.rogn.mybp.controller;

import com.rogn.mybp.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class LoginController {
    @GetMapping("/login")
    public String login(User user) {
        if (StringUtils.isEmpty(user.getUsername()) || StringUtils.isEmpty(user.getPassword())) {
            return "請輸入使用者名稱和密碼!";
        }
        //使用者認證資訊
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
                user.getUsername(),
                user.getPassword()
        );
        try {
            //進行驗證,這裡可以捕獲異常,然後返回對應資訊
            subject.login(usernamePasswordToken);
//            subject.checkRole("admin");
//            subject.checkPermissions("query", "add");
        } catch (UnknownAccountException e) {
            log.error("使用者名稱不存在!", e);
            return "使用者名稱不存在!";
        } catch (AuthenticationException e) {
            log.error("賬號或密碼錯誤!", e);
            return "賬號或密碼錯誤!";
        } catch (AuthorizationException e) {
            log.error("沒有許可權!", e);
            return "沒有許可權";
        }
        return "login success";
    }

    @RequiresRoles("admin")
    @GetMapping("/admin")
    public String admin() {
        return "admin success!";
    }

    @RequiresPermissions("query")
    @GetMapping("/query")
    public String query() {
        return "query success!";
    }

    @RequiresPermissions("add")
    @GetMapping("/add")
    public String add() {
        return "add success!";
    }
}

註解驗證角色和許可權的話無法捕捉異常,從而無法正確的返回給前端錯誤資訊,所以我加了一個類用於攔截異常,具體程式碼如下

3.10 MyExceptionHandler.java

package com.rogn.mybp.common;

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
@Slf4j
public class MyExceptionHandler {
    @ExceptionHandler
    @ResponseBody
    public String ErrorHandler(AuthorizationException e) {
        log.error("沒有通過許可權驗證!", e);
        return "沒有通過許可權驗證!";
    }
}

完整的程式碼可見

4. 測試

共有兩個使用者:wsl和zhangsan,兩種使用者:admin和user,兩種許可權:query和add

            /  query
wsl--admin
            \  add
 
zhangsan--user--query

因此,對於wsl:
http://localhost:8080/login?username=wsl&password=123456 "login success"
http://localhost:8080/admin "admin success!"
http://localhost:8080/query "query success!"
http://localhost:8080/add "add success!"

對於zhangsan:
http://localhost:8080/login?username=zhangsan&password=123456 "login success"
http://localhost:8080/admin "沒有通過許可權驗證!"
http://localhost:8080/query "query success!"
http://localhost:8080/add "沒有通過許可權驗證!"