綜合概述
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/
版權所有,歡迎轉載,轉載請注明原文作者及出處。