SpringBoot+SSM+Shiro+Thymeleaf 實現權限認證
一、SpringBoot+SSM+Shiro+Thymeleaf 實現權限認證
1.1 Apache Shiro 概述
(1)Apache Shiro 是ASF旗下的一款開源軟件(Shiro發音爲“shee-roh”,日語“堡壘(Castle)”的意思),提供的一個強大而靈活的安全框架。
(2)Apache Shiro提供了認證、授權、加密和會話管理功能,將複雜的問題隱藏起來,提供清晰直觀的API使開發者可以很輕鬆地開發自己的程序安全代碼。並且在實現此目標時無須依賴第三方的框架、容器或服務,當然也能做到與這些環境的整合,使其在任何環境下都可拿來使用。
(3)Shiro 將目標集中於Shiro開發團隊所稱的四大安全基石-認證(Authentication)、授權(Authorization)、會話管理(Session Management)和加密(Cryptography):
- 認證(Authentication):用戶身份識別。有時可看作爲“登錄(login)”,它是用戶證明自己是誰的一個行爲。
- 授權(Authorization):訪問控制過程,好比決定“認證(who)”可以訪問“什麼(what)”.
- 會話管理(SessionManagement):管理用戶的會話(sessions),甚至在沒有WEB或EJB容器的環境中。管理用戶與時間相關的狀態。
- 加密(Cryptography):使用加密算法保護數據更加安全,防止數據被偷窺。
1.2 Shiro 重要組件
- Subject:即"用戶",外部應用都是和 Subject 進行交互的,subject 記錄了當前操作用戶,將用戶的概念理解爲當前操作的主體,可能是一個通過瀏覽器請求的用戶,也可能是一個運行的程序。 Subject 在 shiro 中是一個接口,接口中定義了很多認證授權相關的方法,外部程序通過 subject 進行認證授權,而 subject 是通過 SecurityManager 安全管理器進行認證授權(Subject 相當於 SecurityManager 的門面)。
- SecurityManager:即安全管理器,它是 shiro 的核心,負責對所有的 subject 進行安全管理。通過 SecurityManager 可以完成 subject 的認證、授權等。
- Authentication:是一個對用戶進行身份驗證(登錄)的組件。
- Authorization:即授權器,用戶通過認證器認證通過,在訪問功能時需要通過授權器判斷用戶是否有此功能的操作權限。就是用來判斷是否有權限,授權,本質就是訪問控制,控制哪些URL可以訪問.
- Realm:即領域,用於封裝身份認證操作和授權操作,如果用戶身份數據在數據庫那麼 realm 就需要從數據庫獲取用戶身份信息。
在使用 Shiro 之前首先要明確的 Shiro 工作內容,Shiro 只負責對用戶進行身份認證和權限驗證,並不負責權限的管理,也就是說網頁中的按鈕是否顯示、系統中有哪些角色、用戶擁有什麼角色、每個角色對應的權限有哪些,這些都需要我們自己來實現,換句話說 Shiro 只能利用現有的數據進行工作,而不能對數據庫的數據進行修改。
1.3 使用 Shiro 實現認證流程圖
1.4 使用SpringBoot+SSM+Shiro+Thymeleaf 實現身份認證步驟
(1)新建 SpringBoot 項目
新建 SpringBoot 項目,和以前一樣在 src/main 下新建 resources 文件夾,resources 文件夾下新建 static 和 templates 文件夾以及application.properties配置文件。
項目結構如下:
(2)引入項目依賴
<!-- 定義公共資源版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</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-devtools</artifactId>
<optional>true</optional>
<scope>test</scope>
</dependency>
<!-- 對JDBC數據庫的支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--數據庫相關 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--mybatis的支持 包含mybatis的包和mybatis-spring插件包 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<!--c3p0的依賴 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
<!--shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- 模板引擎 Thymeleaf 依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- 沒有該配置,devtools 不生效 -->
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>
(3)在application.properties文件中配置各項信息
#配置項目訪問根路徑
#server.servlet.context-path=/spring-boot-demo
#配置tomcat監聽的端口,如果設置爲80端口,在瀏覽器訪問服務器時可以省略端口號
server.port=80
#spring相關配置
#靜態資源映射
spring.resources.static-locations=classpath:/static/, classpath:/templates/
#數據源配置
#jdbc
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/ebuy?useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=
#c3p0連接池
spring.datasource.type=com.mchange.v2.c3p0.ComboPooledDataSource
#mybatis配置
mybatis.mapperLocations=classpath:/mapper/*Mapper.xml
mybatis.type-aliases-package=com.woniu.entity
#thymeleaf配置
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
(4)使用 mybatis 反向工程生成相關類和映射文件,定義業務層,在業務層中定義方法,業務方法的目的是通過用戶名查詢用戶信息和用戶角色信息和角色權限信息,角色信息和權限信息是爲了在授權操作時使用,便於操作我們可以在User類中封裝Role類型集合,Role類中封裝 Permission類的集合,然後使用 mybatis 的一對多關聯映射進行查詢。
業務層主要代碼:
@Service
public class UserServiceImp implements UserService{
@Resources
private UserMapper userMapper;
public User selectUserInfo(String username) throws Exception{
return userMapper.selectUserInfo(username);
}
}
數據層接口方法:
User selectUserInfo(String username);
映射文件查詢標籤:
<select id="selectUserInfo" parameterType="string resultMap="userInfoMap">
select u.*,r.rid,r.name rname,p.pid,p.resource from `user` u
inner join userRole ur on u.uid = uid
inner join role r on ur.rid = r.rid
inner join rp on ur.rid = rp.rid
inner join permission p on rp.rid = p.pid
where u.username = #{username};
</select>
映射文件中的自定義結果集:
<resultMap id="BaseResultMap" type="com.store.entity.Users">
<id column="uid" jdbcType="INTEGER" property="uid" />
<result column="username" jdbcType="VARCHAR" property="username" />
<result column="password" jdbcType="VARCHAR" property="password" />
<result column="phone" jdbcType="VARCHAR" property="phone" />
<result column="sex" jdbcType="INTEGER" property="sex" />
<result column="age" jdbcType="INTEGER" property="age" />
<result column="token" jdbcType="VARCHAR" property="token" />
<collection property="roles" ofType="com.store.entity.Roles">
<id column="rid" jdbcType="INTEGER" property="rid" />
<result column="rname" jdbcType="VARCHAR" property="rname" />
<collection property="permissions" ofType="com.store.entity.Permission">
<id column="pid" jdbcType="INTEGER" property="pid" />
<result column="resource" jdbcType="VARCHAR" property="resource" />
</collection>
</collection>
</resultMap>
(5)新建 Realm 類封裝認證操作和授權操作,主要代碼如下:
- 繼承 AuthorizingRealm 類,重寫方法
- 重新的兩個方法,doGetAuthenticationInfo方法用於身份認證,doGetAuthorizationInfo方法用於進行授權操作。
- 在身份認證方法中,從參數token中取出用戶名調用業務層的方法獲取用戶對象,如果用戶存在則返回用戶認證信息,交給Shiro去進行密碼比對,如果用戶不存在則直接返回null,Shiro將會拋出用戶名不存在的異常,查詢到用戶信息可以將用戶信息保存到Session中。
public class UsersRealm extends AuthorizingRealm{
@Resource
private LoginAndRegistService lars;
//授權方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
//認證方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
try {
Users users = lars.selectByUserName((String)token.getPrincipal());
if(users != null) {
//如果用戶對象不爲空,則封裝爲一個AuthenticationInfo對象並返回
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(users, users.getPassword(), getName());
return info;
}
} catch (Exception e) {
e.printStackTrace();
throw new AuthenticationException("服務器異常");
}
return null;
}
}
大家這裏可能會有個疑惑,UserRealm 類上爲什麼沒有添加@Component
註解,按照 Spring 的規則只有將對象交給Spring容器來管理,Spring纔會完成依賴注入,這裏不添加@Component
註解,那注入能成功嗎?事實上我們會在另外一個位置用另外一種方式將UserRealm對象交給Spring容器來管理。咱們接着往下看。
(6)初始化 Shiro,初始化 Shiro 的 Realm、SecurityManager、和過濾器工廠對象。
- Realm:我們自定義的 UserReaml,該對象在此處進行初始化並交給Spring容器來管理
- SecurityManager:安全管理器,用於管理所有的認證信息。
- ShiroFilterFactoryBean:過濾器工廠對象,Shiro依賴過濾器多層的過濾器來實現身份認證判斷,權限校驗等,通過ShiroFilterFactoryBean可以規定過濾器的執行順序和地址匹配規則。
代碼:
import java.util.LinkedHashMap;
import java.util.Map;
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 {
// 初始化Realm
@Bean
public UserRealm initRealm() {
UserRealm realm = new UserRealm();
return realm;
}
// 加載安全管理器
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(initRealm());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//註冊securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 攔截器+配置登錄和登錄成功之後的url
//LinkHashMap是有序的,shiro會根據添加的順序進行攔截,匹配到過濾器後就執行該過濾器不會在繼續向下查找過濾器
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//配置不會被攔截地址規則
//anon:所有的url都可以不登陸的情況下訪問
//authc:所有url都必須認證通過纔可以訪問
filterChainDefinitionMap.put("/css/**","anon");
filterChainDefinitionMap.put("/js/**","anon");
filterChainDefinitionMap.put("/fonts/**","anon");
filterChainDefinitionMap.put("/images/**","anon");
filterChainDefinitionMap.put("/login.html", "anon");
filterChainDefinitionMap.put("/login", "anon");
//如果不滿足上方所有的規則 則需要進行登錄驗證
filterChainDefinitionMap.put("/**", "authc");
//未登錄時重定向的網頁地址
shiroFilterFactoryBean.setLoginUrl("/login.html");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//返回
return shiroFilterFactoryBean;
}
}
核心代碼講解:
@Configuration
public class ShiroConfig {
SpringBoot 提供的註解,添加了該註解的類將作爲 Spring 的配置器來使用,該類中所有添加了@Bean註解的方法所返回的數據都會交給 Spring 容器來管理。
// 初始化Realm
@Bean
public UserRealm initRealm() {
UserRealm realm = new UserRealm();
return realm;
}
初始化Realm,該對象需要交給SecurityManager來管理,SecurityManager需要使用其中定義的認證和授權方法。
// 加載安全管理器
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(initRealm());
return securityManager;
}
初始化 SecurityManager,將Realm交給它來管理。注意由於在java.lang包下也有一個SecurityManager類,所以我們使用時必須手動導包:import org.apache.shiro.mgt.SecurityManager;
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//註冊securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 攔截器+配置登錄和登錄成功之後的url
//LinkHashMap是有序的,shiro會根據添加的順序進行攔截,匹配到過濾器後就執行該過濾器不會在繼續向下查找過濾器
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//配置不會被攔截地址規則
//anon:所有的url都可以不登陸的情況下訪問
//authc:所有url都必須認證通過纔可以訪問
filterChainDefinitionMap.put("/css/**","anon");
filterChainDefinitionMap.put("/js/**","anon");
filterChainDefinitionMap.put("/fonts/**","anon");
filterChainDefinitionMap.put("/images/**","anon");
filterChainDefinitionMap.put("/login.html", "anon");
filterChainDefinitionMap.put("/login", "anon");
//如果不滿足上方所有的規則 則需要進行登錄驗證
filterChainDefinitionMap.put("/**", "authc");
//未登錄時重定向的網頁地址
shiroFilterFactoryBean.setLoginUrl("/login.html");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//返回
return shiroFilterFactoryBean;
}
初始化過濾器工廠,由於 SecurityManager 中封裝了 Realm,Realm中又封裝了認證方法,在過濾器中要調用認證方法,所以講 SecurityManager 註冊到工廠類中。
同時定義一個 LinkedHashMap,LinkedHashMap 是一個有序鍵值對,通過該鍵值對來保存過濾器匹配規則,anon是匿名過濾器,用來對一些請求放行,例如靜態資源,登錄頁面,登錄請求,註冊頁面,註冊請求。authc過濾器必須在登錄時才能訪問,如果沒有登錄(SecurityManager中沒有用戶的認證信息),則會重定向到指定頁面,要注意authc過濾器一定要寫在最後,不然會導致靜態資源無法訪問,shiroFilterFactoryBean.setLoginUrl("/login.html");
用來配置未登錄時的跳轉頁面。
(7)新建控制層 UserController,定義方法處理登錄請求。
- 封裝
UsernamePasswordToken
對象,該對象用於保存用戶登錄的用戶名和密碼 - 使用
SecurityUtils.getSubject()
獲取主體對象,只有在登錄成功後纔會在Shiro中保存用戶主體(登錄成功後在Shiro中保存的用戶信息) - 判斷主體是否爲空,如果不爲空說明已經登錄過,就不需要執行登錄認證,直接轉發到首頁。
- 判斷主體爲空,說明當前還未有用戶登錄,就需要執行登錄認證。在執行登錄認證的過程中,Shiro會將登錄結果以異常的方式拋出,用戶名不存在和密碼錯誤都會拋出異常,如果登陸成功將不會拋出異常,我們在控制層中直接返回登錄成功後的邏輯視圖即可,那麼登錄失敗的響應在何處進行呢?既然是拋出了異常,那麼我們完全可以定義一個全局的異常處理器,針對異常種類進行判斷,然後來確定響應的邏輯視圖信息。
控制層主要代碼:
@Controller
public class UsersController {
@RequestMapping("login")
public ModelAndView login(String username, String password) throws Exception{
ModelAndView mav = new ModelAndView();
mav.setViewName("index");
mav.addObject("username", username);
//封裝用戶名和密碼
UsernamePasswordToken token = new UsernamePasswordToken(String username, String password);
//創建subject 實例
Subject subject = SecurityUtils.getSubject();
//判斷當前的subject是否登錄
if(subject.isAuthenticated() == false){
//執行shiro的登錄驗證,最終會執行到Realm的認證方法
subjcet.login(token);
}
return mav;
}
}
全局異常處理器代碼:
@Component
public class GolbalExceptionResolver implements HandlerExceptionResolver{
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ModelAndView mav = new ModelAndView();
ex.printStackTrace();
if(ex instanceof UnknownAccountException) {
mav.setViewName("login");
mav.addObject("message", "該用戶名不存在,請重新輸入");
}else if(ex instanceof IncorrectCredentialsException) {
mav.setViewName("login");
mav.addObject("message", "用戶名或密碼錯誤,請重新輸入");
}else if(ex instanceof UnauthorizedException) {
mav.setViewName("error");
mav.addObject("message", "沒有訪問權限?點擊開通VIP立即獲取>>");
}
else {
mav.setViewName("error");
mav.addObject("message", "條子來了");
}
return mav;
}
}
(8)編寫首頁和登錄網頁,測試登錄認證。
1.5 添加權限驗證模塊步驟
(1)將用戶擁有的角色名稱和權限資源名稱添加到Shiro的用戶權限信息中,在UserRealm的doGetAuthorizationInfo方法中,取出登錄時保存的用戶信息,循環將角色和權限一起添加到Shiro的權限信息中。
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 獲取登錄用戶
User user = (User) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
System.out.println("進入權限認證");
try { // 查詢用戶名稱
for (Role role : user.getRoles()) {
// 添加角色
simpleAuthorizationInfo.addRole(role.getName());
for (Permission permission : role.getPermissions()) {
// 添加權限
simpleAuthorizationInfo.addStringPermission(permission.getResource());
}
}
} catch (Exception e) {
e.printStackTrace();
}
// 添加角色和權限
return simpleAuthorizationInfo;
}
每次用戶發送請求時,Shiro都會執行該方法來獲取用戶的權限,進行判定。
(2)在ShiroConfig類中,添加兩個Bean,用於掃描Shiro的註解和使用SpringAOP來完成權限校驗。如果不添加這兩個Bean,Shiro將無法使用註解的方式完成權限驗證。
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager());
return advisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator app=new DefaultAdvisorAutoProxyCreator();
app.setProxyTargetClass(true);
return app;
}
(3)在控制層的方法上添加@RequiresPermissions(“insert”)註解,表示要執行該方法必須擁有insert權限。insert對應的是數據庫中新增用戶操作的resource值。只所以不使用權限名稱進行判斷,是因爲Shiro的編碼方法不支持中文。而且resource和請求地址是一致的也利於記憶和書寫。
@RequestMapping("insert")
@RequiresPermissions("insert")
public ModelAndView insert(User user){
return new ModelAndView("success");
}
(4)當用戶發送 insert 請求時,基於 SpringAOP 的前置通知,會去判斷當前用戶的權限信息中是否擁有 insert 權限,如果擁有則執行方法,如果沒有該權限,則會拋出UnauthorizedException
異常,爲了捕獲這種異常並響應到權限不足的提示頁面,需要在全局的異常處理器中,對該類異常進行處理。
}else id(ex instanceof UnauthorizedException){
mv.setViewName("unpermission")
}
需要自定義unpermission.html
頁面,提示權限不足的信息。
(5)因爲我們在控制層,添加了禁止重複登陸的判斷,所以當我們使用另外一個用戶登錄時,事實上並不會重新進行登錄,我們會發現Shiro認證的信息仍然是第一次登錄的用戶信息,所以我們需要爲項目提供登出的功能。只需要在ShiroConfig中配置一個登出過濾器,將/logout請求交給Shiro的登出過濾器來處理,在登出過濾器中Shiro會將已經認證的信息刪除,並自動重定向到登錄頁面。
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/***", "authc");
//未登錄時和登出時重定向的網頁網址
shiroFilterFactoryBean.setLoginUrl("login.html");