使用SpringBoot集成Shiro的簡易教程

1. Shiro

Shiro官網

1.1 Shiro簡介

以下爲Shiro的簡單介紹:

  1. Shiro 是一個強大、簡單易用的 Java 安全權限框架
  2. Shiro 可以非常容易的開發出足夠好的應用,其不僅可以用在JavaSE 環境,也可以用在 JavaEE 環境
  3. Shiro 可以完成:認證、授權、加密、會話管理、與Web 集成、緩存等。

具體的信息可進一步查看Shiro官網

1.2 Shiro三大核心組件

Shiro 有三大核心組件,即 Subject、SecurityManager 和 Realm。它們的關係如下圖:
在這裏插入圖片描述

1.2.1 Subject

Subject 爲認證主體,它包含 Principals 和 Credentials 兩個信息。

  • Principals:代表身份。可以是用戶名、郵件、手機號碼等等,用來標識一個登錄主體的身份
  • Credentials:代表憑證。常見的有密碼,數字證書等等。

簡單來說:這兩者代表了需要認證的內容,最常見的便是用戶名、密碼了

1.2.2 SecurityManager

SecurityManager 爲安全管理員,它是Shiro 架構的核心。所有具體的交互都通過SecurityManager進行控制;負責所有Subject、且負責進行認證和授權、及會話、緩存的管理

1.2.3 Realm

Realm 是一個域,它是連接 Shiro 和具體應用的橋樑。當需要與安全數據交互時,比如用戶賬戶、訪問控制等,Shiro 將會在一個或多個 Realm 中查找。

1.3 Shiro的認證和授權流程

官方的入門實例:http://shiro.apache.org/tutorial.html
大致代碼如下:
shiro.ini

[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz

[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5

ini文件大致意思是:

  1. root=secret,admin:用戶名root,密碼secret,角色是admin
  2. schwartz=lightsaber:* :角色schwartz擁有權限 lightsaber:*

Java代碼

public static void main(String[] args) {
	Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
	SecurityManager securityManager = factory.getInstance();
	SecurityUtils.setSecurityManager(securityManager);
	
	Subject currentUser = SecurityUtils.getSubject();
	
	if ( !currentUser.isAuthenticated() ) {
		UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
	    token.setRememberMe(true);
	    currentUser.login(token);
	}
	
	if ( currentUser.hasRole( "schwartz" ) ) {
	    log.info("May the Schwartz be with you!" );
	} else {
	    log.info( "Hello, mere mortal." );
	}
	
	if ( currentUser.isPermitted( "lightsaber:wield" ) ) {
	    log.info("You may use a lightsaber ring.  Use it wisely.");
	} else {
	    log.info("Sorry, lightsaber rings are for schwartz masters only.");
	}
	...
}

上面代碼主要有兩個步驟:
認證:

UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);
currentUser.login(token);

判斷權限:

currentUser.hasRole( "schwartz" )

1.3.1 認證流程

在這裏插入圖片描述
shiro的認證流程如下:

  1. Subject進行login操作,參數是封裝了用戶信息的token
  2. Security Manager進行登錄操作
  3. Security Manager委託給Authenticator進行認證邏輯處理
  4. 調用AuthenticationStrategy進行多Realm身份驗證
  5. 調用對應Realm進行登錄校驗,認證成功則返回用戶屬性,失敗則拋出對應異常

1.3.2 授權流程

在這裏插入圖片描述
授權流程如下:

  1. 調用Subject.isPermitted()/hasRole()方法
  2. 委託給SecurityManager
  3. 而SecurityManager接着會委託給Authorizer
  4. Authorizer會判斷Realm的角色/權限是否和傳入的匹配
  5. 匹配如isPermitted/hasRole會返回true,否則返回false表示授權失敗

2. SpringBoot集成Shiro並完成登錄操作

2.1 項目信息

2.1.1 開發環境

IDEA:2018.2(lombok插件)
SpringBoot:2.3.1.RELEASE
Shiro:1.3.2
freemarker(前端頁面)

2.1.2 項目結構圖

在這裏插入圖片描述

2.1.3 pom依賴

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.3.2</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-freemarker</artifactId>
    </dependency>
    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.8</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

2.2 集成Shiro

根據官方文檔 可以直接使用starter依賴包,但按照網上的搭建過程基本上是有坑的。所以,這裏就使用了shiro-spring的依賴包咯(可查看這篇博客)。

步驟:
1)、導入依賴包
上述有。

2)、修改配置文件application.yml

spring:
  freemarker:
    suffix: .ftl
    template-loader-path: classpath:/templates/
    settings:
      #解決前臺使用${}賦值值爲空的情況
      classic_compatible: true

這裏就是對前端頁面freemarker的配置。至於對shiro的配置,就是通過SpringBoot中的配置類進行的。

3)、配置Shiro
配置shiro的securityManager和自定義realm。因爲realm負責我們的認證與授權,所以是必須的,自定義的realm必須要交給securityManager管理,所以這兩個類需要重寫。然後還有一些資源的權限說明,所以一般需要定義ShiroFilterFactoryBean,所以有3個類我們常寫的:

  • AuthorizingRealm:自定義realm
  • DefaultWebSecurityManager:shiro的核心管理器
  • ShiroFilterFactoryBean:過濾器鏈配置

4)、動手編碼
ShiroConfig:

@Slf4j
@Configuration
public class ShiroConfig {
    @Bean
    public UserRealm userRealm() {
        return new UserRealm();
    }
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 設置realm
        securityManager.setRealm(userRealm());
        return securityManager;
    }
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必須設置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 設置登錄頁面的url;如果不設置值,默認會自動尋找Web工程根目錄下的"/login.jsp"頁面 或 "/login" 映射
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 設置授權失敗時應該跳轉的頁面(即:訪問了權限不足的頁面時,需要跳轉到/role對應的url)
        shiroFilterFactoryBean.setUnauthorizedUrl("/role");
        shiroFilterFactoryBean.setSuccessUrl("/success");

        // 設置攔截器
        Map<String, String> filterMap = new LinkedHashMap<>();
        // anon:表示可以匿名訪問(不需要認證)
        filterMap.put("/guest/**", "anon");
        // 用戶,需要角色權限 user
        filterMap.put("/user/**", "roles[user]");
        filterMap.put("/admin/**", "roles[admin]");
        // 開放登陸接口
        filterMap.put("/doLogin", "anon");
        filterMap.put("/login", "anon");
        // 其餘接口一律攔截
        // 主要這行代碼必須放在所有權限設置的最後,不然會導致所有 url 都被攔截
        filterMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        log.info("Shiro攔截器工廠類注入成功");

        return shiroFilterFactoryBean;
    }
}

【注意】:SecurityManager 類導入的應該是 import org.apache.shiro.mgt.SecurityManager;

shirFilter()方法中主要設置了一些跳轉url(如:沒有登錄時,會跳轉到 /login 對應的頁面)以及各類url的權限攔截(如:訪問"/user"開頭的url,需要"user"的角色;訪問"/guest"開頭的url,可以匿名訪問)。

權限攔截 Filter
這裏對Shiro中的Filter稍加說明一下:當運行一個Web應用程序時,Shiro將會創建一些有用的默認 Filter 實例,並自動地將它們置爲可用,而這些默認的 Filter 實例是被 DefaultFilter 枚舉類定義的。
在這裏插入圖片描述
對其中一些過濾器稍加解釋一下:

Filter 解釋
anon 無參,開放權限,可以理解爲匿名用戶或遊客
authc 無參,需要認證
logout 無參,註銷,執行後會直接跳轉到shiroFilterFactoryBean.setLoginUrl(); 設置的 url
user 無參,表示必須存在用戶,當登入操作時不做檢查
perms[user] 參數可寫多個,表示需要某個或某些權限才能通過,多個參數時寫 perms[“user, admin”],當有多個參數時必須每個參數都通過纔算通過
roles[admin] 參數可寫多個,表示是某個或某些角色才能通過,多個參數時寫 roles[“admin,user”],當有多個參數時必須每個參數都通過纔算通過

