Spring Boot整合Shiro框架進行身份驗證
一.什麼是Shiro
Apache Shiro 是 Java 的一個安全框架,Shiro 可以幫助我們完成:認證、授權、加密、會話管理等。相比較Spring Security 她更加的小巧易用。其基本功能點如下圖所示:
Authentication:身份認證 / 登錄,驗證用戶是不是擁有相應的身份;
Authorization:授權,即權限驗證,驗證某個已認證的用戶是否擁有某個權限;即判斷用戶是否能做事情,常見的如:驗證某個用戶是否擁有某個角色。或者細粒度的驗證某個用戶對某個資源是否具有某個權限;
Session Manager:會話管理,即用戶登錄後就是一次會話,在沒有退出之前,它的所有信息都在會話中;會話可以是普通 JavaSE 環境的,也可以是如 Web 環境的;
Cryptography:加密,保護數據的安全性,如密碼加密存儲到數據庫,而不是明文存儲;
Web Support:Web 支持,可以非常容易的集成到 Web 環境;
Caching:緩存,比如用戶登錄後,其用戶信息、擁有的角色 / 權限不必每次去查,這樣可以提高效率;
Concurrency:shiro 支持多線程應用的併發驗證,即如在一個線程中開啓另一個線程,能把權限自動傳播過去;
Testing:提供測試支持;
Run As:允許一個用戶假裝爲另一個用戶(如果他們允許)的身份進行訪問;
Remember Me:記住我,這個是非常常見的功能,即一次登錄後,下次再來的話不用登錄了。
Shiro 不會去維護用戶、維護權限;這些需要我們自己去設計和提供;然後通過相應的接口注入給 Shiro。下面我們就來看看在Spring Boot中怎麼進行身份的驗證。
二.創建Spring Boot項目
創建spring boot項目,需要加入的組件有:web,mysql,thymeleaf,shiro,devtools。
下面是創建完項目後pom文件的依賴:
<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.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</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-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
三.創建實體類及建表
進行身份的驗證或者權限的授權,大體思路是把用戶賦予角色,將角色賦予相應的權限。所以我們需要創建用戶類,角色類,和權限類。
1.用戶類
public class UserInfo implements Serializable {
private Integer uid;
private String name;
private String password;
//祕鑰
private String salt;
// 一個用戶具有多個角色
private List<Role> roleList;
//省略get、set方法
}
2.角色類
public class Role {
private Integer id;
// 角色標識程序中判斷使用,如"admin"
private String role;
// 一個角色可以有多個權限
private List<Permission> permissions;
// 一個角色對應多個用戶
private List<UserInfo> userInfos;
//省略get、set方法
}
3.權限類
public class Permission implements Serializable {
private Integer id;
private String name;
// 資源路徑.
private String url;
// 權限字符串如 role:create,role:update,role:delete,role:view
private String permission;
// 一個權限可以包含多個角色
private List<Role> roles;
//省略get、set方法
}
4.用戶表
CREATE TABLE `user_info` (
`uid` int(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`salt` varchar(255) DEFAULT NULL,
PRIMARY KEY (`uid`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
INSERT INTO `user_info` VALUES ('1', 'ADMIN', '5b48c185e886ff93be5244f979b2864b', 'asdfghjkl');
5.角色表
CREATE TABLE `role` (
`id` int(11) NOT NULL,
`role` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
INSERT INTO `role` VALUES ('1', 'admin');
INSERT INTO `role` VALUES ('2', 'vip');
6.權限表
CREATE TABLE `permission` (
`id` int(11) NOT NULL,
`name` varchar(255) CHARACTER SET latin1 DEFAULT NULL,
`permission` varchar(255) CHARACTER SET latin1 DEFAULT NULL,
`url` varchar(255) CHARACTER SET latin1 DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
INSERT INTO `permission` VALUES ('1', 'UserManger', 'userInfo:view', 'userInfo/userList');
INSERT INTO `permission` VALUES ('2', 'UserAdd', 'userInfo:add', 'userInfo/userAdd');
INSERT INTO `permission` VALUES ('3', 'UserDelete', 'userInfo:del', 'userInfo/userDel');
7.用戶角色關聯表
CREATE TABLE `user_role` (
`role_id` int(11) NOT NULL,
`uid` int(11) NOT NULL,
KEY `FKgkmyslkrfeyn9ukmolvek8b8f` (`uid`),
KEY `FKhh52n8vd4ny9ff4x9fb8v65qx` (`role_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
INSERT INTO `user_role` VALUES ('1', '1');
INSERT INTO `user_role` VALUES ('1', '2');
8.角色權限關聯表
CREATE TABLE `role_permission` (
`role_id` int(11) NOT NULL,
`permission_id` int(11) NOT NULL,
KEY `FKomxrs8a388bknvhjokh440waq` (`permission_id`),
KEY `FK9q28ewrhntqeipl1t04kh1be7` (`role_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
INSERT INTO `role_permission` VALUES ('1', '1');
INSERT INTO `role_permission` VALUES ('1', '2');
四.配置application.properties文件
##mapper文件位置
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
mybatis.type-aliases-package=com.example.demo.entity
##配置數據源
spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8
spring.datasource.username = root
spring.datasource.password = 000000
spring.thymeleaf.cache = false
spring.thymeleaf.mode = LEGACYHTML5
五.創建mapper類
UserMapper:
package com.example.demo.mapper;
import com.example.demo.entity.UserInfo;
public interface UserMapper {
public UserInfo getUserByName(String name);
public List<Role> getRoles(int uid);
public List<Permission> getPermissions(int roleId);
}
UserMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.demo.mapper.UserMapper">
<select id="getUserByName" parameterType="com.example.demo.entity.UserInfo" resultType="com.example.demo.entity.UserInfo">
select * from user_info where name = #{name}
</select>
<select id="getRoles" parameterType="java.lang.Integer" resultType="com.example.demo.entity.Role">
select * from role where id in (select role_id from user_role where uid = #{uid})
</select>
<select id="getPermissions" parameterType="java.lang.Integer" resultType="com.example.demo.entity.Permission">
select * from permission where id in(select permission_id from role_permission where role_id = #{roleId})
</select>
</mapper>
這個可以參考上一篇Spring Boot整合Mybatis
六.控制器
package com.example.demo.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;
@Controller
public class UserController {
@RequestMapping({ "/", "/index" })
public String index() {
return "/index";
}
@RequestMapping("/login")
public String login(HttpServletRequest request, Map<String, Object> map) throws Exception {
System.out.println("HomeController.login()");
// 登錄失敗從request中獲取shiro處理的異常信息。
// shiroLoginFailure:就是shiro異常類的全類名.
String exception = (String) request.getAttribute("shiroLoginFailure");
System.out.println("exception=" + exception);
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";
}
@RequestMapping("/test")
public String test() {
System.out.println("------test-------");
return "test";
}
}
在src/main/resources的templates目錄下創建一個index.html和login.html文件
login.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</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>
七.引入Shiro
到目前爲止並沒有Shiro的參與,啓動項目我們現在可以隨便的訪問index.html頁面,現在我們要求訪問index的時候必須要先登錄,如果沒有登錄則跳轉到login.html頁面。下面就該Shiro登場了。
Subject:主體,代表了當前 “用戶”,這個用戶不一定是一個具體的人,與當前應用交互的任何東西都是 Subject,如網絡爬蟲,機器人等;即一個抽象概念;所有 Subject 都綁定到 SecurityManager,與 Subject 的所有交互都會委託給 SecurityManager;可以把 Subject 認爲是一個門面;SecurityManager 纔是實際的執行者;
SecurityManager:安全管理器;即所有與安全有關的操作都會與 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它負責與後邊介紹的其他組件進行交互,如果學習過 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器;
Realm:域,Shiro 從從 Realm 獲取安全數據(如用戶、角色、權限),就是說 SecurityManager 要驗證用戶身份,那麼它需要從 Realm 獲取相應的用戶進行比較以確定用戶身份是否合法;也需要從 Realm 得到用戶相應的角色 / 權限進行驗證用戶是否能進行操作;可以把 Realm 看成 DataSource,即安全數據源。
也就是說對於我們而言,最簡單的一個 Shiro 應用:
應用代碼通過 Subject 來進行認證和授權,而 Subject 又委託給 SecurityManager;
我們需要給 Shiro 的 SecurityManager 注入 Realm,從而讓 SecurityManager 能得到合法的用戶及其權限進行判斷。
理論看完了看看具體項目中應該怎麼做吧:
1.Shiro 配置
Apache Shiro 核心通過 Filter 來實現,就好像SpringMVC 通過DispachServlet 來主控制一樣。 所以我們需要定義一系列關於URL的規則和訪問權限。
package com.example.demo.config;
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;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
System.out.println("ShiroConfiguration.shirFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 攔截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置不會被攔截的鏈接 順序判斷
filterChainDefinitionMap.put("/static/**", "anon");
// 配置退出 過濾器,其中的具體的退出代碼Shiro已經替我們實現了
filterChainDefinitionMap.put("/logout", "logout");
// <!-- 過濾鏈定義,從上向下順序執行,一般將/**放在最爲下邊 -->:這是一個坑呢,一不小心代碼就不好使了;
// <!-- authc:所有url都必須認證通過纔可以訪問; anon:所有url都都可以匿名訪問-->
filterChainDefinitionMap.put("/**", "authc");
// 如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面,設置的話沒經過驗證會發送test請求到控制器,由控制器決定轉到對應視圖
//shiroFilterFactoryBean.setLoginUrl("/test");
shiroFilterFactoryBean.setLoginUrl("/login");
// 登錄成功後要跳轉的鏈接
shiroFilterFactoryBean.setSuccessUrl("/index");
// 未授權界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 憑證匹配器 (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了 )
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");// 散列算法:這裏使用MD5算法;
hashedCredentialsMatcher.setHashIterations(1);// 散列的次數,比如散列兩次,相當於md5(md5(""));
return hashedCredentialsMatcher;
}
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
}
這些配置套路都是一樣的,主要的校驗邏輯是在realm域裏面,所以我們主要的工作就是寫這個域。
看下Shiro 默認提供的 Realm
以後一般繼承 AuthorizingRealm(授權)即可;其繼承了 AuthenticatingRealm(身份驗證),而且也間接繼承了 CachingRealm(帶有緩存實現)。繼承AuthorizingRealm類重寫doGetAuthenticationInfo(驗證)和doGetAuthorizationInfo(授權)兩個方法。
package com.example.demo.config;
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.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import com.example.demo.entity.UserInfo;
import com.example.demo.service.UserService;
public class MyShiroRealm extends AuthorizingRealm {
@Resource
private UserService userService;
/**
* 認證信息.(身份驗證) : Authentication 是用來驗證用戶身份
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
// 獲取用戶的輸入的賬號.
String name = (String) token.getPrincipal();
System.out.println(token.getCredentials());
// 通過name和password從數據庫中查找 User對象,
UserInfo userInfo = userService.getUserByName(name);
if (userInfo == null) {
return null;
}
int uid = userInfo.getUid();
List<Role> roles = userService.getRoles(uid);
int roleId = roles.get(0).getId();
List<Permission> permissions = userService.getPermissions(roleId);
roles.get(0).setPermissions(permissions);
userInfo.setRoleList(roles);
// 加密方式;
// 交給AuthenticatingRealm使用CredentialsMatcher進行密碼匹配
String password = userInfo.getPassword();
// 祕鑰
ByteSource salt = ByteSource.Util.bytes(userInfo.getSalt());
// 當前域的名稱(MyShiroRealm)
String realmName = getName();
// 認證信息
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userInfo, password, salt, realmName);
return authenticationInfo;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
}
}
到此工作已經全部結束了,啓動項目看看效果吧。瀏覽器輸入http://localhost:8080/index,試圖訪問index.html頁面,但是沒有登錄自動跳轉到了登錄頁面。
輸入正確的賬號密碼後就可以訪問到index.html頁面了
附:用戶表中的密碼是進行加過密,Shiro提供一整套的加密方法,這個後面再說。現在提供一個密碼加密的工具類供測試使用:
package com.example.demo.util;
import org.apache.shiro.crypto.hash.Md5Hash;
/**
* 加解密工具類
*
*/
public class CryptographyUtil {
public static String md5(String str, String salt) {
return new Md5Hash(str, salt).toString();
}
public static void main(String[] args) {
String password = "123456";
System.out.println(CryptographyUtil.md5(password, "asdfghjkl"));
}
}
項目的整體結構: