綜合概述
Shiro是Apache旗下的一個開源專案,它是一個非常易用的安全框架,提供了包括認證、授權、加密、會話管理等功能,與Spring Security一樣屬基於許可權的安全框架,但是與Spring Security 相比,Shiro使用了比較簡單易懂易於使用的授權方式。Shiro屬於輕量級框架,相對於Spring Security簡單很多,並沒有security那麼複雜。
優勢特點
它是一個功能強大、靈活的、優秀的、開源的安全框架。
它可以勝任身份驗證、授權、企業會話管理和加密等工作。
它易於使用和理解,與Spring Security相比,入門門檻低。
主要功能
- 驗證使用者身份
- 使用者訪問許可權控制
- 支援單點登入(SSO)功能
- 可以響應認證、訪問控制,或Session事件
- 支援提供“Remember Me”服務
- .......
框架體系
Shiro 的整體框架大致如下圖所示(圖片來自網際網路):
Authentication(認證), Authorization(授權), Session Management(會話管理), Cryptography(加密)代表Shiro應用安全的四大基石。
它們分別是:
- Authentication(認證):使用者身份識別,通常被稱為使用者“登入”。
- Authorization(授權):訪問控制。比如某個使用者是否具有某個操作的使用許可權。
- Session Management(會話管理):特定於使用者的會話管理,甚至在非web 應用程式。
- Cryptography(加密):在對資料來源使用加密演算法加密的同時,保證易於使用。
除此之外,還有其他的功能來支援和加強這些不同應用環境下安全領域的關注點。
特別是對以下的功能支援:
- Web支援:Shiro 提供的 web 支援 api ,可以很輕鬆的保護 web 應用程式的安全。
- 快取:快取是 Apache Shiro 保證安全操作快速、高效的重要手段。
- 併發:Apache Shiro 支援多執行緒應用程式的併發特性。
- 測試:支援單元測試和整合測試,確保程式碼和預想的一樣安全。
- “Run As”:這個功能允許使用者在許可的前提下假設另一個使用者的身份。
- “Remember Me”:跨 session 記錄使用者的身份,只有在強制需要時才需要登入。
主要流程
在概念層,Shiro 架構包含三個主要的理念:Subject, SecurityManager 和 Realm。下面的圖展示了這些元件如何相互作用,我們將在下面依次對其進行描述。
Shiro執行流程圖(圖片來自網際網路)
三個主要理念:
- Subject:代表當前使用者,Subject 可以是一個人,也可以是第三方服務、守護程式帳戶、時鐘守護任務或者其它當前和軟體互動的任何事件。
- SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架構的核心,配合內部安全元件共同組成安全傘。
- Realms:用於進行許可權資訊的驗證,我們自己實現。Realm 本質上是一個特定的安全 DAO:它封裝與資料來源連線的細節,得到Shiro 所需的相關的資料。在配置 Shiro 的時候,你必須指定至少一個Realm 來實現認證(authentication)和/或授權(authorization)。
我們需要實現Realms的Authentication 和 Authorization。其中 Authentication 是用來驗證使用者身份,Authorization 是授權訪問控制,用於對使用者進行的操作授權,證明該使用者是否允許進行當前操作,如訪問某個連結,某個資原始檔等。
實現案例
接下來,我們就通過一個具體的案例,來講解如何進行Shiro的整合,然後藉助Shiro實現登入認證和訪問控制。
生成專案模板
為方便我們初始化專案,Spring Boot給我們提供一個專案模板生成網站。
1. 開啟瀏覽器,訪問:https://start.spring.io/
2. 根據頁面提示,選擇構建工具,開發語言,專案資訊等。
3. 點選 Generate the project,生成專案模板,生成之後會將壓縮包下載到本地。
4. 使用IDE匯入專案,我這裡使用Eclipse,通過匯入Maven專案的方式匯入。
新增相關依賴
清理掉不需要的測試類及測試依賴,新增 Maven 相關依賴,這裡需要新增上WEB、Swagger、JPA和Shiro的依賴,Swagger的新增是為了方便介面測試。
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 http://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.1.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.louis.springboot</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!-- web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- swagger --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> <!-- jpa --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- shiro --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> <!-- 打包時拷貝MyBatis的對映檔案 --> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/sqlmap/*.xml</include> </includes> <filtering>false</filtering> </resource> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.*</include> </includes> <filtering>true</filtering> </resource> </resources> </build> </project>
新增相關配置
1.新增資料來源和jpa相關配置
將application.properties檔案改名為application.yml ,並在其中新增MySQL資料來源連線資訊。
注意:
這裡需要首先建立一個MySQL資料庫,並輸入自己的使用者名稱和密碼。這裡的資料庫是springboot。
另外,如果你使用的是MySQL 5.x及以前版本,驅動配置driverClassName是com.mysql.jdbc.Driver。
application.yml
server: port: 8080 spring: datasource: driverClassName: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8 username: root password: 123456 jpa: show-sql: true # 預設false,在日誌裡顯示執行的sql語句 database: mysql hibernate.ddl-auto: update #指定為update,每次啟動專案檢測表結構有變化的時候會新增欄位,表不存在時會新建,如果指定create,則每次啟動專案都會清空資料並刪除表,再新建 properties.hibernate.dialect: org.hibernate.dialect.MySQL5Dialect database-platform: org.hibernate.dialect.MySQL5Dialect hibernate: naming: implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl #指定jpa的自動錶生成策略,駝峰自動對映為下劃線格式 #physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
2. 新增swagger 配置
新增一個swagger 配置類,在工程下新建 config 包並新增一個 SwaggerConfig 配置類。
SwaggerConfig.java
package com.louis.springboot.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket createRestApi(){ return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.any()) .paths(PathSelectors.any()).build(); } private ApiInfo apiInfo(){ return new ApiInfoBuilder() .title("SpringBoot API Doc") .description("This is a restful api document of Spring Boot.") .version("1.0") .build(); } }
編寫業務程式碼
新增一個使用者類User,包含使用者名稱和密碼,用來進行登入認證,另外使用者可以擁有角色。
User.java
package com.louis.springboot.demo.model; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.OneToMany; @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Column(unique = true) private String name; private String password; @OneToMany(cascade = CascadeType.ALL,mappedBy = "user") private List<Role> roles; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public List<Role> getRoles() { return roles; } public void setRoles(List<Role> roles) { this.roles = roles; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
新增一個角色類Role,表示使用者角色,角色擁有可操作的許可權集合。
Role.java
package com.louis.springboot.demo.model; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.ManyToOne; import javax.persistence.OneToMany; @Entity public class Role { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String roleName; @ManyToOne(fetch = FetchType.EAGER) private User user; @OneToMany(cascade = CascadeType.ALL,mappedBy = "role") private List<Permission> permissions; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getRoleName() { return roleName; } public void setRoleName(String roleName) { this.roleName = roleName; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } public List<Permission> getPermissions() { return permissions; } public void setPermissions(List<Permission> permissions) { this.permissions = permissions; } }
新增一個許可權類Permission,表示資源訪問許可權。
Permission.java
package com.louis.springboot.demo.model; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.ManyToOne; @Entity public class Permission { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String permission; @ManyToOne(fetch = FetchType.EAGER) private Role role; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getPermission() { return permission; } public void setPermission(String permission) { this.permission = permission; } public Role getRole() { return role; } public void setRole(Role role) { this.role = role; } }
新增一個DAO基礎介面,用來被其他DAO繼承。
BaseDao.java
package com.louis.springboot.demo.dao; import java.io.Serializable; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.PagingAndSortingRepository; @NoRepositoryBean public interface BaseDao<T, I extends Serializable> extends PagingAndSortingRepository<T, I>, JpaSpecificationExecutor<T> { }
新增一個UserDao,用來操作使用者資訊。
UserDao.java
package com.louis.springboot.demo.dao; import com.louis.springboot.demo.model.User; public interface UserDao extends BaseDao<User, Long> { User findByName(String name); }
新增一個RoleDao,用來操作角色資訊。
RoleDao.java
package com.louis.springboot.demo.dao; import com.louis.springboot.demo.model.Role; public interface RoleDao extends BaseDao<Role, Long> { }
新增一個LoginService服務介面。
LoginService.java
package com.louis.springboot.demo.service; import com.louis.springboot.demo.model.Role; import com.louis.springboot.demo.model.User; public interface LoginService { User addUser(User user); Role addRole(Role role); User findByName(String name); }
新增一個LoginServiceImpl,實現服務功能,這裡為了方便,在插入角色的時候會預設設定其許可權。
LoginServiceImpl.java
package com.louis.springboot.demo.service.impl; import java.util.ArrayList; import java.util.List; import javax.transaction.Transactional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.louis.springboot.demo.dao.RoleDao; import com.louis.springboot.demo.dao.UserDao; import com.louis.springboot.demo.model.Permission; import com.louis.springboot.demo.model.Role; import com.louis.springboot.demo.model.User; import com.louis.springboot.demo.service.LoginService; @Service @Transactional public class LoginServiceImpl implements LoginService { @Autowired private UserDao userDao; @Autowired private RoleDao roleDao; //新增使用者 @Override public User addUser(User user) { userDao.save(user); return user; } //新增角色 @Override public Role addRole(Role role) { User user = userDao.findByName(role.getUser().getName()); role.setUser(user); Permission permission1 = new Permission(); permission1.setPermission("create"); permission1.setRole(role); Permission permission2 = new Permission(); permission2.setPermission("update"); permission2.setRole(role); List<Permission> permissions = new ArrayList<Permission>(); permissions.add(permission1); permissions.add(permission2); role.setPermissions(permissions); roleDao.save(role); return role; } //查詢使用者通過使用者名稱 @Override public User findByName(String name) { return userDao.findByName(name); } }
新增一個登入控制器,編寫相關的介面。create介面新增了@RequiresPermissions("create"),用於進行許可權註解測試。
LoginController.java
package com.louis.springboot.demo.controller; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresRoles; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import com.louis.springboot.demo.model.Role; import com.louis.springboot.demo.model.User; import com.louis.springboot.demo.service.LoginService; @RestController public class LoginController { @Autowired private LoginService loginService; /** * POST登入 * @param map * @return */ @PostMapping(value = "/login") public String login(@RequestBody User user) { // 新增使用者認證資訊 UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(user.getName(), user.getPassword()); // 進行驗證,這裡可以捕獲異常,然後返回對應資訊 SecurityUtils.getSubject().login(usernamePasswordToken); return "login ok!"; } /** * 新增使用者 * @param user * @return */ @PostMapping(value = "/addUser") public String addUser(@RequestBody User user) { user = loginService.addUser(user); return "addUser is ok! \n" + user; } /** * 新增角色 * @param role * @return */ @PostMapping(value = "/addRole") public String addRole(@RequestBody Role role) { role = loginService.addRole(role); return "addRole is ok! \n" + role; } /** * 註解的使用 * @return */ @RequiresRoles("admin") @RequiresPermissions("create") @GetMapping(value = "/create") public String create() { return "Create success!"; } @GetMapping(value = "/index") public String index() { return "index page!"; } @GetMapping(value = "/error") public String error() { return "error page!"; } /** * 退出的時候是get請求,主要是用於退出 * @return */ @GetMapping(value = "/login") public String login() { return "login"; } @GetMapping(value = "/logout") public String logout() { return "logout"; } }
新增一個MyShiroRealm並繼承AuthorizingRealm,實現其中的兩個方法。
doGetAuthenticationInfo:實現使用者認證,通過服務載入使用者資訊並構造認證物件返回。
doGetAuthorizationInfo:實現許可權認證,通過服務載入使用者角色和許可權資訊設定進去。
MyShiroRealm.java
package com.louis.springboot.demo.config; 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 com.louis.springboot.demo.model.Permission; import com.louis.springboot.demo.model.Role; import com.louis.springboot.demo.model.User; import com.louis.springboot.demo.service.LoginService; /** * 實現AuthorizingRealm介面使用者使用者認證 * @author Louis * @date Jun 20, 2019 */ public class MyShiroRealm extends AuthorizingRealm { @Autowired private LoginService loginService; /** * 使用者認證 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // 加這一步的目的是在Post請求的時候會先進認證,然後在到請求 if (authenticationToken.getPrincipal() == null) { return null; } // 獲取使用者資訊 String name = authenticationToken.getPrincipal().toString(); User user = loginService.findByName(name); if (user == null) { // 這裡返回後會報出對應異常 return null; } else { // 這裡驗證authenticationToken和simpleAuthenticationInfo的資訊 SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, user.getPassword().toString(), getName()); return simpleAuthenticationInfo; } } /** * 角色許可權和對應許可權新增 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // 獲取登入使用者名稱 String name = (String) principalCollection.getPrimaryPrincipal(); // 查詢使用者名稱稱 User user = loginService.findByName(name); // 新增角色和許可權 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); for (Role role : user.getRoles()) { // 新增角色 simpleAuthorizationInfo.addRole(role.getRoleName()); for (Permission permission : role.getPermissions()) { // 新增許可權 simpleAuthorizationInfo.addStringPermission(permission.getPermission()); } } return simpleAuthorizationInfo; } }
新增一個Shiro配置類,主要配置路由的訪問控制,以及注入自定義的認證器MyShiroRealm。
ShiroConfig.java
package com.louis.springboot.demo.config; import java.util.HashMap; import java.util.Map; 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.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ShiroConfig { // 將自己的驗證方式加入容器 @Bean public MyShiroRealm myShiroRealm() { MyShiroRealm myShiroRealm = new MyShiroRealm(); return myShiroRealm; } // 許可權管理,配置主要是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> filterMap = new HashMap<String, String>(); // 登出 filterMap.put("/logout", "logout"); // swagger filterMap.put("/swagger**/**", "anon"); filterMap.put("/webjars/**", "anon"); filterMap.put("/v2/**", "anon"); // 對所有使用者認證 filterMap.put("/**", "authc"); // 登入 shiroFilterFactoryBean.setLoginUrl("/login"); // 首頁 shiroFilterFactoryBean.setSuccessUrl("/index"); // 錯誤頁面,認證不通過跳轉 shiroFilterFactoryBean.setUnauthorizedUrl("/error"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap); return shiroFilterFactoryBean; } // 加入註解的使用,不加入這個註解不生效 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } }
編譯測試執行
1. 右鍵專案 -> Run as -> Maven install,開始執行Maven構建,第一次會下載Maven依賴,可能需要點時間,如果出現如下資訊,就說明專案編譯打包成功了。
2. 右鍵檔案 DemoApplication.java -> Run as -> Java Application,開始啟動應用,如果一開始資料庫沒有對應的表,在應用啟動時會建立,我們可以通過控制檯檢視到對應的SQL語句。
3. 開啟瀏覽器,訪問:http://localhost:8080/swagger-ui.html,進入swagger介面文件介面。
4. 首先用MySQL客戶端,開啟資料庫,往使用者表裡面插入一條記錄,{id=1, name="admin", password="123"}。
然後試著用Swagger呼叫addUser往使用者表插入一條記錄。
{ "id": 2, "name": "xiaoming", "password": "123" }
結果返回"error page!",這是因為我們沒有登入,還沒有操作許可權。
接著呼叫POST的login介面,輸入以下使用者資訊進行登入。
{ "name": "admin", "password": "123" }
登入成功之後,返回“login ok!”資訊。
再次呼叫addUser往使用者表插入記錄,發現記錄已經可以成功插入了。
通過客戶端工具我們也可以檢視到記錄已經插入進來了。
通過上面的測試,我們已經成功的驗證了,受保護的介面需要在登入之後才允許訪問。接下來我們來測試一下許可權註解的效果,我們在create方法上加上了許可權註解@RequiresPermissions("create"),表示使用者需要擁有"create"的許可權才能訪問。
先嚐試呼叫以下create介面,發現儘管我們已經登入了,依然因為沒有許可權返回了“error page!”。
然後我們呼叫addRole插入以下角色記錄,這個角色關聯了我們當前登入admin使用者,且角色在建立時我們程式碼預設設定擁有了“create”許可權。
{ "id": 1, "roleName": "admin", "user": { "name": "admin" } }
如果執行正確的話,會返回如下資訊,說明角色已經成功插入了。
然後我們再一次呼叫create介面,因為此刻admin使用者擁有admin角色,而admin角色擁有“create”許可權,所以已經具有介面訪問許可權了。
參考資料
W3C資料:https://www.w3cschool.cn/shiro/
百度百科:https://baike.baidu.com/item/shiro/17753571?fr=aladdin
相關導航
原始碼下載
碼雲:https://gitee.com/liuge1988/spring-boot-demo.git
作者:朝雨憶輕塵
出處:https://www.cnblogs.com/xifengxiaoma/
版權所有,歡迎轉載,轉載請註明原文作者及出處。