【注意】:anon, authc, authcBasic, user 是第一組認證過濾器;perms, port, rest, roles, ssl 是第二組授權過濾器,要通過授權過濾器,就先要完成登陸認證操作。即:先要完成認證才能前去尋找授權,才能走第二組授權器。如:訪問需要 roles 權限的 url,如果還沒有登陸的話,會直接跳轉到 shiroFilterFactoryBean.setLoginUrl(); 設置的 url

UserRealm:
要繼承 AuthorizingRealm 類來自定義我們自己的 realm 以進行我們自定義的認證和授權操作

public class UserRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    // 授權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 獲取授權用戶信息(principalCollection存放了認證成功的用戶信息)
        User user = (User) principalCollection.iterator().next();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // TODO 改爲從數據庫中獲取該用戶的角色
        authorizationInfo.addRole(user.getRole());
        return authorizationInfo;
    }
    // 認證
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
        String username = token.getUsername();
        String password = String.valueOf(token.getPassword());
        // TODO 改爲從數據庫獲取對應用戶名密碼的用戶
        User user = userService.getUserByName(username);
        if (null == user) {
            throw new UnknownAccountException("用戶名不存在");
        } else if (!password.equals(user.getPassword())) {
            throw new IncorrectCredentialsException("密碼不正確");
        }
        SecurityUtils.getSubject().getSession().setAttribute("user", user);
        return new SimpleAuthenticationInfo(user, password, getName());
    }
}

重寫的兩個方法分別是實現身份認證以及授權認證。

  1. doGetAuthenticationInfo():需要進行認證時,纔會進入此方法。如調用:Subject.login(token)
  2. doGetAuthorizationInfo():需要授權認證時,纔會進入此方法。如:配置類中配置了 ==filterMap.put("/admin/**", “roles[admin]”);==管理員的角色,這時,如果訪問了以"/admin"開頭的url,則會進入此方法。

UserServiceImpl:
UserServiceImpl是UserService接口的實現類,這裏就貼出它的實現類的代碼了。

@Service
public class UserServiceImpl implements UserService {
    private static Map<String, User> userMap = new HashMap<>();
    static {
        userMap.put("zzc", new User("1", "zzc", "666", "user"));
        userMap.put("wzc", new User("2", "wzc", "888", "user"));
        userMap.put("yht", new User("3", "yht", "999", "admin!"));
    }

    @Override
    public User getUserByName(String username) {
        // Map通過key進行得到value
        return userMap.get(username);
    }
}

這裏沒使用數據庫了,就直接使用了假數據,並存儲在了Map中,通過靜態代碼塊添加了部分數據。Map的key是id(這裏假設id是與username 一樣,方便調用getUserByName()),value是User。

User:

@Data
public class User {
    private String id;
    private String username;
    private String password;
    // 角色
    private String role;

    public User(String id, String username, String password, String role) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.role = role;
    }
}

好了,接下來就是controller層了。

GuestController:

@RestController
@RequestMapping("/guest")
public class GuestController {

    @GetMapping("/enter")
    public String enter() {
        return "歡迎進入,您的身份是遊客";
    }
}

UserController:

@RestController
@RequestMapping("/user")
public class UserController {
    @GetMapping("/enter")
    public String enter() {
        return "歡迎進入,您的身份是用戶";
    }
}

AdminController:

@RestController
@RequestMapping("/admin")
public class AdminController {
    @GetMapping("/enter")
    public String enter() {
        return "Admin enter!!";
    }
}

這上面Controller裏面的內容基本一致,就是爲了測試不同的url是由不同的角色才能訪問的(接下去繼續看)。

LoginController:

@Slf4j
@Controller
public class LoginController {
    @Autowired
    private HttpServletRequest request;
    
    // 進入登錄頁面
    @GetMapping("/login")
    public String login() {
        log.info("進入login()方法");
        return "login";
    }

