30分鐘瞭解Springboot整合Shiro
前言:06年7月的某日,不才創作了一篇題為《 ofollow,noindex">30分鐘學會如何使用Shiro 》的文章。不在意之間居然斬獲了22萬的閱讀量,許多人因此加了我的聯絡方式諮詢原始碼工程,只可惜當時並沒有專門保留。2年後的今天在機緣巧合之下,我又重拾此話題。希望能帶給小夥伴們在Springboot下如何使用Shiro,當然若各位感興趣我還希望之後能創作一些與它有關的更加深入的知識。作為一個知識分享型博主,我希望能夠幫助大家儘快上手。因此我儘可能去除了與整合無關的干擾因素,方便大家只要按照文章的思路就一定能有所收穫。
專案結構截圖:
專案在結構上沒有任何特殊之處,基本就是MVC的傳統結構重點需要關注的是3個Entity類、2個Controller類和1個Config類。
首先,提供pom的完整文件結構:
<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> <groupId>com.learnhow.springboot</groupId> <artifactId>web</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>web</name> <url>http://maven.apache.org</url> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.4.RELEASE</version> <relativePath /> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> </configuration> </plugin> </plugins> </build> </project>
其次,建立資料庫和表結構。由於我們採用jpa作為資料庫持久層框架,因此我們將建表的任務交給框架自動完成,我們只需要在entity中寫清楚對應關係即可。
CREATE DATABASE enceladus;// enceladus是資料庫的名稱
application.yml
server: port: 8088 spring: application: name: shiro datasource: url: jdbc:mysql://192.168.31.37:3306/enceladus username: root password: 12345678 driver-class-name: com.mysql.jdbc.Driver jpa: database: mysql showSql: true hibernate: ddlAuto: update properties: hibernate: dialect: org.hibernate.dialect.SQL/">MySQL5Dialect format_sql: true
最基礎的Shiro配置至少需要三張主表分別代表使用者(user)、角色(role)、許可權(permission),使用者和角色,角色與許可權之間都是ManyToMany的對應關係,不熟悉實體對應關係的小夥伴可以先去熟悉一下Hibernate。
User.java
import java.io.Serializable; import java.util.List; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.Table; @Entity @Table(name = "user_t") public class User implements Serializable { private static final long serialVersionUID = -3320971805590503443L; @Id @GeneratedValue private long id; private String username; private String password; private String salt; @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "user_role_t", joinColumns = { @JoinColumn(name = "uid") }, inverseJoinColumns = { @JoinColumn(name = "rid") }) private List<SysRole> roles; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getSalt() { return salt; } public void setSalt(String salt) { this.salt = salt; } public List<SysRole> getRoles() { return roles; } public void setRoles(List<SysRole> roles) { this.roles = roles; } public String getCredentialsSalt() { return username + salt + salt; } @Override public String toString() { return "User [id=" + id + ", username=" + username + "]"; } } user
SysRole.java
import java.io.Serializable; import java.util.List; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.Table; @Entity @Table(name = "role_t") public class SysRole implements Serializable { private static final long serialVersionUID = -8687790154329829056L; @Id @GeneratedValue private Integer id; private String role; @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "role_permission_t", joinColumns = { @JoinColumn(name = "rid") }, inverseJoinColumns = { @JoinColumn(name = "pid") }) private List<SysPermission> permissions; @ManyToMany @JoinTable(name = "user_role_t", joinColumns = { @JoinColumn(name = "rid") }, inverseJoinColumns = { @JoinColumn(name = "uid") }) private List<User> users; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } public List<SysPermission> getPermissions() { return permissions; } public void setPermissions(List<SysPermission> permissions) { this.permissions = permissions; } public List<User> getUsers() { return users; } public void setUsers(List<User> users) { this.users = users; } } role
SysPermission.java
import java.io.Serializable; import java.util.List; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.Table; @Entity @Table(name = "permission_t") public class SysPermission implements Serializable { private static final long serialVersionUID = 353629772108330570L; @Id @GeneratedValue private Integer id; private String name; @ManyToMany @JoinTable(name = "role_permission_t", joinColumns = { @JoinColumn(name = "pid") }, inverseJoinColumns = { @JoinColumn(name = "rid") }) private List<SysRole> roles; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public List<SysRole> getRoles() { return roles; } public void setRoles(List<SysRole> roles) { this.roles = roles; } } perm
在註明對應關係以後,jpa會幫助我們建立3張實體表和2張中間表:
最後我們還需要初始化一些基礎資料:
INSERT INTO `permission_t` VALUES (1, 'Retrieve'); INSERT INTO `permission_t` VALUES (2, 'Create'); INSERT INTO `permission_t` VALUES (3, 'Update'); INSERT INTO `permission_t` VALUES (4, 'Delete'); INSERT INTO `role_t` VALUES (1, 'guest'); INSERT INTO `role_t` VALUES (2, 'user'); INSERT INTO `role_t` VALUES (3, 'admin'); INSERT INTO `role_permission_t` VALUES (1, 1); INSERT INTO `role_permission_t` VALUES (1, 2); INSERT INTO `role_permission_t` VALUES (2, 2); INSERT INTO `role_permission_t` VALUES (3, 2); INSERT INTO `role_permission_t` VALUES (1, 3); INSERT INTO `role_permission_t` VALUES (2, 3); INSERT INTO `role_permission_t` VALUES (3, 3); INSERT INTO `role_permission_t` VALUES (4, 3);
至此,前期的準備工作已經完成。下面為了讓Shiro能夠在專案中生效我們需要通過程式碼的方式提供配置資訊。Shiro的安全管理提供了兩個層面的控制:(1)使用者認證:需要使用者通過登陸證明你是你自己。(2)許可權控制:在證明了你是你自己的基礎上系統為當前使用者賦予許可權。後者我們已經在資料庫中完成了大部分配置。
使用者認證的常規手段就是登陸認證,在目前的情況下我們認為只有使用者自己知道登陸密碼。不過Shiro為我們做的更多,它還提供了一套能夠很方便我們使用的密碼雜湊演算法。因為普通的雜湊技巧可以很容易的通過暴力手段破解,我們可以在雜湊的過程中加入一定的演算法複雜度(增加雜湊次數與Salt)從而解決這樣的問題。
import org.apache.shiro.crypto.RandomNumberGenerator; import org.apache.shiro.crypto.SecureRandomNumberGenerator; import org.apache.shiro.crypto.hash.SimpleHash; import org.apache.shiro.util.ByteSource; import com.learnhow.springboot.web.entity.User; public class PasswordHelper { private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator(); public static final String ALGORITHM_NAME = "md5"; // 基礎雜湊演算法 public static final int HASH_ITERATIONS = 2; // 自定義雜湊次數 public void encryptPassword(User user) { // 隨機字串作為salt因子,實際參與運算的salt我們還引入其它干擾因子 user.setSalt(randomNumberGenerator.nextBytes().toHex()); String newPassword = new SimpleHash(ALGORITHM_NAME, user.getPassword(), ByteSource.Util.bytes(user.getCredentialsSalt()), HASH_ITERATIONS).toHex(); user.setPassword(newPassword); } }
這個類幫助我們解決使用者註冊的密碼雜湊問題,當然我們還需要使用同樣的演算法來保證在登陸的時候密碼能夠被雜湊成相同的字串。如果兩次雜湊的結果不同系統就無法完成密碼比對,因此在計算雜湊因子的時候我們不能引入變數,例如我們可以將username作為salt因子加入雜湊演算法,但是不能選擇password或datetime,具體原因各位請手動測試。
另外為了幫助Shiro能夠正確為當前登陸使用者做認證和賦權,我們需要實現自定義的Realm。具體來說就是實現doGetAuthenticationInfo和doGetAuthorizationInfo,這兩個方法前者負責登陸認證後者負責提供一個許可權資訊。
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.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; import com.learnhow.springboot.web.entity.SysPermission; import com.learnhow.springboot.web.entity.SysRole; import com.learnhow.springboot.web.entity.User; import com.learnhow.springboot.web.service.UserService; public class EnceladusShiroRealm extends AuthorizingRealm { @Autowired private UserService userService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); String username = (String) principals.getPrimaryPrincipal(); User user = userService.findUserByName(username); for (SysRole role : user.getRoles()) { authorizationInfo.addRole(role.getRole()); for (SysPermission permission : role.getPermissions()) { authorizationInfo.addStringPermission(permission.getName()); } } return authorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal(); User user = userService.findUserByName(username); if (user == null) { return null; } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getCredentialsSalt()), getName()); return authenticationInfo; } }
還記得前面我們說過,認證的時候我們需要提供相同的雜湊演算法嗎?可是在上面的程式碼裡,我們並未提供。那麼Shiro是怎麼做的呢?AuthorizingRealm是一個抽象類,我們會在另外的配置檔案裡向它提供基礎演算法與雜湊次數這兩個變數。
import java.util.HashMap; import java.util.Map; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.mgt.SecurityManager; 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 ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, String> filterChainDefinitionMap = new HashMap<String, String>(); shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setUnauthorizedUrl("/unauthc"); shiroFilterFactoryBean.setSuccessUrl("/home/index"); filterChainDefinitionMap.put("/*", "anon"); filterChainDefinitionMap.put("/authc/index", "authc"); filterChainDefinitionMap.put("/authc/admin", "roles[admin]"); filterChainDefinitionMap.put("/authc/renewable", "perms[Create,Update]"); filterChainDefinitionMap.put("/authc/removable", "perms[Delete]"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName(PasswordHelper.ALGORITHM_NAME); // 雜湊演算法 hashedCredentialsMatcher.setHashIterations(PasswordHelper.HASH_ITERATIONS); // 雜湊次數 return hashedCredentialsMatcher; } @Bean public EnceladusShiroRealm shiroRealm() { EnceladusShiroRealm shiroRealm = new EnceladusShiroRealm(); shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); // 原來在這裡 return shiroRealm; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(shiroRealm()); return securityManager; } @Bean public PasswordHelper passwordHelper() { return new PasswordHelper(); } }
接下來,我們將目光集中到上文的shirFilter方法中。Shiro通過一系列filter來控制訪問許可權,並在它的內部為我們預先定義了多個過濾器,我們可以直接通過字串配置這些過濾器。
常用的過濾器如下:
authc:所有已登陸使用者可訪問
roles:有指定角色的使用者可訪問,通過[ ]指定具體角色,這裡的角色名稱與資料庫中配置一致
perms:有指定許可權的使用者可訪問,通過[ ]指定具體許可權,這裡的許可權名稱與資料庫中配置一致
anon:所有使用者可訪問,通常作為指定頁面的靜態資源時使用
為了測試方便我們不引入頁面配置直接通過rest方式訪問
不受許可權控制訪問的地址
import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.learnhow.springboot.web.PasswordHelper; import com.learnhow.springboot.web.entity.User; import com.learnhow.springboot.web.service.UserService; @RestController @RequestMapping public class HomeController { @Autowired private UserService userService; @Autowired private PasswordHelper passwordHelper; @GetMapping("login") public Object login() { return "Here is Login page"; } @GetMapping("unauthc") public Object unauthc() { return "Here is Unauthc page"; } @GetMapping("doLogin") public Object doLogin(@RequestParam String username, @RequestParam String password) { UsernamePasswordToken token = new UsernamePasswordToken(username, password); Subject subject = SecurityUtils.getSubject(); try { subject.login(token); } catch (IncorrectCredentialsException ice) { return "password error!"; } catch (UnknownAccountException uae) { return "username error!"; } User user = userService.findUserByName(username); subject.getSession().setAttribute("user", user); return "SUCCESS"; } @GetMapping("register") public Object register(@RequestParam String username, @RequestParam String password) { User user = new User(); user.setUsername(username); user.setPassword(password); passwordHelper.encryptPassword(user); userService.saveUser(user); return "SUCCESS"; } }
需要指定許可權可以訪問的地址
import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.learnhow.springboot.web.entity.User; @RestController @RequestMapping("authc") public class AuthcController { @GetMapping("index") public Object index() { Subject subject = SecurityUtils.getSubject(); User user = (User) subject.getSession().getAttribute("user"); return user.toString(); } @GetMapping("admin") public Object admin() { return "Welcome Admin"; } // delete @GetMapping("removable") public Object removable() { return "removable"; } // insert & update @GetMapping("renewable") public Object renewable() { return "renewable"; } }
這樣,我們對在Springboot下如何使用Shiro的介紹就告一段落,有希望看到後續更加深入文章的小夥伴歡迎踴躍馬克。另外,大部分程式碼我已經在文章中提供如果需要原始碼的小夥伴也可以@我。