本來想寫一篇spring boot整合Shiro實現許可權驗證的文章,發現這篇寫的非常不錯,就直接借鑑了!
(1). Shiro簡單介紹
Shiro是Apache下的一個開源專案,我們稱之為Apache Shiro。它是一個很易用與Java專案的的安全框架,提供了認證、授權、加密、會話管理,與spring Security
一樣都是做一個許可權的安全框架,但是與Spring Security 相比,在於 Shiro 使用了比較簡單易懂易於使用的授權方式。
Apache Shiro 的三大核心元件
- Subject 當前使用者操作
- SecurityManager 用於管理所有的Subject
- Realms 用於進行許可權資訊的驗證,也是我們需要自己實現的。
我們需要實現Realms的Authentication 和 Authorization。其中 Authentication 是用來驗證使用者身份,Authorization 是授權訪問控制,用於對使用者進行的操作授權,證明該使用者是否允許進行當前操作,如訪問某個連結,某個資原始檔等。
Apache Shiro 核心通過 Filter 來實現,就好像SpringMvc 通過DispachServlet 來主控制一樣。
既然是使用 Filter 一般也就能猜到,是通過URL規則來進行過濾和許可權校驗,所以我們需要定義一系列關於URL的規則和訪問許可權。
另外我們可以通過Shiro 提供的會話管理來獲取Session中的資訊。Shiro 也提供了快取支援,使用 CacheManager 來管理。
官方網站:http://shiro.apache.org/
完整架構圖:
Shiro是很強大的一個安全框架,這裡只是拋裝引玉下,還有很多的需要大家自己去學習Shiro。
(2). 整合Shiro核心分析
整合Shiro的話,我們需要知道Shiro框架大概的一些管理物件。
第一:ShiroFilterFactory,Shiro過濾器工廠類,具體的實現類是:ShiroFilterFactoryBean,此實現類是依賴於SecurityManager安全管理器。
第二:SecurityManager,Shiro的安全管理,主要是身份認證的管理,快取管理,cookie管理,所以在實際開發中我們主要是和SecurityManager進行打交道的,ShiroFilterFactory主要配置好了Filter就可以了。當然SecurityManager並進行身份認證快取的實現,我們需要進行對應的編碼然後進行注入到安全管理器中。
第三:Realm,用於身份資訊許可權資訊的驗證。
第四:其它的就是快取管理,記住登入之類的,這些大部分都是需要自己進行簡單的實現,然後注入到SecurityManager讓Shiro的安全管理器進行管理就好了。
(3). 無Shiro的Spring Boot
我們先編寫一個無Shiro的簡單的框架,在這個框架中我們可以訪問到index,login,userInfo,userInfoAdd。
這個步驟對於有Spring Boot基礎的就應該很簡單了,在這裡簡單的介紹下:
(a) 新建一個maven Java project,取名為spring-boot-shiro1
(b) 在pom.xml中引入基本依賴,在這裡還沒有引入shiro等的依賴:
-
<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.example</groupId>
-
<artifactId>spring-boot-shiro1</artifactId>
-
<version>0.0.1-SNAPSHOT</version>
-
<packaging>jar</packaging>
-
-
-
<properties>
-
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-
</properties>
-
-
-
-
<parent>
-
<groupId>org.springframework.boot</groupId>
-
<artifactId>spring-boot-starter-parent</artifactId>
-
<version>1.4.0.RELEASE</version>
-
</parent>
-
-
-
<dependencies>
-
-
<dependency>
-
<groupId>org.springframework.boot</groupId>
-
<artifactId>spring-boot-starter-web</artifactId>
-
</dependency>
-
-
<dependency>
-
<groupId>org.springframework.boot</groupId>
-
<artifactId>spring-boot-starter-thymeleaf</artifactId>
-
</dependency>
-
-
-
<dependency>
-
<groupId>org.springframework.boot</groupId>
-
<artifactId>spring-boot-devtools</artifactId>
-
<optional>true</optional>
-
</dependency>
-
-
</dependencies>
-
</project>
(c) 編寫網頁檔案:
index.html,login.html,userInfo.html,userInfoAdd.html
這個檔案存在在src/main/resouces/templates, 這幾個檔案中都是簡單的程式碼,只有登入介面中有賬號和密碼:
index.html
-
<!DOCTYPE html>
-
<html>
-
<head>
-
<meta charset="UTF-8" />
-
<title>Insert title here</title>
-
</head>
-
<body>
-
<h3>index</h3>
-
</body>
-
</html>
login.html
-
<!DOCTYPE html>
-
<html>
-
<head>
-
<meta charset="UTF-8" />
-
<title>Insert title here</title>
-
</head>
-
<body>
-
錯誤資訊:<h4 th:text="${msg}"></h4>
-
<form action="" method="post">
-
<p>賬號:<input type="text" name="username" value="admin"/></p>
-
<p>密碼:<input type="text" name="password" value="123456"/></p>
-
<p><input type="submit" value="登入"/></p>
-
</form>
-
</body>
-
</html>
userInfo.html,
-
<!DOCTYPE html>
-
<html>
-
<head>
-
<meta charset="UTF-8" />
-
<title>Insert title here</title>
-
</head>
-
<body>
-
<h3>使用者查詢介面</h3>
-
</body>
-
</html>
userInfoAdd.html
<h3>使用者新增介面</h3>
(d)編寫啟動類
-
package com.example;
-
-
import org.springframework.boot.SpringApplication;
-
import org.springframework.boot.autoconfigure.SpringBootApplication;
-
-
@SpringBootApplication
-
public class Application {
-
public static void main(String[] args) {
-
SpringApplication.run(Application.class, args);
-
}
-
}
(e)編寫HomeController類
新建HomeController類
-
package com.example.controller;
-
-
import java.util.Map;
-
-
import javax.servlet.http.HttpServletRequest;
-
-
import org.apache.shiro.authc.IncorrectCredentialsException;
-
import org.apache.shiro.authc.UnknownAccountException;
-
import org.springframework.stereotype.Controller;
-
import org.springframework.web.bind.annotation.RequestMapping;
-
import org.springframework.web.bind.annotation.RequestMethod;
-
-
@Controller
-
public class HomeController {
-
@RequestMapping({ "/", "index" })
-
public String index() {
-
return "/index";
-
}
-
-
@RequestMapping(value = "/login", method = RequestMethod.GET)
-
public String login() {
-
return "/login";
-
}
-
}
整合shiro大概分這麼一個步驟:
(a) pom.xml中新增Shiro依賴;
(b) 注入Shiro Factory和SecurityManager。
(c) 身份認證
(d) 許可權控制
(a) pom.xml中新增Shiro依賴;
要使用Shiro進行許可權控制,那麼很明顯的就需要新增對Shiro的依賴包,在pom.xml中加入如下配置:
-
-
<dependency>
-
<groupId>org.apache.shiro</groupId>
-
<artifactId>shiro-spring</artifactId>
-
<version>1.2.3</version>
-
</dependency>
(b) 注入Shiro Factory和SecurityManager。
在Spring中注入類都是使用配置檔案的方式,在Spring Boot中是使用註解的方式,那麼應該如何進行實現呢?
Shiro幾個核心的類,第一就是ShiroFilterFactory,第二就是SecurityManager,那麼最簡單的配置就是注入這兩個類就ok了,那麼如何注入呢?看如下程式碼:
新建ShiroConfiguration.java
-
package com.example.config.shiro;
-
-
import java.util.LinkedHashMap;
-
import java.util.Map;
-
-
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
-
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 ShiroConfiguration {
-
-
@Bean
-
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
-
System.out.println("ShiroConfiguration.shiroFilter()");
-
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
-
-
// 必須設定SecuritManager
-
shiroFilterFactoryBean.setSecurityManager(securityManager);
-
-
// 攔截器
-
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
-
-
// 配置退出過濾器,其中的具體程式碼Shiro已經替我們實現了
-
filterChainDefinitionMap.put("/logout", "logout");
-
-
// :這是一個坑呢,一不小心程式碼就不好使了;
-
//
-
-
filterChainDefinitionMap.put("/**", "authc");
-
-
// 如果不設定預設會自動尋找Web工程根目錄下的"/login.jsp"頁面
-
shiroFilterFactoryBean.setLoginUrl("/login");
-
// 登入成功後要跳轉的連結
-
shiroFilterFactoryBean.setSuccessUrl("/index");
-
// 未授權介面;
-
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
-
-
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
-
return shiroFilterFactoryBean;
-
-
}
-
-
@Bean
-
public SecurityManager securityManager() {
-
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
-
-
return securityManager;
-
}
-
-
-
-
}
這裡說下:ShiroFilterFactory中已經由Shiro官方實現的過濾器:
Shiro內建的FilterChain
Filter Name
|
Class
|
anon
|
org.apache.shiro.web.filter.authc.AnonymousFilter
|
authc
|
org.apache.shiro.web.filter.authc.FormAuthenticationFilter
|
authcBasic
|
org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
|
perms
|
org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
|
port
|
org.apache.shiro.web.filter.authz.PortFilter
|
rest
|
org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
|
roles
|
org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
|
ssl
|
org.apache.shiro.web.filter.authz.SslFilter
|
user
|
org.apache.shiro.web.filter.authc.UserFilter
|
anon:所有url都都可以匿名訪問;
authc: 需要認證才能進行訪問;
user:配置記住我或認證通過可以訪問;
這幾個是我們會用到的,在這裡說明下,其它的請自行查詢文件進行學習。
這時候我們執行程式,訪問/index頁面我們會發現自動跳轉到了login頁面,當然這個時候輸入賬號和密碼是無法進行訪問的。下面這才是重點:任何身份認證,如何許可權控制。
(c) 身份認證
在認證、授權內部實現機制中都有提到,最終處理都將交給Real進行處理。因為在Shiro中,最終是通過Realm來獲取應用程式中的使用者、角色及許可權資訊的。通常情況下,在Realm中會直接從我們的資料來源中獲取Shiro需要的驗證資訊。可以說,Realm是專用於安全框架的DAO.
認證實現
Shiro的認證過程最終會交由Realm執行,這時會呼叫Realm的getAuthenticationInfo(token)方法。
該方法主要執行以下操作:
1、檢查提交的進行認證的令牌資訊
2、根據令牌資訊從資料來源(通常為資料庫)中獲取使用者資訊
3、對使用者資訊進行匹配驗證。
4、驗證通過將返回一個封裝了使用者資訊的AuthenticationInfo例項。
5、驗證失敗則丟擲AuthenticationException異常資訊。
而在我們的應用程式中要做的就是自定義一個Realm類,繼承AuthorizingRealm抽象類,過載doGetAuthenticationInfo (),重寫獲取使用者資訊的方法。
既然需要進行身份許可權控制,那麼少不了建立使用者實體類,許可權實體類。
在許可權管理系統中,有這麼幾個角色很重要,這個要是不清楚的話,那麼就很難理解,我們為什麼這麼編碼了。第一是使用者表:在使用者表中儲存了使用者的基本資訊,賬號、密碼、姓名,性別等;第二是:許可權表(資源+控制許可權):這個表中主要是儲存了使用者的URL地址,許可權資訊;第三就是角色表:在這個表重要儲存了系統存在的角色;第四就是關聯表:使用者-角色管理表(使用者在系統中都有什麼角色,比如admin,vip等),角色-許可權關聯表(每個角色都有什麼許可權可以進行操作)。依據這個理論,我們進行來進行編碼,很明顯的我們第一步就是要進行實體類的建立。在這裡我們使用Mysql和JPA進行運算元據庫。
那麼我們先在pom.xml中引入mysql和JPA的依賴:
UserInfo.java、SysRole.java、SysPermission.java至於之前的關聯表我們使用JPA進行自動生成。
-
-
<dependency>
-
<groupId>org.springframework.boot</groupId>
-
<artifactId>spring-boot-starter-data-jpa</artifactId>
-
</dependency>
-
-
-
<dependency>
-
<groupId>mysql</groupId>
-
<artifactId>mysql-connector-java</artifactId>
-
</dependency>
配置src/main/resouces/application.properties配置資料庫和jpa(application.properties新建一個即可):
-
########################################################
-
###datasource
-
########################################################
-
spring.datasource.url = jdbc:mysql://localhost:3306/test
-
spring.datasource.username = root
-
spring.datasource.password = root
-
spring.datasource.driverClassName = com.mysql.jdbc.Driver
-
spring.datasource.max-active=20
-
spring.datasource.max-idle=8
-
spring.datasource.min-idle=8
-
spring.datasource.initial-size=10
-
-
-
-
########################################################
-
### Java Persistence Api
-
########################################################
-
# Specify the DBMS
-
spring.jpa.database = MYSQL
-
# Show or not log for each sql query
-
spring.jpa.show-sql = true
-
# Hibernate ddl auto (create, create-drop, update)
-
spring.jpa.hibernate.ddl-auto = update
-
# Naming strategy
-
#[org.hibernate.cfg.ImprovedNamingStrategy | org.hibernate.cfg.DefaultNamingStrategy]
-
spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.DefaultNamingStrategy
-
# stripped before adding them to the entity manager)
-
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect
準備工作準備好之後,那麼就可以編寫實體類了:
UserInfo.java
-
package com.example.domain;
-
-
import java.io.Serializable;
-
import java.util.List;
-
-
import javax.persistence.Column;
-
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;
-
-
/**
-
* 使用者資訊.
-
* @author Administrator
-
*
-
*/
-
@Entity
-
public class UserInfo implements Serializable {
-
-
/**
-
*
-
*/
-
private static final long serialVersionUID = 1L;
-
-
@Id
-
@GeneratedValue
-
private long uid;// 使用者id
-
-
@Column(unique = true)
-
private String username;// 帳號
-
-
private String name;// 名稱(暱稱或者真實姓名,不同系統不同定義)
-
-
private String password; // 密碼;
-
private String salt;// 加密密碼的鹽
-
-
private byte state;// 使用者狀態,0:建立未認證(比如沒有啟用,沒有輸入驗證碼等等)--等待驗證的使用者 ,
-
// 1:正常狀態,2:使用者被鎖定.
-
-
@ManyToMany(fetch = FetchType.EAGER) // 立即從資料庫中進行載入資料
-
@JoinTable(name = "SysUserRole", joinColumns = { @JoinColumn(name = "uid") }, inverseJoinColumns = {
-
@JoinColumn(name = "roleId") })
-
private List<SysRole> roleList;// 一個使用者具有多個角色
-
-
public long getUid() {
-
return uid;
-
}
-
-
public void setUid(long uid) {
-
this.uid = uid;
-
}
-
-
public String getUsername() {
-
return username;
-
}
-
-
public void setUsername(String username) {
-
this.username = username;
-
}
-
-
public String getName() {
-
return name;
-
}
-
-
public void setName(String name) {
-
this.name = name;
-
}
-
-
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 byte getState() {
-
return state;
-
}
-
-
public void setState(byte state) {
-
this.state = state;
-
}
-
-
public List<SysRole> getRoleList() {
-
return roleList;
-
}
-
-
public void setRoleList(List<SysRole> roleList) {
-
this.roleList = roleList;
-
}
-
-
/**
-
* 密碼鹽.
-
*
-
* @return
-
*/
-
public String getCredentialsSalt() {
-
return this.username + this.salt;
-
}
-
-
@Override
-
public String toString() {
-
return "UserInfo [uid=" + uid + ", username=" + username + ", name=" + name + ", password=" + password
-
+ ", salt=" + salt + ", state=" + state + "]";
-
}
-
-
}
在這裡salt主要是用來進行密碼加密的,當然也可以使用明文進行編碼測試,實際開發中還是建議密碼進行加密。
getCredentialsSalt()
這個方法重新對鹽重新進行了定義,使用者名稱+salt,這樣就更加不容易被破解了
SysRole.java
-
package com.example.domain;
-
-
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;
-
-
/**
-
* 系統角色實體類;
-
*
-
* @author Administrator
-
*
-
*/
-
@Entity
-
public class SysRole implements Serializable {
-
private static final long serialVersionUID = 1L;
-
@Id
-
@GeneratedValue
-
private Long id; // 編號
-
private String role; // 角色標識程式中判斷使用,如"admin",這個是唯一的:
-
private String description; // 角色描述,UI介面顯示使用
-
private Boolean available = Boolean.FALSE; // 是否可用,如果不可用將不會新增給使用者
-
-
// 角色 -- 許可權關係:多對多關係;
-
@ManyToMany(fetch = FetchType.EAGER)
-
@JoinTable(name = "SysRolePermission", joinColumns = { @JoinColumn(name = "roleId") }, inverseJoinColumns = {
-
@JoinColumn(name = "permissionId") })
-
private List<SysPermission> permissions;
-
-
// 使用者 - 角色關係定義;
-
@ManyToMany
-
@JoinTable(name = "SysUserRole", joinColumns = { @JoinColumn(name = "roleId") }, inverseJoinColumns = {
-
@JoinColumn(name = "uid") })
-
private List<UserInfo> userInfos;// 一個角色對應多個使用者
-
-
public List<UserInfo> getUserInfos() {
-
return userInfos;
-
}
-
-
public void setUserInfos(List<UserInfo> userInfos) {
-
this.userInfos = userInfos;
-
}
-
-
public Long getId() {
-
return id;
-
}
-
-
public void setId(Long id) {
-
this.id = id;
-
}
-
-
public String getRole() {
-
return role;
-
}
-
-
public void setRole(String role) {
-
this.role = role;
-
}
-
-
public String getDescription() {
-
return description;
-
}
-
-
public void setDescription(String description) {
-
this.description = description;
-
}
-
-
public Boolean getAvailable() {
-
return available;
-
}
-
-
public void setAvailable(Boolean available) {
-
this.available = available;
-
}
-
-
public List<SysPermission> getPermissions() {
-
return permissions;
-
}
-
-
public void setPermissions(List<SysPermission> permissions) {
-
this.permissions = permissions;
-
}
-
-
@Override
-
public String toString() {
-
return "SysRole [id=" + id + ", role=" + role + ", description=" + description + ", available=" + available
-
+ ", permissions=" + permissions + "]";
-
}
-
}
SysPermission.java
-
package com.example.domain;
-
-
import java.io.Serializable;
-
import java.util.List;
-
-
import javax.persistence.Column;
-
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;
-
-
/**
-
* 許可權實體類;
-
*
-
*/
-
@Entity
-
public class SysPermission implements Serializable {
-
private static final long serialVersionUID = 1L;
-
-
@Id
-
@GeneratedValue
-
private long id;// 主鍵.
-
private String name;// 名稱.
-
-
@Column(columnDefinition = "enum('menu','button')")
-
private String resourceType;// 資源型別,[menu|button]
-
private String url;// 資源路徑.
-
private String permission; // 許可權字串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view
-
private Long parentId; // 父編號
-
private String parentIds; // 父編號列表
-
private Boolean available = Boolean.FALSE;
-
-
// @ManyToMany(fetch = FetchType.LAZY)
-
// @JoinTable(name = "SysRolePermission", joinColumns = { @JoinColumn(name = "permissionId") }, inverseJoinColumns = {
-
// @JoinColumn(name = "roleId") })
-
// private List<SysRole> 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 String getResourceType() {
-
return resourceType;
-
}
-
-
public void setResourceType(String resourceType) {
-
this.resourceType = resourceType;
-
}
-
-
public String getUrl() {
-
return url;
-
}
-
-
public void setUrl(String url) {
-
this.url = url;
-
}
-
-
public String getPermission() {
-
return permission;
-
}
-
-
public void setPermission(String permission) {
-
this.permission = permission;
-
}
-
-
public Long getParentId() {
-
return parentId;
-
}
-
-
public void setParentId(Long parentId) {
-
this.parentId = parentId;
-
}
-
-
public String getParentIds() {
-
return parentIds;
-
}
-
-
public void setParentIds(String parentIds) {
-
this.parentIds = parentIds;
-
}
-
-
public Boolean getAvailable() {
-
return available;
-
}
-
-
public void setAvailable(Boolean available) {
-
this.available = available;
-
}
-
-
// public List<SysRole> getRoles() {
-
// return roles;
-
// }
-
//
-
// public void setRoles(List<SysRole> roles) {
-
// this.roles = roles;
-
// }
-
-
@Override
-
public String toString() {
-
return "SysPermission [id=" + id + ", name=" + name + ", resourceType=" + resourceType + ", url=" + url
-
+ ", permission=" + permission + ", parentId=" + parentId + ", parentIds=" + parentIds + ", available="
-
+ available + "]";
-
}
-
-
}
ok,到這裡實體類就編碼完畢了,這時候執行Application,就會自動建表
MySQL> show tables;
+---------------------+
| Tables_in_test |
+---------------------+
| sys_permission |
| sys_role |
| sys_role_permission |
| sys_user_role |
| user_info |
+---------------------+
5 rows in set (0.08 sec)
mysql>
sql
-
INSERT INTO `sys_permission`(name,parent_id,parent_ids,available,permission,resource_type,url)
-
VALUES ('使用者管理',0,'0/' ,1,'userInfo:view', 'menu', 'userInfo/userList');
-
-
INSERT INTO `sys_permission`(name,parent_id,parent_ids,available,permission,resource_type,url)
-
VALUES ('使用者新增',1,'0/1',1,'userInfo:add', 'button', 'userInfo/userAdd');
-
-
INSERT INTO `sys_permission`(name,parent_id,parent_ids,available,permission,resource_type,url)
-
VALUES ('使用者刪除',1,'0/1',1,'userInfo:del', 'button', 'userInfo/userDel');
-
-
INSERT INTO `sys_role`(available,description,role) VALUES (1,'管理員','admin');
-
INSERT INTO `sys_role`(available,description,role) VALUES (1,'VIP會員','vip');
-
-
INSERT INTO `sys_role_permission`(permission_id,role_id) VALUES ('1', '1');
-
INSERT INTO `sys_role_permission`(permission_id,role_id) VALUES ('2', '1');
-
-
INSERT INTO `user_info`(name,password,salt,state,username) VALUES ('管理員', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', '0', 'admin');
-
-
INSERT INTO `sys_user_role`(uid,role_id) VALUES (1,1);
-
INSERT INTO `sys_user_role`(uid,role_id) VALUES (1,2);
這時候資料都準備完畢了,那麼接下來就應該編寫Repository進行訪問資料了
-
package com.example.repository;
-
-
import org.springframework.data.repository.CrudRepository;
-
-
import com.example.domain.UserInfo;
-
-
/**
-
* UserInfo持久化類
-
*
-
* @author Administrator
-
*
-
*/
-
public interface UserInfoRepository extends CrudRepository<UserInfo, Long> {
-
/** 通過username查詢使用者資訊 **/
-
public UserInfo findByUsername(String username);
-
-
}
在這裡你會發現我們只編寫了UserInfo的資料庫操作,那麼我們怎麼獲取我們的許可權資訊了,通過userInfo.getRoleList()可以獲取到對應的角色資訊,然後在通過對應的角色可以獲取到許可權資訊,當然這些都是JPA幫我們實現了,我們也可以進行直接獲取到許可權資訊,只要寫一個關聯查詢然後過濾掉重複的許可權即可,這裡不進行實現。
編寫一個業務處理類UserInfoService>
-
package com.example.service;
-
-
import com.example.domain.UserInfo;
-
-
public interface UserInfoService {
-
-
public UserInfo findByUsername(String username);
-
-
}
-
package com.example.service.impl;
-
-
import javax.annotation.Resource;
-
-
import org.springframework.stereotype.Service;
-
import org.springframework.transaction.annotation.Transactional;
-
-
import com.example.domain.UserInfo;
-
import com.example.repository.UserInfoRepository;
-
import com.example.service.UserInfoService;
-
-
@Service
-
public class UserInfoServiceImpl implements UserInfoService{
-
@Resource
-
private UserInfoRepository userInfoRepository;
-
-
@Transactional(readOnly=true)
-
@Override
-
public UserInfo findByUsername(String username) {
-
System.out.println("UserInfoServiceImpl.findByUsername()");
-
return userInfoRepository.findByUsername(username);
-
}
-
-
}
基本工作準備好之後,剩下的才是重點,shiro的認證最終是交給了Realm進行執行了,所以我們需要自己重新實現一個Realm,此Realm繼承AuthorizingRealm。
-
package com.example.config.shiro;
-
-
import javax.annotation.Resource;
-
-
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.context.annotation.Bean;
-
-
import com.example.domain.SysPermission;
-
import com.example.domain.SysRole;
-
import com.example.domain.UserInfo;
-
import com.example.service.UserInfoService;
-
-
/**
-
* 身份校驗核心類
-
*
-
* @author Administrator
-
*
-
*/
-
public class MyShiroRealm extends AuthorizingRealm {
-
-
@Resource
-
private UserInfoService userInfoService;
-
-
/**
-
* 認證資訊(身份驗證) Authentication 是用來驗證使用者身份
-
*/
-
@Override
-
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
-
System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
-
// 獲取使用者的輸入帳號
-
String username = (String) token.getPrincipal();
-
System.out.println(token.getCredentials());
-
// 通過username從資料庫中查詢 User物件,如果找到,沒找到.
-
// 實際專案中,這裡可以根據實際情況做快取,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重複執行該方法
-
UserInfo userInfo = userInfoService.findByUsername(username);
-
System.out.println("----->>userInfo=" + userInfo);
-
if (userInfo == null) {
-
return null;
-
}
-
-
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userInfo, // 使用者名稱
-
userInfo.getPassword(), // 密碼
-
ByteSource.Util.bytes(userInfo.getCredentialsSalt()), // salt=username+salt
-
getName() // realm name
-
);
-
return authenticationInfo;
-
}
-
-
@Override
-
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
-
// TODO Auto-generated method stub
-
System.out.println("許可權配置-->MyShiroRealm.doGetAuthorizationInfo()");
-
-
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
-
UserInfo userInfo = (UserInfo) principals.getPrimaryPrincipal();
-
-
for(SysRole role:userInfo.getRoleList()){
-
-
authorizationInfo.addRole(role.getRole());
-
System.out.println(role.getPermissions());
-
for(SysPermission p:role.getPermissions()){
-
System.out.println(p);
-
authorizationInfo.addStringPermission(p.getPermission());
-
}
-
}
-
return authorizationInfo;
-
}
-
-
-
}
繼承AuthorizingRealm主要需要實現兩個方法:
doGetAuthenticationInfo();
doGetAuthorizationInfo();
其中doGetAuthenticationInfo主要是用來進行身份認證的,也就是說驗證使用者輸入的賬號和密碼是否正確。
-
SimpleAuthenticationInfoauthenticationInfo =
-
new SimpleAuthenticationInfo(
-
userInfo, //使用者名稱
-
userInfo.getPassword(), //密碼
-
ByteSource.Util.bytes(userInfo.getCredentialsSalt()),//salt=username+salt
-
getName() //realm name
-
);
交給AuthenticatingRealm使用CredentialsMatcher進行密碼匹配,如果覺得人家的不好可以自定義實現
如果你是進行明文進行編碼的話,那麼使用使用如下方式:
-
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
-
userInfo, //使用者名稱
-
userInfo.getPassword(), //密碼
-
getName() //realm name
-
);
至於doGetAuthorizationInfo()是許可權控制,當訪問到頁面的時候,使用了相應的註解或者shiro標籤才會執行此方法否則不會執行,所以如果只是簡單的身份認證沒有許可權的控制的話,那麼這個方法可以不進行實現,直接返回null即可。
在這個方法中主要是使用類:SimpleAuthorizationInfo
進行角色的新增和許可權的新增。
authorizationInfo.addRole(role.getRole());
authorizationInfo.addStringPermission(p.getPermission());
當然也可以新增集合:
authorizationInfo.setRoles(roles);
authorizationInfo.setStringPermissions(stringPermissions);
到這裡我們還需要有一個步驟很重要就是將我們自定義的Realm注入到SecurityManager中。
在ShiroConfiguration.java中新增方法
-
/**
-
* 身份認證realm;
-
*
-
*/
-
@Bean
-
public MyShiroRealm myShiroRealm(){
-
MyShiroRealm myShiroRealm = new MyShiroRealm();
-
return myShiroRealm;
-
}
將myShiroRealm注入到securityManager中:
-
@Bean
-
public SecurityManager securityManager(){
-
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
-
//設定realm.
-
securityManager.setRealm(myShiroRealm());
-
return securityManager;
-
}
到這裡的話身份認證許可權控制基本是完成了,最後我們在編寫一個登入的時候,登入的處理:
在HomeController中新增login post處理:
-
package com.example.controller;
-
-
import java.util.Map;
-
-
import javax.servlet.http.HttpServletRequest;
-
-
import org.apache.shiro.authc.IncorrectCredentialsException;
-
import org.apache.shiro.authc.UnknownAccountException;
-
import org.springframework.stereotype.Controller;
-
import org.springframework.web.bind.annotation.RequestMapping;
-
import org.springframework.web.bind.annotation.RequestMethod;
-
-
@Controller
-
public class HomeController {
-
@RequestMapping({ "/", "index" })
-
public String index() {
-
return "/index";
-
}
-
-
@RequestMapping(value = "/login", method = RequestMethod.GET)
-
public String login() {
-
return "/login";
-
}
-
-
@RequestMapping(value = "/login", method = RequestMethod.POST)
-
public String login(HttpServletRequest request, Map<String, Object> map) {
-
System.out.println("HomeController.login");
-
// 登入失敗從request中獲取shiro處理的異常資訊
-
// shiroLoginFailure:就是shiro異常類的全類名
-
String exception = (String) request.getAttribute("shiroLoginFailure");
-
String msg = "";
-
if (exception != null) {
-
if (UnknownAccountException.class.getName().equals(exception)) {
-
System.out.println("UnknownAccountException -->帳號不存在:");
-
msg = "UnknownAccountException -->帳號不存在:";
-
} else if (IncorrectCredentialsException.class.getName().equals(exception)) {
-
System.out.println("IncorrectCredentialsException -- > 密碼不正確:");
-
msg = "IncorrectCredentialsException -- > 密碼不正確:";
-
} else if ("kaptchaValidateFailed".equals(exception)) {
-
System.out.println("kaptchaValidateFailed -- > 驗證碼錯誤");
-
msg = "kaptchaValidateFailed -- > 驗證碼錯誤";
-
} else {
-
msg = "else >> " + exception;
-
System.out.println("else -- >" + exception);
-
}
-
}
-
map.put("msg", msg);
-
// 此方法不處理登入成功,由shiro進行處理.
-
return "/login";
-
}
-
}
這時候我們啟動應用程式,訪問http://127.0.0.1:8080/index
會自動跳轉到http://127.0.0.1:8080/login 介面,然後輸入賬號和密碼:admin/123456,這時候會提示:IncorrectCredentialsException -- > 密碼不正確。
這主要是因為我們在上面進行了密文的方式,那麼怎麼加密方式,我們並沒有告訴Shiro,所以認證失敗了
在這裡我們需要編寫一個加密演算法類,當然Shiro也已經有了具體的實現HashedCredentialsMatcher
我們只需要進行注入使用即可:
在ShiroConfiguration中加入方法:
-
/**
-
* 憑證匹配器
-
* (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了
-
* 所以我們需要修改下doGetAuthenticationInfo中的程式碼;
-
* )
-
* @return
-
*/
-
@Bean
-
public HashedCredentialsMatcher hashedCredentialsMatcher(){
-
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
-
-
hashedCredentialsMatcher.setHashAlgorithmName("md5");//雜湊演算法:這裡使用MD5演算法;
-
hashedCredentialsMatcher.setHashIterations(2);//雜湊的次數,比如雜湊兩次,相當於 md5(md5(""));
-
-
return hashedCredentialsMatcher;
-
}
在myShiroRealm()方法中注入憑證匹配器:
-
@Bean
-
public MyShiroRealm myShiroRealm(){
-
MyShiroRealm myShiroRealm = new MyShiroRealm();
-
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());;
-
return myShiroRealm;
-
}
這時候在訪問/login進行登入就可以登陸到/index介面了。
(d) 許可權控制
在我們新建一個UserInfoController
-
package com.example.controller;
-
-
import org.apache.shiro.authz.annotation.RequiresPermissions;
-
import org.springframework.stereotype.Controller;
-
import org.springframework.web.bind.annotation.RequestMapping;
-
-
@Controller
-
@RequestMapping("userInfo")
-
public class UserInfoController {
-
/**
-
* 使用者查詢.
-
* @return
-
*/
-
@RequestMapping("/userList")
-
public String userInfo(){
-
return "userInfo";
-
}
-
-
/**
-
* 使用者新增;
-
* @return
-
*/
-
@RequestMapping("/userAdd")
-
public String userInfoAdd(){
-
return "userInfoAdd";
-
}
-
/**
-
* 使用者刪除;
-
* @return
-
*/
-
@RequestMapping("/userDel")
-
@RequiresPermissions("userInfo:del")//許可權管理;
-
public String userDel(){
-
return "userInfoDel";
-
}
-
}
然後執行登入進行訪問:http://127.0.0.1:8080/userInfo/userAdd
並沒有執行doGetAuthorizationInfo()列印資訊,所以我們會發現我們的身份認證是好使了,但是許可權控制好像沒有什麼作用哦。
我們少了幾部分程式碼,
第一就是開啟shiro aop註解支援,這個只需要在ShiroConfiguration加入如下方法進行開啟即可:
-
/**
-
* 開啟shiro aop註解支援.
-
* 使用代理方式;所以需要開啟程式碼支援;
-
* @param securityManager
-
* @return
-
*/
-
@Bean
-
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
-
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
-
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
-
return authorizationAttributeSourceAdvisor;
-
}
第二就是在controller方法中加入相應的註解:
-
package com.example.controller;
-
-
import org.apache.shiro.authz.annotation.RequiresPermissions;
-
import org.springframework.stereotype.Controller;
-
import org.springframework.web.bind.annotation.RequestMapping;
-
-
@Controller
-
@RequestMapping("userInfo")
-
public class UserInfoController {
-
/**
-
* 使用者查詢.
-
* @return
-
*/
-
@RequestMapping("/userList")
-
@RequiresPermissions("userInfo:view")//許可權管理;
-
public String userInfo(){
-
return "userInfo";
-
}
-
-
/**
-
* 使用者新增;
-
* @return
-
*/
-
@RequestMapping("/userAdd")
-
@RequiresPermissions("userInfo:add")//許可權管理;
-
public String userInfoAdd(){
-
return "userInfoAdd";
-
}
-
/**
-
* 使用者刪除;
-
* @return
-
*/
-
@RequestMapping("/userDel")
-
@RequiresPermissions("userInfo:del")//許可權管理;
-
public String userDel(){
-
return "userInfoDel";
-
}
-
}
這時候在訪問http://127.0.0.1:8080/userInfo/userAdd 會看到控制檯列印資訊
-
許可權配置-->MyShiroRealm.doGetAuthorizationInfo()
如果訪問:http://127.0.0.1:8080/userInfo/userDel會看到控制檯列印資訊
-
org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method
-
Whitelabel Error Page
-
-
This application has no explicit mapping for /error, so you are seeing this as a fallback.
-
-
Sun Aug 28 21:36:31 CST 2016
-
There was an unexpected error (type=Internal Server Error, status=500).
-
Subject does not have permission [userInfo:del]
----------------------------------------------
pom.xml
-
<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.example</groupId>
-
<artifactId>spring-boot-shiro1</artifactId>
-
<version>0.0.1-SNAPSHOT</version>
-
<packaging>jar</packaging>
-
-
<properties>
-
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-
</properties>
-
-
-
<parent>
-
<groupId>org.springframework.boot</groupId>
-
<artifactId>spring-boot-starter-parent</artifactId>
-
<version>1.4.0.RELEASE</version>
-
</parent>
-
-
<dependencies>
-
-
<dependency>
-
<groupId>org.springframework.boot</groupId>
-
<artifactId>spring-boot-starter-web</artifactId>
-
</dependency>
-
-
<dependency>
-
<groupId>org.springframework.boot</groupId>
-
<artifactId>spring-boot-starter-thymeleaf</artifactId>
-
</dependency>
-
-
<dependency>
-
<groupId>org.apache.shiro</groupId>
-
<artifactId>shiro-spring</artifactId>
-
<version>1.2.3</version>
-
</dependency>
-
-
<dependency>
-
<groupId>org.springframework.boot</groupId>
-
<artifactId>spring-boot-starter-data-jpa</artifactId>
-
</dependency>
-
-
-
<dependency>
-
<groupId>org.springframework.boot</groupId>
-
<artifactId>spring-boot-devtools</artifactId>
-
<optional>true</optional>
-
</dependency>
-
-
-
<dependency>
-
<groupId>mysql</groupId>
-
<artifactId>mysql-connector-java</artifactId>
-
</dependency>
-
</dependencies>
-
</project>
-
<pre name="code" class="html" style="font-size: 14px; line-height: 28px;">ShiroConfiguration.java
-
package com.example.config.shiro;
-
-
import java.util.LinkedHashMap;
-
import java.util.Map;
-
-
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
-
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 ShiroConfiguration {
-
-
@Bean
-
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
-
System.out.println("ShiroConfiguration.shiroFilter()");
-
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
-
-
// 必須設定SecuritManager
-
shiroFilterFactoryBean.setSecurityManager(securityManager);
-
-
// 攔截器
-
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
-
-
// 配置退出過濾器,其中的具體程式碼Shiro已經替我們實現了
-
filterChainDefinitionMap.put("/logout", "logout");
-
-
// :這是一個坑呢,一不小心程式碼就不好使了;
-
//
-
-
filterChainDefinitionMap.put("/**", "authc");
-
-
// 如果不設定預設會自動尋找Web工程根目錄下的"/login.jsp"頁面
-
shiroFilterFactoryBean.setLoginUrl("/login");
-
// 登入成功後要跳轉的連結
-
shiroFilterFactoryBean.setSuccessUrl("/index");
-
// 未授權介面;
-
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
-
-
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
-
return shiroFilterFactoryBean;
-
-
}
-
-
@Bean
-
public SecurityManager securityManager() {
-
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
-
// 設定realm.
-
securityManager.setRealm(myShiroRealm());
-
return securityManager;
-
}
-
-
/**
-
* 身份認證realm; (這個需要自己寫,賬號密碼校驗;許可權等)
-
*
-
* @return
-
*/
-
@Bean
-
public MyShiroRealm myShiroRealm() {
-
MyShiroRealm myShiroRealm = new MyShiroRealm();
-
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
-
return myShiroRealm;
-
}
-
/**
-
* 憑證匹配器
-
* (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了
-
* 所以我們需要修改下doGetAuthenticationInfo中的程式碼;
-
* )
-
* @return
-
*/
-
@Bean
-
public HashedCredentialsMatcher hashedCredentialsMatcher(){
-
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
-
-
hashedCredentialsMatcher.setHashAlgorithmName("md5");//雜湊演算法:這裡使用MD5演算法;
-
hashedCredentialsMatcher.setHashIterations(2);//雜湊的次數,比如雜湊兩次,相當於 md5(md5(""));
-
-
return hashedCredentialsMatcher;
-
}
-
-
/**
-
* 開啟shiro aop註解支援.
-
* 使用代理方式;所以需要開啟程式碼支援;
-
* @param securityManager
-
* @return
-
*/
-
@Bean
-
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
-
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
-
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
-
return authorizationAttributeSourceAdvisor;
-
}
-
-
}