    // 執行登錄操作
    @PostMapping("/doLogin")
    public String doLogin(String username, String password) {
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        String errorMsg = null;
        try {
            SecurityUtils.getSubject().login(token);
        } catch (AuthenticationException e) {
            if (e instanceof UnknownAccountException) {
                errorMsg = "用戶不存在";
            } else if (e instanceof LockedAccountException) {
                errorMsg = "用戶被禁用";
            } else if (e instanceof IncorrectCredentialsException) {
                errorMsg = "密碼錯誤";
            } else {
                errorMsg = "認證失敗";
            }
            request.setAttribute("errorMsg", errorMsg);
            return "/login";
        }
        return "redirect:/success";
    }

    // 登錄成功頁面
    @GetMapping("/success")
    public String success() {
        return "success";
    }

    // 退出
    @GetMapping("/logout")
    public String logout() {
        log.info("進入logout()方法");
        SecurityUtils.getSubject().logout();
        return "redirect:/login";
    }
    
    // 授權失敗時,跳轉到role頁面
    @GetMapping("/role")
    public String role() {
        return "role";
    }
}

login.ftl:
登錄頁面

<body>
<h1>用戶登錄</h1>
<form action="/doLogin" method="post">
    username: <input type="text" name="username" /><br>
    password: <input type="password" name="password" /><br>
    <input type="submit" value="登錄" /><br>
</form>
<div style="color: red">${errorMsg}</div>
</body>

success.ftl:

<body>
<h1>登錄成功:${user.username}</h1>
<div>
    <a href="/logout">退出</a><br>
    <a href="/admin/enter">進入管理員頁面</a>
</div>
</body>

role.ftl:

<body>
<h1>權限不足</h1>
</body>

5)、測試
針對之前配置類的攔截器中的配置進行測試:

  1. 以"/guest"開頭的url,匿名訪問(不需要認證/登錄)
filterMap.put("/guest/**", "anon");

在這裏插入圖片描述
訪問:http://localhost:8080/guest/enter 的url,由於不需要認證,所以可以直接訪問。

  1. 以"/user"開頭的url,需要"user"的角色
filterMap.put("/user/**", "roles[user]");

在這裏插入圖片描述
上述的url還沒有按回車鍵,按後:
在這裏插入圖片描述
跳轉到了這個登錄界面(按回車鍵,跳轉到登錄界面,這個過程,我無法證明,但讀者可以去試試。)。話說回來,爲什麼會跳轉到登錄界面?因爲訪問以"/user"開頭的url是需要"user"的角色的,都還沒有登錄,怎麼可能會讓你訪問呢?所以,需要你去登錄。

執行登錄邏輯:
進行登錄操作時,會調用LoginController.doLogin()方法,會執行

SecurityUtils.getSubject().login(token);

上面那條語句,所以,會調用UserRealm.doGetAuthenticationInfo()方法進行認證(如果忘記了,請往上看UserRealm類那塊)。

我們以“zzc-666”的用戶進行登錄,它的角色是"user"。
先使用錯的賬號進行登錄:
在這裏插入圖片描述
再來使用一個錯的賬號:
在這裏插入圖片描述
使用對的賬號和密碼:
在這裏插入圖片描述
在登錄成功界面,我們來訪問“進入管理員頁面的”鏈接,它的html代碼爲:

<a href="/admin/enter">進入管理員頁面</a>

根據配置類中的配置:

filterMap.put("/admin/**", "roles[admin]");

可知,訪問此條鏈接是需要管理員角色的,那麼我這個"user"角色能否訪問?
在這裏插入圖片描述
由此可知,權限不足,無法訪問,那麼爲何會跳轉到這個頁面呢?
根據此配置:

shiroFilterFactoryBean.setUnauthorizedUrl("/role");

好了,SpringBoot集成Shiro的簡單介紹就到這兒了。

【參考資料】
教你 Shiro 整合 SpringBoot,避開各種坑

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章