寫在前面: 從2018年底開始學習SpringBoot,也用SpringBoot寫過一些項目。這裏對學習Springboot的一些知識總結記錄一下。如果你也在學習SpringBoot,可以關注我,一起學習,一起進步。
相關文章:
【Springboot系列】Springboot入門到項目實戰
文章目錄
之前寫項目安全控件基本都是用的SpringSecurity,後來發現Shiro在實際開發中用的也挺多的。這裏一起來看一下Shiro在Springboot中的基本用法吧!想了解SpringSecurity的請移步:SpringSecurity安全控件使用指南。
Shiro簡介
1、簡介
Apache Shiro 是 Java 的一個安全(權限)框架。 Shiro 可以非常容易的開發出足夠好的應用,其不僅可以用在 JavaSE 環境,也可以用在 JavaEE 環境。Shiro 可以完成:認證、授權、加密、會話管理、與Web 集成、緩存等。相對於SpringSecurity簡單的多,也沒有SpringSecurity那麼複雜。
2、shiro架構
Shiro三大功能模塊
- Subject:主體,一般指用戶(把操作交給SecurityManager)。
- SecurityManager:安全管理器,管理所有Subject,可以配合內部安全組件(關聯Realm)。
- Realms:用於進行權限信息的驗證,shiro鏈接數據的橋樑。
細分功能
- Authentication:身份認證/登錄,驗證用戶是否擁有相應的身份(賬號密碼驗證)。
- Authorization:授權,驗證某個已認證的用戶是否擁有某權限。
- Session Manager:會話管理,用戶登錄後,用戶信息保存在session會話中。
- Cryptography:加密,保護數據的安全性,如密碼加密存儲到數據庫,而不是明文存儲。
- Web Support:Web支持,集成Web環境。
- Caching:緩存,用戶信息、角色、權限等緩存到如redis等緩存中。
- Concurrency:多線程併發驗證,在一個線程中開啓另一個線程,可以把權限自動傳播過去。
- Testing:測試支持;
- Run As:允許一個用戶假裝爲另一個用戶(如果他們允許)的身份進行訪問。
- Remember Me:記住我,登錄後,下次再來的話不用登錄了。
數據庫設計
1、表關係
菜單(TbMenu)=====> 頁面上需要顯示的所有菜單
角色(SysRole)=====> 角色及角色對應的菜單
用戶(SysUser)=====> 用戶及用戶對應的角色
用戶和角色中間表(sys_user_role)====> 用戶和角色中間表
【注】 權限管理實現原理都差不多,雖然使用的安全控件不一樣,但數據庫表的設計基本一樣,這裏數據庫表的設計和SpringSecurity實現動態權限菜單控制基本一致。
2、數據庫表結構
菜單表tb_menu
角色及菜單權限表sys_role,其中父節點parent 爲null時爲角色,不爲null時爲對應角色的菜單權限。
用戶表sys_user。
用戶和角色多對多關係,用戶和角色中間表sys_user_role(由Spring-Data-Jpa自動生成)。
搭建項目
1、新建Springboot項目
創建一個SprintBoot項目,添加項目需要的依賴(這裏持久層使用的是SpringDataJpa)。
Shiro依賴
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
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 https://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.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.mcy</groupId>
<artifactId>springboot-shiro</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-shiro</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- shiro與spring整合依賴 -->
<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-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、項目結構
3、配置鏈接數據庫屬性
spring.datasource.url=jdbc:mysql://localhost:3306/shiro?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.use-new-id-generator-mappings=false
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
server.port=80
server.servlet.context-path=/shiro
編寫代碼
1、編寫實體類
菜單表實體類TbMenu,Spring-Data-Jpa可以根據實體類去數據庫新建或更新對應的表結構,詳情可以訪問Spring-Data-Jpa入門
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.data.annotation.CreatedBy;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
/**
* 菜單表
* @author
*/
@Entity
public class TbMenu {
private Integer id;
private String name;
private String url;
private Integer idx;
@JsonIgnore
private TbMenu parent;
@JsonIgnore
private List<TbMenu> children = new ArrayList<>();
@Id
@GeneratedValue
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Column(unique = true)
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Integer getIdx() {
return idx;
}
public void setIdx(Integer idx) {
this.idx = idx;
}
@ManyToOne
@CreatedBy
public TbMenu getParent() {
return parent;
}
public void setParent(TbMenu parent) {
this.parent = parent;
}
@OneToMany(cascade = CascadeType.ALL, mappedBy = "parent")
@OrderBy(value = "idx")
public List<TbMenu> getChildren() {
return children;
}
public void setChildren(List<TbMenu> children) {
this.children = children;
}
}
角色及權限表SysRole,parent 爲null時爲角色,不爲null時爲權限。
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.data.annotation.CreatedBy;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
/***
* 角色及角色對應的菜單權限
* @author
*parent 爲null時爲角色,不爲null時爲權限
*/
public class SysRole {
private Integer id;
private String name; //名稱
@JsonIgnore
private SysRole parent;
private Integer idx; //排序
@JsonIgnore
private List<SysRole> children = new ArrayList<>();
@Id
@GeneratedValue
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Column(length = 20)
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@ManyToOne
@CreatedBy
public SysRole getParent() {
return parent;
}
public void setParent(SysRole parent) {
this.parent = parent;
}
@OneToMany(cascade = CascadeType.ALL, mappedBy = "parent")
public List<SysRole> getChildren() {
return children;
}
public void setChildren(List<SysRole> children) {
this.children = children;
}
public Integer getIdx() {
return idx;
}
public void setIdx(Integer idx) {
this.idx = idx;
}
}
最後實現的就是用戶管理了,只需要對添加的用戶分配對應的角色就可以了,用戶登錄時,顯示角色對應的權限。
import com.fasterxml.jackson.annotation.JsonIgnore;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
/**
* 用戶表
*/
@Entity
public class SysUser {
private Integer id;
private String username; //賬號
private String password; //密碼
private String name; //姓名
@JsonIgnore
private List<SysRole> roles = new ArrayList<>(); //角色
@Id
@GeneratedValue
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Column(length = 20, unique = true)
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Column(length = 100)
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Column(length = 20)
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@ManyToMany(cascade = CascadeType.REFRESH, fetch = FetchType.EAGER)
@JoinTable(name = "sys_user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
public List<SysRole> getRoles() {
return roles;
}
public void setRoles(List<SysRole> roles) {
this.roles = roles;
}
}
2、Shiro配置類
Shiro配置類ShiroConfig。
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @description Shiro配置類
*/
@Configuration
public class ShiroConfig {
@Bean("hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//指定加密方式爲MD5
credentialsMatcher.setHashAlgorithmName("MD5");
//加密次數
credentialsMatcher.setHashIterations(1024);
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
@Bean("userRealm")
public UserRealm userRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) {
UserRealm userRealm = new UserRealm();
userRealm.setCredentialsMatcher(matcher);
return userRealm;
}
@Bean
public ShiroFilterFactoryBean shirFilter(@Qualifier("securityManager")DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 設置 SecurityManager,安全管理器
bean.setSecurityManager(securityManager);
// 設置登錄跳轉頁面
bean.setLoginUrl("/index");
// 設置未授權提示頁面(沒有權限訪問後的頁面)
bean.setUnauthorizedUrl("/unAuth");
/**
* Shiro內置過濾器,可以實現攔截器相關的攔截器
* 常用的過濾器:
* anon:無需認證(登錄)可以訪問
* authc:必須認證纔可以訪問
* user:如果使用rememberMe的功能可以直接訪問
* perms:該資源必須得到資源權限纔可以訪問
* role:該資源必須得到角色權限纔可以訪問
**/
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/login","anon");
filterMap.put("/user","authc");
filterMap.put("/system","perms[system]");
filterMap.put("/static/**","anon");
filterMap.put("/**","authc");
filterMap.put("/logout", "logout");
bean.setFilterChainDefinitionMap(filterMap);
return bean;
}
/**
* 注入 securityManager
*/
@Bean(name="securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(HashedCredentialsMatcher hashedCredentialsMatcher) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 關聯realm.
securityManager.setRealm(userRealm(hashedCredentialsMatcher));
return securityManager;
}
}
自定義Realm用於查詢用戶的角色和權限信息並保存到權限管理器中。代碼如下:
import com.mcy.springbootshiro.entity.SysRole;
import com.mcy.springbootshiro.entity.SysUser;
import com.mcy.springbootshiro.service.SysUserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
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.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class UserRealm extends AuthorizingRealm {
@Autowired
private SysUserService userService;
/**
* 授權邏輯方法
**/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("執行授權");
//獲取當前登錄對象
Subject subject = SecurityUtils.getSubject();
SysUser user = (SysUser)subject.getPrincipal();
if(user != null){
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 角色與權限字符串集合
Collection<String> rolesCollection = new HashSet<>();
//獲得當前用戶的角色
List<SysRole> roles = user.getRoles();
for(SysRole role : roles){
rolesCollection.add(role.getName());
}
//添加當前用戶的角色權限,用於判斷可以訪問那些功能
info.addStringPermissions(rolesCollection);
return info;
}
return null;
}
/**
* 認證
**/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("執行認證");
// 編寫shiro判斷邏輯,判斷用戶名和密碼
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
// 判斷用戶名
SysUser bean = userService.findByUsername(token.getUsername());
//用戶名不存在
if(bean == null){
throw new UnknownAccountException();
}
//以賬號作爲加密的鹽值
ByteSource credentialsSalt = ByteSource.Util.bytes(bean.getUsername());
// 判斷密碼,
return new SimpleAuthenticationInfo(bean, bean.getPassword(),
credentialsSalt, getName());
}
}
其中密碼加密使用的是MD5加密,加密代碼如下:
public static void main(String[] args){
String hashAlgorithName = "MD5";
//加密密碼
String password = "123456";
//加密次數
int hashIterations = 1024;
//賬號作爲加密的鹽值
ByteSource credentialsSalt = ByteSource.Util.bytes("admin");
Object obj = new SimpleHash(hashAlgorithName, password, credentialsSalt, hashIterations);
System.out.println(obj);
}
3、編寫控制器
新建IndexController測試控制器。這裏持久層框架使用的是SpringDataJpa,查詢只需遵循查詢方法命名規範即可,所以這裏就直接寫控制器代碼了。想進一步瞭解SpringDataJpa的小夥伴,請移步:Spring-Data-Jpa入門。
IndexController控制器代碼如下:
import com.mcy.springbootshiro.entity.SysRole;
import com.mcy.springbootshiro.entity.SysUser;
import com.mcy.springbootshiro.service.SysRoleService;
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.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class IndexController {
@Autowired
private SysRoleService roleService;
@RequestMapping({"/index", "/"})
public String index(){
return "login";
}
@RequestMapping(value = "/login")
public String login(String username, String password, Model model){
//1.獲取subject
Subject subject = SecurityUtils.getSubject();
//2.封裝用戶數據
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
//3.執行登錄方法
subject.login(token);
//登錄成功
return "redirect:/mian";
} catch (UnknownAccountException e) {
//e.printStackTrace();
//登錄失敗:用戶名不存在
model.addAttribute("msg", "用戶名不存在");
return "login";
} catch (IncorrectCredentialsException e) {
//e.printStackTrace();
//登錄失敗:用戶名不存在
model.addAttribute("msg", "密碼輸入有誤");
return "login";
}
}
@RequestMapping(value = "/mian")
public String main(){
SysUser user = (SysUser) SecurityUtils.getSubject().getPrincipal();
System.out.println(user.getName());
//登錄成功後,輸出對應的角色和菜單
for(SysRole role: user.getRoles()){
System.out.println(role.getName()+"=====角色");
for(SysRole roles : role.getChildren()){
System.out.println(roles.getName()+"===="+role.getName()+"角色對應的菜單");
}
}
return "main";
}
@RequestMapping("/logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
if (subject != null) {
subject.logout();
}
return "redirect:/main";
}
@RequestMapping("/unAuth")
public String unAuth(){
return "unAuth";
}
@RequestMapping("/system")
public String system(){
return "system";
}
@RequestMapping("/user")
public String user(){
return "user";
}
}
4、編寫頁面
頁面內容都比較簡單,主要爲一個登錄頁面,登錄成功後的頁面和幾個訪問頁面。
登錄頁面login.html代碼如下(測試頁面寫的比較簡單,頁面中font標籤爲登錄失敗的提示信息,內容爲後臺傳遞過來的):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登錄頁面</title>
<style type="text/css">
#main{
width: 400px;
margin: 100px auto;
}
input{
border-radius: 5px;
width: 200px;
height: 20px;
}
button{
margin-left: 30px;
width: 80px;
border-radius: 15px;
}
</style>
</head>
<body>
<div id="main">
<form action="login" method="post">
<font color="red" th:text="${msg}"></font><br><br>
賬號:<input type="text" name="username"><br><br>
密碼:<input type="password" name="password"><br><br>
<button type="submit">登錄</button>
</form>
</div>
</body>
</html>
登錄成功main.html頁面(幾個頁面跳轉超鏈接)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<li><a href="user">登錄即可訪問</a></li>
<li><a href="system">管理員權限才能訪問</a></li>
<li><a href="logout">退出登錄</a></li>
</body>
</html>
管理員權限才能訪問的頁面system.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
管理員權限,才能訪問
</body>
</html>
登錄即可訪問的頁面user.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登錄後即可訪問
</body>
</html>
沒有權限訪問的頁面,提示頁面unAuth.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>您暫時沒有權限訪問該頁面</p>
</body>
</html>
測試
1、應用測試
運行項目後訪問http://localhost/shiro/index,進入登錄頁面,登錄擁有管理員權限的用戶。
登錄成功後,可以從編輯器控制檯中看出第一步進行了Shrio的認證。之後執行了登錄成功後跳轉頁面的後臺方法,輸出了對應的角色和角色對應的菜單。
【注】 此案例中前臺菜單沒有進行遍歷,菜單,權限等功能也沒有寫對應的增刪改查。直接在數據庫中添加數據進行測試的。如果想了解菜單遍歷,菜單權限等功能的實現,請訪問SpringSecurity實現動態菜單加載。其中數據庫設計,功能操作都是一樣的。
點擊“管理員權限才能訪問”首先會進行執行授權,根據判斷是否有管理員身份,確定是否可以訪問。因爲是擁有管理員身份的用戶登錄的,所以可以訪問,效果如下:
正是system.html頁面中的內容。如果沒有管理員身份,則顯示效果如下:
2、案例代碼下載
案例代碼下載地址:https://github.com/machaoyin/springboot-shrio
最後有什麼不足之處,歡迎大家指出,期待與你的交流。