SpringBoot2.x集成Apache Shiro並完成簡單的Case開發
在上文 Apache Shiro權限框架理論介紹 中,我們介紹了Apache Shiro的基礎理論知識。本文我們將在 SpringBoot 中集成Apache Shiro,完成一些簡單的Case開發。
Apache Shiro和Spring Security不同,它沒有自帶的登錄頁面和基於內存的權限驗證。所以我們將使用jsp去編寫簡單的登錄頁面,使用Mybatis連接MySQL數據庫進行用戶及其權限和角色信息的存取。
首先在IDEA中,創建一個Spring Boot工程:
選擇需要的模塊:
項目創建完成後,補充相應的依賴,pom.xml文件中配置的完整依賴項如下:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- apache shiro 依賴 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.2.3</version> </dependency> <!-- alibaba的druid數據庫連接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.9</version> </dependency> <!-- apache 工具包 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</version> </dependency> <!-- spring 工具包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>5.0.7.RELEASE</version> </dependency> <!-- jsp 依賴 --> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency> </dependencies>
註:在本文中,不會贅述SpringBoot集成Mybatis的配置,若對此不熟悉的話,可以參考我另一篇文章:SpringBoot2.x整合MyBatis
以上也提到了我們需要在數據庫中進行用戶及其權限和角色信息的存取,並且我們將按照RBAC模型完成文中Case的開發,所以首先需要創建數據庫表格及向表格插入一些數據。具體的sql語句如下:
-- 權限表 -- create table permission ( pid int (11) not null auto_increment, name varchar (255) not null default ‘‘, url varchar (255) default ‘‘, primary key (pid) ) engine = InnoDB default charset = utf8; insert into permission values (‘1‘,‘add‘,‘‘); insert into permission values (‘2‘,‘delete‘,‘‘); insert into permission values (‘3‘,‘edit‘,‘‘); insert into permission values (‘4‘,‘query‘,‘‘); -- 用戶表 -- create table user ( uid int (11) not null auto_increment, username varchar (255) not null default ‘‘, password varchar (255) default ‘‘, primary key (uid) ) engine = InnoDB default charset = utf8; insert into user values (‘1‘,‘admin‘,‘123‘); insert into user values (‘2‘,‘user‘,‘123‘); -- 角色表 -- create table role ( rid int (11) not null auto_increment, rname varchar (255) not null default ‘‘, primary key (rid) ) engine = InnoDB default charset = utf8; insert into role values (‘1‘,‘admin‘); insert into role values (‘2‘,‘customer‘); -- 權限、角色關系表 -- create table permission_role ( rid int (11) not null, pid int (11) not null, key idx_rid(rid), key idx_pid(pid) ) engine = InnoDB default charset = utf8; insert into permission_role values (‘1‘,‘1‘); insert into permission_role values (‘1‘,‘2‘); insert into permission_role values (‘1‘,‘3‘); insert into permission_role values (‘1‘,‘4‘); insert into permission_role values (‘2‘,‘1‘); insert into permission_role values (‘2‘,‘4‘); -- 用戶、角色關系表 -- create table user_role ( uid int (11) not null, rid int (11) not null, key idx_uid(uid), key idx_rid(rid) ) engine = InnoDB default charset = utf8; insert into user_role values (1,1); insert into user_role values (2,2);
創建與表格所對應的pojo類。如下:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Permission {
private Integer pid;
private String name;
private String url;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
private Integer rid;
private String rname;
private Set<Permission> permissions = new HashSet<>();
private Set<User> users = new HashSet<>();
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer uid;
private String username;
private String password;
private Set<Role> roles = new HashSet<>();
}
然後創建dao層的mapper接口:
public interface UserMapper {
/**
* 根據用戶名查找用戶
*
* @param username 用戶名
* @return user
*/
User findByUserName(@Param("username") String username);
}
以及編寫與之對應的xml文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.zero.example.shiro.mapper.UserMapper">
<resultMap id="userMap" type="org.zero.example.shiro.model.User">
<id property="uid" column="uid"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<collection property="roles" ofType="org.zero.example.shiro.model.Role">
<id property="rid" column="rid"/>
<result property="rname" column="rname"/>
<collection property="permissions" ofType="org.zero.example.shiro.model.Permission">
<id property="pid" column="pid"/>
<result property="name" column="name"/>
<result property="url" column="url"/>
</collection>
</collection>
</resultMap>
<select id="findByUserName" parameterType="string" resultMap="userMap">
select u.*, r.*, p.*
from user u
inner join user_role ur on ur.uid = u.uid
inner join role r on r.rid = ur.rid
inner join permission_role pr on pr.rid = r.rid
inner join permission p on pr.pid = p.pid
where u.username = #{username}
</select>
</mapper>
接著是service層接口:
public interface UserService {
/**
* 根據用戶名查找用戶
*
* @param username 用戶名
* @return user
*/
User findByUserName(String username);
}
編寫實現類來實現UserService接口:
@Service("userService")
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
@Autowired
public UserServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public User findByUserName(String username) {
return userMapper.findByUserName(username);
}
}
到此為止,我們就完成了項目基本結構的搭建,接下來我們就可以開始Case的開發了。
自定義權限管理
我們來基於Apache Shiro實現一個自定義的認證、授權及密碼匹配規則。首先是創建我們自定義的Realm,在Realm實現授權及認證登錄,代碼如下:
package org.zero.example.shiro.realm;
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.util.CollectionUtils;
import org.zero.example.shiro.model.Permission;
import org.zero.example.shiro.model.Role;
import org.zero.example.shiro.model.User;
import org.zero.example.shiro.service.UserService;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* @program: shiro
* @description: 自定義Realm
* @author: 01
* @create: 2018-09-08 16:13
**/
public class AuthRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
// 授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 從session中拿出用戶對象
User user = (User) principalCollection.fromRealm(this.getClass().getName()).iterator().next();
List<String> permissionList = new ArrayList<>();
Set<String> roleNameSet = new HashSet<>();
// 獲取用戶的角色集
Set<Role> roleSet = user.getRoles();
if (!CollectionUtils.isEmpty(roleSet)) {
for (Role role : roleSet) {
// 添加角色名稱
roleNameSet.add(role.getRname());
// 獲取角色的權限集
Set<Permission> permissionSet = role.getPermissions();
if (!CollectionUtils.isEmpty(permissionSet)) {
for (Permission permission : permissionSet) {
// 添加權限名稱
permissionList.add(permission.getName());
}
}
}
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(permissionList);
info.setRoles(roleNameSet);
return info;
}
// 認證登錄
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
// 獲取登錄的用戶名
String userName = usernamePasswordToken.getUsername();
// 從數據庫中查詢用戶
User user = userService.findByUserName(userName);
return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName());
}
}
因為登錄時用戶輸入的密碼需要與數據庫裏的密碼進行對比,所以我們還可以自定義一個密碼校驗規則。代碼如下:
package org.zero.example.shiro.matcher;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
/**
* @program: shiro
* @description: 自定義密碼校驗規則
* @author: 01
* @create: 2018-09-08 16:30
**/
public class CredentialMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String password = new String(usernamePasswordToken.getPassword());
String dbPassword = (String) info.getCredentials();
return this.equals(password, dbPassword);
}
}
最後是新建一個配置類來註入shiro相關的配置,代碼如下:
package org.zero.example.shiro.config;
import org.apache.shiro.mgt.SecurityManager;
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.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.zero.example.shiro.matcher.CredentialMatcher;
import org.zero.example.shiro.realm.AuthRealm;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @program: shiro
* @description: shiro配置類
* @author: 01
* @create: 2018-09-08 16:34
**/
@Configuration
public class ShiroConfiguration {
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
// 登錄的url
bean.setLoginUrl("/login");
// 登錄成功後跳轉的url
bean.setSuccessUrl("/index");
// 權限拒絕時跳轉的url
bean.setUnauthorizedUrl("/unauthorize");
// 定義請求攔截規則,key是正則表達式用於匹配訪問的路徑,value則用於指定使用什麽攔截器進行攔截
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 攔截index接口,authc表示需要認證才能訪問
filterChainDefinitionMap.put("/index", "authc");
// anon表示不攔截
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/loginUser", "anon");
// 指定admin接口只允許admin角色的用戶訪問
filterChainDefinitionMap.put("/admin", "roles[admin]");
// 用戶在登錄後可以訪問所有的接口
filterChainDefinitionMap.put("/**", "user");
bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return bean;
}
@Bean("securityManager")
public SecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm) {
// 設置自定義的SecurityManager
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(authRealm);
return manager;
}
@Bean("authRealm")
public AuthRealm authRealm(@Qualifier("credentialMatcher") CredentialMatcher matcher) {
// 設置自定義的Realm
AuthRealm authRealm = new AuthRealm();
authRealm.setCredentialsMatcher(matcher);
return authRealm;
}
@Bean("credentialMatcher")
public CredentialMatcher credentialMatcher() {
// 設置自定義密碼校驗規則
return new CredentialMatcher();
}
// =========== spring 與 shiro 關聯的相關配置 ============
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
// 設置spring在對shiro進行處理的時候,使用的SecurityManager為我們自定義的SecurityManager
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
// 設置代理類
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
}
相關接口及登錄頁面的開發
新建一個 DemoController,用於提供外部訪問的接口。代碼如下:
package org.zero.example.shiro.controller;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.zero.example.shiro.model.User;
import javax.servlet.http.HttpSession;
/**
* @program: shiro
* @description: shiro demo
* @author: 01
* @create: 2018-09-08 18:01
**/
@Slf4j
@Controller
public class DemoController {
@RequestMapping("/login")
public String login() {
return "login";
}
@RequestMapping("/index")
public String index() {
return "index";
}
@RequestMapping("/logout")
public String logout() {
Subject subject = SecurityUtils.getSubject();
if (subject != null) {
subject.logout();
}
return "login";
}
@RequestMapping("/admin")
@ResponseBody
public String admin() {
return "success admin";
}
@RequestMapping("/unauthorize")
public String unauthorize() {
return "unauthorize";
}
@RequestMapping("/loginUser")
public String loginUser(@RequestParam("username") String username,
@RequestParam("password") String password,
HttpSession session) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
User user = (User) subject.getPrincipal();
session.setAttribute("user", user);
return "index";
} catch (Exception e) {
log.error("驗證不通過: {}", e.getMessage());
return "login";
}
}
}
在配置文件中,配置jsp文件所在的路徑:
spring:
mvc:
view:
prefix: /pages/
suffix: .jsp
由於需要跳轉jsp,所以還需配置項目的web resource路徑:
配置好後會生成一個webapp目錄,在該目錄下創建pages目錄,並新建jsp文件。其中login.jsp文件內容如下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Login</title>
</head>
<body>
<h1>歡迎登錄</h1>
<form action="/loginUser" method="post">
<input type="text" name="username"/><br/>
<input type="text" name="password"/><br/>
<input type="submit" value="登錄"/>
</form>
</body>
</html>
index.jsp文件內容如下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Home</title>
</head>
<body>
<h1>歡迎登錄, ${user.username}</h1>
</body>
</html>
unauthorize.jsp文件內容如下:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Unauthorize</title>
</head>
<body>
<h2>無權限訪問!</h2>
</body>
</html>
啟動項目,在沒有登錄的情況下訪問index接口,會跳轉到登錄頁面上:
用戶成功後,就會跳轉到index頁面上:
若使用user用戶訪問admin接口,則會跳轉到權限拒絕頁面上,這符合我們定義的規則:
只有admin用戶才可以訪問所有接口:
如果我們要實現某個接口需要某個權限才能訪問的話,可以在ShiroConfiguration類的shiroFilter方法中,關於定義請求攔截規則那一塊去配置。例如我希望edit只能由擁有edit權限的用戶才能訪問,則添加如下代碼即可:
// 設置用戶需要擁有edit權限才可以訪問edit接口
filterChainDefinitionMap.put("/edit", "perms[edit]");
如果需要開啟權限緩存的話,可以在配置 AuthRealm 的時候進行定義。例如我這裏使用Shiro自帶的權限緩存,如下:
@Bean("authRealm")
public AuthRealm authRealm(@Qualifier("credentialMatcher") CredentialMatcher matcher) {
// 設置自定義的Realm
AuthRealm authRealm = new AuthRealm();
authRealm.setCredentialsMatcher(matcher);
// 設置緩存
authRealm.setCacheManager(new MemoryConstrainedCacheManager());
return authRealm;
}
總結
優點:
- 提供了一套框架,而且這個框架可用,且易於使用
- 靈活,應對需求能力強,Web能力強
- 可以與很多框架和應用進行集成
缺點:
- 學習資料比較少
- 除了需要自己實現RBAC外,操作的界面也需要自己實現
SpringBoot2.x集成Apache Shiro並完成簡單的Case開發