前言:06年7月的某日,不才創做了一篇題爲《30分鐘學會如何使用Shiro》的文章。不在乎之間竟然斬獲了22萬的閱讀量,許多人所以加了個人聯繫方式諮詢源碼工程,只惋惜當時並無專門保留。2年後的今天在機緣巧合之下,我又重拾此話題。但願能帶給小夥伴們在Springboot下如何使用Shiro,固然若各位感興趣我還但願以後能創做一些與它有關的更加深刻的知識。做爲一個知識分享型博主,我但願可以幫助你們儘快上手。所以我儘量去除了與整合無關的干擾因素,方便你們只要按照文章的思路就必定能有所收穫。html
項目結構截圖:java
項目在結構上沒有任何特殊之處,基本就是MVC的傳統結構重點須要關注的是3個Entity類、2個Controller類和1個Config類。mysql
首先,提供pom的完整文檔結構:web
<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.ymlspring
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.MySQL5Dialect
format_sql: true
最基礎的Shiro配置至少須要三張主表分別表明用戶(user)、角色(role)、權限(permission),用戶和角色,角色與權限之間都是ManyToMany的對應關係,不熟悉實體對應關係的小夥伴能夠先去熟悉一下Hibernate。sql
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 + "]"; } }
SysRole.javaapache
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; } }
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; } }
在註明對應關係之後,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的介紹就告一段落,有但願看到後續更加深刻文章的小夥伴歡迎踊躍馬克。另外,大部分代碼我已經在文章中提供若是須要源碼的小夥伴也能夠@我。
寫在後面的話:
最近有很多朋友在看了個人博客之後加個人QQ或者發郵件要求提供演示源碼,爲了方便交流我索性建了一個技術交流羣,從此有些源碼我可能就放羣資料裏面了。固然以前的一些東西還在補充中,有些問題也但願大夥能共同交流。QQ羣號:960652410