記錄一下使用SpringBoot集成Shiro框架實現前后端分離Web項目的過程,后端使用SpringBoot整合Shiro,前端使用vue+elementUI,達到前后端使用token來進行交互的應用,這種方式通常叫做無狀態(tài),后端只需要使用Shiro框架根據前端傳來的token信息授權訪問相應資源。
案例源碼:SpringBoot+Shiro框架整合實現前后端分離的權限管理基礎Demo
首先新建SpringBoot項目,導入Springboot整合shiro所需要的依賴包
<!-- SpringBoot整合shiro所需相關依賴-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.10.0</version>
</dependency>
<!--web模塊的啟動器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
使用的SpringBoot版本
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
使用SpringBoot集合Shiro之前,需要建立相應的類和從數據庫獲取的用戶數據(這里新建一個java靜態(tài)類來模擬解決)
用戶登錄的類UserValidate.java
package boot.example.shiro.domain;
/**
* 螞蟻舞
*/
public class UserValidate {
String username;
String password;
? ? // get set
}
用戶類SysUsers.java
package boot.example.shiro.domain;
/**
* 螞蟻舞
*/
public class SysUsers {
private Integer user_id;
private String username;
private String password;
private int user_type; // 用戶類型 -1表示超級賬號 1表示普通賬號
private Integer role_id; // 用戶角色 拿權限需要的
private Integer locked; // 用戶狀態(tài) 1-正常 2=鎖定
public SysUsers() {
}
public SysUsers(Integer user_id, String username, String password, int user_type, Integer role_id, Integer locked) {
this.user_id = user_id;
this.username = username;
this.password = password;
this.user_type = user_type;
this.role_id = role_id;
this.locked = locked;
}
? ? // get set
}
模擬三個用戶shiro_admin, myw_admin, app_admin以及相關的方法和靜態(tài)mock數據
模擬數據庫的類ShiroDataMapper.java
package boot.example.shiro.config;
import boot.example.shiro.domain.SysUsers;
import java.util.ArrayList;
import java.util.List;
/**
* 螞蟻舞
*/
public class ShiroDataMapper {
private static final String shiro_admin = "shiro_admin";
private static final String myw_admin = "myw_admin";
private static final String app_admin = "app_admin";
private static final SysUsers sysUsers_shiro_admin = new SysUsers(1, shiro_admin, "123", -1, 1, 1);
private static final SysUsers sysUsers_myw_admin = new SysUsers(2, myw_admin, "1234", 1, 2, 1);
private static final SysUsers sysUsers_app_admin = new SysUsers(3, app_admin, "12345",3, 3, 1);
public static SysUsers getSysUsersByUserName(String username){
if(username.equalsIgnoreCase(shiro_admin)){
return sysUsers_shiro_admin;
}
if(username.equalsIgnoreCase(myw_admin)){
return sysUsers_myw_admin;
}
if(username.equalsIgnoreCase(app_admin)){
return sysUsers_app_admin;
}
return null;
}
public static List<String> listSysRolesPermissions(Integer roleId){
if(roleId == 2){
List<String> list = new ArrayList<>();
list.add("sys:user:list");
list.add("sys:user:update");
list.add("sys:user:add");
list.add("sys:user:delete");
return list;
}
if(roleId == 3){
List<String> list = new ArrayList<>();
list.add("sys:user:list");
return list;
}
return null;
}
}
getSysUsersByUserName方法是用來模擬從數據庫獲取用戶對象數據的,listSysRolesPermissions是根據用戶的角色來獲取對應的權限列表的。
Shiro框架的ShiroRealm.java
shiro的realm主要用來實現認證(AuthenticationInfo)和授權(AuthorizationInfo)
package boot.example.shiro.config;
import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class ShiroRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
? ? ? ? // to do
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
? ? ? ? // to do
}
}
認證的實現,當用戶通過接口登錄后就會觸發(fā)這里的認證登錄
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 獲取登錄username
String username = (String)token.getPrincipal();
// 從數據庫獲取用戶對象 (這里模擬的)
SysUsers sysUsers = ShiroDataMapper.getSysUsersByUserName(username);
// 在數據庫里沒找到用戶,異常用戶,拋出異常(交給異常處理)
if(sysUsers == null) {
throw new UnknownAccountException(); //沒找到帳號
}
// 一般用戶允不允許登錄也是有一個鎖定狀態(tài)的 從用戶對象里拿到鎖定狀態(tài),判斷是否鎖定
if(2 == sysUsers.getLocked()) {
throw new LockedAccountException(); //帳號鎖定
}
// 交給SimpleAuthenticationInfo去驗證密碼
return new SimpleAuthenticationInfo(sysUsers, sysUsers.getPassword(), this.getClass().getName());
}
授權實現,給超級管理所有權限,給具體的普通用戶對應的權限
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 獲取用戶對象
SysUsers user = (SysUsers)principals.getPrimaryPrincipal();
// 對象為null 拋出異常
if(user == null){
throw new UnknownAccountException();
}
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
SysUsers sysUsers = ShiroDataMapper.getSysUsersByUserName(user.getUsername());
if(sysUsers == null){
throw new UnknownAccountException();
}
// // 用戶類型 -1表示超級賬號 1表示普通賬號
if(sysUsers.getUser_type() < 0){
authorizationInfo.addRole("*"); // roles的權限 所有
authorizationInfo.addStringPermission("*:*:*"); // perms的權限 所有
} else {
// 用角色id從數據庫獲取權限列表,這里是模擬的
List<String> mapList = ShiroDataMapper.listSysRolesPermissions(sysUsers.getRole_id());
authorizationInfo.addRole("key");
if (!mapList.isEmpty()) {
Set<String> permsSet = new HashSet<>();
for (String perm : mapList) {
permsSet.addAll(Arrays.asList(perm.trim().split(",")));
}
authorizationInfo.setStringPermissions(permsSet);
}
}
return authorizationInfo;
}
ShiroRealm.java完整代碼
package boot.example.shiro.config;
import boot.example.shiro.domain.SysUsers;
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.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 螞蟻舞
*/
public class ShiroRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 獲取用戶對象
SysUsers user = (SysUsers)principals.getPrimaryPrincipal();
// 對象為null 拋出異常
if(user == null){
throw new UnknownAccountException();
}
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
SysUsers sysUsers = ShiroDataMapper.getSysUsersByUserName(user.getUsername());
if(sysUsers == null){
throw new UnknownAccountException();
}
// // 用戶類型 -1表示超級賬號 1表示普通賬號
if(sysUsers.getUser_type() < 0){
authorizationInfo.addRole("*"); // roles的權限 所有
authorizationInfo.addStringPermission("*:*:*"); // perms的權限 所有
} else {
// 用角色id從數據庫獲取權限列表,這里是模擬的
List<String> mapList = ShiroDataMapper.listSysRolesPermissions(sysUsers.getRole_id());
authorizationInfo.addRole("key");
if (!mapList.isEmpty()) {
Set<String> permsSet = new HashSet<>();
for (String perm : mapList) {
permsSet.addAll(Arrays.asList(perm.trim().split(",")));
}
authorizationInfo.setStringPermissions(permsSet);
}
}
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 獲取登錄username
String username = (String)token.getPrincipal();
// 從數據庫獲取用戶對象 (這里模擬的)
SysUsers sysUsers = ShiroDataMapper.getSysUsersByUserName(username);
// 在數據庫里沒找到用戶,異常用戶,拋出異常(交給異常處理)
if(sysUsers == null) {
throw new UnknownAccountException(); //沒找到帳號
}
// 一般用戶允不允許登錄也是有一個鎖定狀態(tài)的 從用戶對象里拿到鎖定狀態(tài),判斷是否鎖定
if(2 == sysUsers.getLocked()) {
throw new LockedAccountException(); //帳號鎖定
}
// 交給SimpleAuthenticationInfo去驗證密碼
return new SimpleAuthenticationInfo(sysUsers, sysUsers.getPassword(), this.getClass().getName());
}
}
Shiro框架的ShiroConfig.java
SpringBoot集成Shiro有一個最主要的配置類,這個類里有Shiro框架的會話管理(SessionManager)和安全管理(SecurityManager)和訪問過濾器(ShiroFilterFactoryBean)和SpringBoot注解支持和生命周期相關的Bean配置
@Configuration必須加上的!
@Configuration
public class ShiroConfig {
}
ShiroConfig里首先來配置密碼校驗的bean
// 密碼校驗bean
@Bean("credentialMatcher")
public ShiroCredentialMatcher credentialMatcher() {
return new ShiroCredentialMatcher();
}
密碼校驗繼承類ShiroCredentialMatcher.java
這里繼承了SimpleCredentialsMatcher 實現方式是將登錄的密碼和數據庫查詢出來的密碼進行一個equals對比,使用這種方式,密碼可以是明碼進行對比,也可以MD5后的密碼,同樣的登錄密碼和數據庫內的密碼也可以在這里分別經過各自某種加密解密后在對比(安全系數瞬間增強,即使從數據庫拿到了密碼也沒法簡單確認出登錄密碼)
package boot.example.shiro.config;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
/**
* 螞蟻舞
*/
public class ShiroCredentialMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String password = new String(usernamePasswordToken.getPassword());
String dbPassword = (String) info.getCredentials();
System.out.println("usernamePasswordToken--"+usernamePasswordToken.getUsername()+"--"+password);
System.out.println("info.getCredentials()-"+info.getCredentials()+"---info.getPrincipals()-"+info.getPrincipals());
// 密碼比對
return this.equals(password, dbPassword);
}
}
SimpleCredentialsMatcher的源碼

身份認證和權限校驗Realm的bean
ShiroRealm就是授權和認證的類,設置的緩存管理使用的是內存,setCredentialsMatcher就是密碼校驗,MemoryConstrainedCacheManager緩存在內存中(方便快捷)
// 身份認證和權限校驗Realm
@Bean("shiroRealm")
public ShiroRealm shiroRealm(@Qualifier("credentialMatcher") ShiroCredentialMatcher matcher){
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCacheManager(new MemoryConstrainedCacheManager());
shiroRealm.setCredentialsMatcher(matcher);
return shiroRealm;
}
SessionManager會話管理
shiro的會話管理SessionManager是用來管理應用中所有 Subject 的會話的創(chuàng)建、維護、刪除、失效、驗證,有三個默認的實現類
DefaultSessionManager
DefaultWebSessionManager:用于web環(huán)境的實現
ServletContainerSessionManager
shiro默認的會話管理是依賴于瀏覽器的cookie來維持的,也就是說前端代碼嵌入到了SpringBoot整合Shiro的環(huán)境中,Shiro的會話管理將sesionId 放到 cookie中,現在大多數項目都是前后端分離的,去拿cookie還不如用token機制,一種無狀態(tài)的機制,在登錄的時候獲取的token實際上就是shiro的sessionId,如此的話,那么可以繼承實現DefaultWebSessionManager類,修改一些需要改變的方法
// 會話管理, 管理用戶登錄后的會話
@Bean("sessionManager")
public ShiroSessionManager sessionManager(){
//將繼承后重寫的ShiroSessionManager加入bean
return new ShiroSessionManager();
}
token的靜態(tài)類ShiroConstant.java
package boot.example.shiro.config;
/**
* 螞蟻舞
*/
public class ShiroConstant {
// 定義的請求頭中使用的標記key,用來傳遞 token
public static final String authorization_token = "token";
}
重寫會話管理類ShiroSessionManager.java
package boot.example.shiro.config;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
/**
* 螞蟻舞
*/
public class ShiroSessionManager extends DefaultWebSessionManager {
public ShiroSessionManager() {
super();
//在這里設置ShiroSession失效時間
setGlobalSessionTimeout(MILLIS_PER_MINUTE * 15);
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
//獲取請求頭中的token值,如果請求頭中有token值,則取巧認為其值為會話的sessionId(那么用戶在登陸的時候需要給前端傳送這個sessionId)
String sessionId = WebUtils.toHttp(request).getHeader(ShiroConstant.authorization_token);
System.out.println("sessionId--" + sessionId);
if (StringUtils.isEmpty(sessionId)){
/**
* 注意: 在這里有一種特殊情況,那就是不經過shiroFilter過濾器的訪問,例如authc認證用戶
* 既然不經過shiroFilter 那么當后端重啟清空了會話,可前端依舊把sessionId傳給了后端,
* 出現這種情況,shiro會按照shiroFilterFactoryBean.setLoginUrl("/shiro-redirect/index");設置跳轉到登錄頁面,重新登陸
* 格式是http://127.0.0.1:20400/shiro-redirect/index;JSESSIONID=04d5ed45-85c1-420b-b7bd-fa622385309f
* 如果是沒有分離的項目,那么直接跳轉到了登錄頁,如果是分離的項目,那就會給前端報出400的錯誤(這里是整合需要注意的關鍵點)
*/
//如果沒有攜帶sessionId的參數,直接按照父類的方式在cookie進行獲取sessionId
return super.getSessionId(request, response);
} else {
//請求頭中如果有token, 則其值為sessionId(登陸的時候就傳送這個sessionId)
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "request cookie");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId); // 這里加上sessionId
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return sessionId;
}
}
}
看看DefaultWebSessionManager父類getSessionId的源碼

調用了私有getReferencedSessionId方法
先調用this.getSessionIdCookieValue(request, response)獲取sessionId 如果sessionid不存在,則去判斷JSESSIONID的參數是不是帶有(這個在前后端分離的項目有個大坑,不經過shiroFilter里的訪問,接口會報出400錯誤,ShiroSessionManager的demo代碼里有說明),暫時不去分析那么多,前后端分離一般也不會用到類似authc認證用戶訪問的,一般都是接口訪問,有shiroFilter過濾器。
private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
String id = this.getSessionIdCookieValue(request, response);
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "cookie");
} else {
id = this.getUriPathSegmentParamValue(request, "JSESSIONID");
if (id == null && request instanceof HttpServletRequest) {
String name = this.getSessionIdName();
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
String queryString = httpServletRequest.getQueryString();
if (queryString != null && queryString.contains(name)) {
id = request.getParameter(name);
}
if (id == null && queryString != null && queryString.contains(name.toLowerCase())) {
id = request.getParameter(name.toLowerCase());
}
}
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "url");
}
}
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
}
request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, this.isSessionIdUrlRewritingEnabled());
return id;
}
SecurityManager安全管理器 Shiro框架的核心組件
// 安全管理器
@Bean("securityManager")
public SecurityManager securityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm) {
// web的安全管理器
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 設置授權和認證
manager.setRealm(shiroRealm);
// 設置會話管理
manager.setSessionManager(sessionManager());
return manager;
}
ShiroFilterFactoryBean訪問過濾器(經常說成是攔截器,實際上是攔截的功能)
// 訪問shiro的過濾器
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("shiroFilter", new ShiroFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// 跳轉到登錄頁,實際跳轉后訪問的是接口,接口返回請登錄的信息
shiroFilterFactoryBean.setLoginUrl("/shiro-redirect/index");
//bean.setSuccessUrl("/shiro-redirect/index");
// 實際跳轉到未認證頁面,請重新登陸
shiroFilterFactoryBean.setUnauthorizedUrl("/shiro-redirect/unauthorized");
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 靜態(tài)路徑放開 anon:匿名用戶可訪問
filterChainDefinitionMap.put("/public/**", "anon");
filterChainDefinitionMap.put("/static/**", "anon");
// 調試工具全部放開 anon:匿名用戶可訪問
filterChainDefinitionMap.put("/swagger-resources", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
filterChainDefinitionMap.put("/v2/api-docs", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/doc.html", "anon");
// 登錄相關全部放開 anon:匿名用戶可訪問
filterChainDefinitionMap.put("/shiro-login/**", "anon");
filterChainDefinitionMap.put("/shiro-redirect/**", "anon");
// 匿名用戶可訪問 anon:匿名用戶可訪問
filterChainDefinitionMap.put("/shiro-anon/**", "anon");
// 認證用戶可訪問 authc:認證用戶可訪問
filterChainDefinitionMap.put("/shiro-authc/*", "authc");
// 自定義過濾器過濾的內容
filterChainDefinitionMap.put("/**", "shiroFilter");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
自定義過濾的類ShiroFilter.java
package boot.example.shiro.config;
import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 螞蟻舞
*/
public class ShiroFilter extends BasicHttpAuthenticationFilter {
// sendChallenge重寫的目的是避免前端在沒有登錄的情況下訪問@RequiresPermissions()等未授權接口返回401錯誤,
// 給前端調用接口一個數據,讓前端去重新登陸
// 如果使用瀏覽器訪問,瀏覽器會彈出一個輸入賬號密碼的彈框,重寫后瀏覽器訪問出現接口數據
protected boolean sendChallenge(ServletRequest request, ServletResponse response) {
System.out.println("Authentication required: sending 401 Authentication challenge response.");
HttpServletResponse httpResponse = WebUtils.toHttp(response);
responseSkip(httpResponse, ResponseCode.noLoginSkipResponse());
return false;
}
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域時會首先發(fā)送一個option請求,這里我們給option請求直接返回正常狀態(tài)
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
// 在配置的ShiroFilterFactoryBean攔截過濾器里,必須使用無狀態(tài)的token 這里如果沒有token 直接告訴前端需要重新登陸
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader(ShiroConstant.authorization_token);
if(authorization == null || authorization.length() == 0){
// 未攜帶token 不需要提示前端自動跳轉重新登陸
responseSkip(httpServletResponse, ResponseCode.noAuthHeaderTokenResponse("未攜帶token,請求無效"));
return false;
}
// 驗證token的正確性
Subject subject = SecurityUtils.getSubject();
if(!subject.isAuthenticated()){
// token失效 提示前端需要自動跳轉重新登陸
responseSkip(httpServletResponse, ResponseCode.invalidHeaderTokenSkipResponse());
return false;
}
return super.preHandle(request, response);
}
private void responseSkip(HttpServletResponse response, Response customizeResponse){
try {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
ObjectMapper objectMapper = new ObjectMapper();
String str = objectMapper.writeValueAsString(customizeResponse);
response.getWriter().println(str);
} catch (IOException e1) {
throw new RuntimeException(e1);
}
}
}
注解支持的bean配置
支持在SpringBoot在Controller使用@RequiresPermission()等標簽注解以及配置shiro的生命周期
// 支持在SpringBoot的Controller使用@RequiresPermission()等標簽注解 以及
@Bean("authorizationAttributeSourceAdvisor")
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 強制使用cglib,防止重復代理和可能引起代理出錯的問題 (沒明白)
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
// 配置shiro的生命周期處理
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
ShiroConfig.java完整類
package boot.example.shiro.config;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
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.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 螞蟻舞
*/
@Configuration
public class ShiroConfig {
// 密碼校驗bean
@Bean("credentialMatcher")
public ShiroCredentialMatcher credentialMatcher() {
return new ShiroCredentialMatcher();
}
// 身份認證和權限校驗Realm
@Bean("shiroRealm")
public ShiroRealm shiroRealm(@Qualifier("credentialMatcher") ShiroCredentialMatcher matcher){
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCacheManager(new MemoryConstrainedCacheManager());
shiroRealm.setCredentialsMatcher(matcher);
return shiroRealm;
}
// 會話管理, 管理用戶登錄后的會話
@Bean("sessionManager")
public ShiroSessionManager sessionManager(){
//將繼承后重寫的ShiroSessionManager加入bean
return new ShiroSessionManager();
}
// 安全管理器
@Bean("securityManager")
public SecurityManager securityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm) {
// web的安全管理器
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 設置授權和認證
manager.setRealm(shiroRealm);
// 設置會話管理
manager.setSessionManager(sessionManager());
return manager;
}
// 訪問shiro的過濾器
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("shiroFilter", new ShiroFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// 跳轉到登錄頁,實際跳轉后訪問的是接口,接口返回請登錄的信息
shiroFilterFactoryBean.setLoginUrl("/shiro-redirect/index");
//bean.setSuccessUrl("/shiro-redirect/index");
// 實際跳轉到未認證頁面,請重新登陸
shiroFilterFactoryBean.setUnauthorizedUrl("/shiro-redirect/unauthorized");
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 靜態(tài)路徑放開 anon:匿名用戶可訪問
filterChainDefinitionMap.put("/public/**", "anon");
filterChainDefinitionMap.put("/static/**", "anon");
// 調試工具全部放開 anon:匿名用戶可訪問
filterChainDefinitionMap.put("/swagger-resources", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
filterChainDefinitionMap.put("/v2/api-docs", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/doc.html", "anon");
// 登錄相關全部放開 anon:匿名用戶可訪問
filterChainDefinitionMap.put("/shiro-login/**", "anon");
filterChainDefinitionMap.put("/shiro-redirect/**", "anon");
// 匿名用戶可訪問 anon:匿名用戶可訪問
filterChainDefinitionMap.put("/shiro-anon/**", "anon");
// 認證用戶可訪問 authc:認證用戶可訪問
filterChainDefinitionMap.put("/shiro-authc/*", "authc");
// 自定義過濾器過濾的內容
filterChainDefinitionMap.put("/**", "shiroFilter");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
// 支持在SpringBoot的Controller使用@RequiresPermission()等標簽注解 以及
@Bean("authorizationAttributeSourceAdvisor")
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 強制使用cglib,防止重復代理和可能引起代理出錯的問題 (沒明白)
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
// 配置shiro的生命周期處理
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
SpringBoot整合Shiro的web應用需要Controller層來調用測試功能的
首先是ShiroConfig里設置的重定向類
BootShiroIndexRedirectController.java
package boot.example.shiro.controller;
import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 螞蟻舞
*/
@Controller
@RequestMapping("/shiro-redirect")
public class BootShiroIndexRedirectController {
public Logger log = LoggerFactory.getLogger(this.getClass());
@RequestMapping("/index")
@ResponseBody
public Response index() {
log.warn("redirect index");
return ResponseCode.noLoginResponse();
}
@RequestMapping("/unauthorized")
@ResponseBody
public Response unauthorized() {
log.warn("redirect unauthorized");
return ResponseCode.unauthorizedPermissionResponse();
}
}
匿名游客訪問類BootShiroTestAnonController.java
package boot.example.shiro.controller;
import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 螞蟻舞
*/
@RestController
@RequestMapping(value="/shiro-anon")
public class BootShiroTestAnonController {
@GetMapping(value="/hello")
public Response anonHello() {
return ResponseCode.successResponse("匿名游客用戶可訪問");
}
}
已經認證也就是登錄的用戶訪問類BootShiroTestAuthcController.java
package boot.example.shiro.controller;
import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value="/shiro-authc")
public class BootShiroTestAuthcController {
@GetMapping(value="/hello")
public Response authCHello() {
return ResponseCode.successResponse("你是認證用戶,可訪問此接口");
}
}
使用權限注解的類BootShiroTestSysUserController.java
package boot.example.shiro.controller;
import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.*;
/**
* 螞蟻舞
*/
@RestController
@RequestMapping(value="/sysUser")
public class BootShiroTestSysUserController {
@GetMapping(value="/hello")
public Response shiroFilterHello() {
return ResponseCode.successResponse("你正在訪問登錄后shiroFilter過濾器里的,無注解的接口");
}
@RequiresPermissions("sys:user:list")
@GetMapping(value="/list")
@ResponseBody
public Response userList() {
return ResponseCode.successResponse("你已經成功訪問到查詢用戶接口");
}
@RequiresPermissions("sys:user:add")
@GetMapping(value="/insert")
@ResponseBody
public Response userAdd() {
return ResponseCode.successResponse("你已經成功訪問到新增用戶接口");
}
@RequiresPermissions("sys:user:update")
@GetMapping(value="/update")
@ResponseBody
public Response userUpdate() {
return ResponseCode.successResponse("你已經成功訪問到更新用戶接口");
}
@RequiresPermissions("sys:user:delete")
@GetMapping(value="/delete")
@ResponseBody
public Response userDelete() {
return ResponseCode.successResponse("你已經成功訪問到刪除用戶接口");
}
}
登出類BootShiroLogoutController.java
package boot.example.shiro.controller;
import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 螞蟻舞
*/
@Controller
@RequestMapping("/shiro-logout")
public class BootShiroLogoutController {
@GetMapping(value="/logout")
@ResponseBody
public Response logoutGet() {
Subject subject = SecurityUtils.getSubject();
if(subject != null){
subject.logout();
return ResponseCode.successResponse("登出成功");
}
return ResponseCode.failResponse("登出失敗");
}
@PostMapping(value="/logout")
@ResponseBody
public Response logoutPost() {
Subject subject = SecurityUtils.getSubject();
if(subject != null){
subject.logout();
return ResponseCode.successResponse("登出成功");
}
return ResponseCode.failResponse("登出失敗");
}
}
登錄使用的類BootShiroLoginController.java
package boot.example.shiro.controller;
import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import boot.example.shiro.domain.SysUsers;
import boot.example.shiro.domain.UserValidate;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;
/**
* 螞蟻舞
*/
@Controller
@RequestMapping("/shiro-login")
public class BootShiroLoginController {
@GetMapping(value="/auth")
@ResponseBody
public Response authGet(@RequestParam(value = "username", required = true, defaultValue="shiro_admin") String username, @RequestParam(value = "password", required = true, defaultValue="123") String password) {
UserValidate userValidate = new UserValidate();
userValidate.setPassword(password);
userValidate.setUsername(username);
UsernamePasswordToken token = new UsernamePasswordToken(userValidate.getUsername(), userValidate.getPassword());
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
SysUsers sysUsers = (SysUsers) subject.getPrincipal();
Map<String, Object> map = new HashMap<>();
map.put("token", subject.getSession().getId().toString());
map.put("session", subject.getSession());
map.put("sysUsers", sysUsers);
return ResponseCode.successResponse(map);
} catch ( UnknownAccountException uae ) {
return ResponseCode.failResponse("error username");
} catch ( IncorrectCredentialsException ice ) {
return ResponseCode.failResponse("error password");
} catch ( LockedAccountException lae ) {
return ResponseCode.failResponse("locked user");
}
}
@PostMapping(value="/auth")
@ResponseBody
public Response authPost(@RequestBody UserValidate userValidate, HttpSession session) {
System.out.println(userValidate.toString());
UsernamePasswordToken token = new UsernamePasswordToken(userValidate.getUsername(), userValidate.getPassword());
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
SysUsers sysUsers = (SysUsers) subject.getPrincipal();
Map<String, Object> map = new HashMap<>();
map.put("token", subject.getSession().getId().toString());
map.put("session", subject.getSession());
map.put("sysUsers", sysUsers);
return ResponseCode.successResponse(map);
} catch ( UnknownAccountException uae ) {
return ResponseCode.failResponse("error username");
} catch ( IncorrectCredentialsException ice ) {
return ResponseCode.failResponse("error password");
} catch ( LockedAccountException lae ) {
return ResponseCode.failResponse("locked user");
}
}
}
SpringBoot整合Shiro的代碼里有拋出異常的情況,主要的異常在登錄的時候會在try catch里處理,返回給前端,但還是有些異常是捕獲不到的,因此需要加上異常處理
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
// shiro 未授權異常
@ExceptionHandler(UnauthorizedException.class)
@ResponseBody
public Response UnauthorizedExceptionHandler(HttpServletRequest request, UnauthorizedException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.unauthorizedPermissionResponse("未授權,您的操作權限不夠,可聯系管理員獲取操作權限");
}
// shiro 授權異常
@ExceptionHandler(AuthorizationException.class)
@ResponseBody
public Response AuthorizationException(HttpServletRequest request, AuthorizationException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse( "授權用戶不存在或已經過期,請重新登錄");
}
// shiro 未經身份驗證或身份驗證異常
@ExceptionHandler(UnauthenticatedException.class)
@ResponseBody
public Response UnauthenticatedException(HttpServletRequest request, UnauthenticatedException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse("未經身份驗證,身份驗證異常,請重新登錄");
}
// shiro 賬號鎖定異常
@ExceptionHandler(LockedAccountException.class)
@ResponseBody
public Response LockedAccountException(HttpServletRequest request, LockedAccountException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse("你的賬號已鎖定,請聯系管理員解鎖");
}
// shiro 未找到用戶異常
@ExceptionHandler(UnknownAccountException.class)
@ResponseBody
public Response UnknownAccountException(HttpServletRequest request, UnknownAccountException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse("你的賬號不存在");
}
// shiro 登錄用戶密碼校驗異常
@ExceptionHandler(IncorrectCredentialsException.class)
@ResponseBody
public Response IncorrectCredentialsException(HttpServletRequest request, IncorrectCredentialsException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse("你輸入的密碼錯誤");
}
完整的異常處理類GlobalExceptionHandler.java
package boot.example.shiro.config;
import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.authz.permission.InvalidPermissionStringException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.bind.BindException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
/**
* 螞蟻舞
*/
@ControllerAdvice
public class GlobalExceptionHandler {
public Logger log = LoggerFactory.getLogger(this.getClass());
// 全局異常:默認異常
@ExceptionHandler(Exception.class)
@ResponseBody
public Response defaultExceptionHandler(HttpServletRequest request, Exception e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.exceptionResponse(request.getRequestURI()+e.toString());
}
@ExceptionHandler(BindException.class)
@ResponseBody
public Response bindExceptionHandler(HttpServletRequest request, BindException e) {
return ResponseCode.exceptionResponse(e.toString());
}
// 全局異常:請求header缺少HeaderToken
@ExceptionHandler(ServletRequestBindingException.class)
@ResponseBody
public Response ServletRequestBindingExceptionHandler(HttpServletRequest request, ServletRequestBindingException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.noAuthHeaderTokenResponse();
}
// 全局異常:請求內容類型異常
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
@ResponseBody
public Response HttpMediaTypeNotSupportedExceptionHandler(HttpServletRequest request, HttpMediaTypeNotSupportedException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.exceptionResponse(e.toString());
}
// 全局異常:請求方法異常
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseBody
public Response HttpRequestMethodNotSupportedExceptionHandler(HttpServletRequest request, HttpRequestMethodNotSupportedException e) {
log.error(request.getRequestURI() +"----"+e.toString());
return ResponseCode.exceptionResponse(e.toString());
}
// 全局異常:請求參數格式或者參數類型不正確異常
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseBody
public Response HttpMessageNotReadableExceptionHandler(HttpServletRequest request, HttpMessageNotReadableException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.exceptionResponse(e.toString());
}
// shiro 權限不可用
@ExceptionHandler(InvalidPermissionStringException.class)
@ResponseBody
public Response InvalidPermissionStringException(HttpServletRequest request, IncorrectCredentialsException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.notPermissionResponse("你的權限不可用");
}
// shiro 未授權異常
@ExceptionHandler(UnauthorizedException.class)
@ResponseBody
public Response UnauthorizedExceptionHandler(HttpServletRequest request, UnauthorizedException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.unauthorizedPermissionResponse("未授權,您的操作權限不夠,可聯系管理員獲取操作權限");
}
// shiro 授權異常
@ExceptionHandler(AuthorizationException.class)
@ResponseBody
public Response AuthorizationException(HttpServletRequest request, AuthorizationException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse( "授權用戶不存在或已經過期,請重新登錄");
}
// shiro 未經身份驗證或身份驗證異常
@ExceptionHandler(UnauthenticatedException.class)
@ResponseBody
public Response UnauthenticatedException(HttpServletRequest request, UnauthenticatedException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse("未經身份驗證,身份驗證異常,請重新登錄");
}
// shiro 賬號鎖定異常
@ExceptionHandler(LockedAccountException.class)
@ResponseBody
public Response LockedAccountException(HttpServletRequest request, LockedAccountException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse("你的賬號已鎖定,請聯系管理員解鎖");
}
// shiro 未找到用戶異常
@ExceptionHandler(UnknownAccountException.class)
@ResponseBody
public Response UnknownAccountException(HttpServletRequest request, UnknownAccountException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse("你的賬號不存在");
}
// shiro 登錄用戶密碼校驗異常
@ExceptionHandler(IncorrectCredentialsException.class)
@ResponseBody
public Response IncorrectCredentialsException(HttpServletRequest request, IncorrectCredentialsException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse("你輸入的密碼錯誤");
}
}
跨域支持的BeanConfig.java
package boot.example.shiro.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* 螞蟻舞
*/
@Configuration
public class BeanConfig {
@Bean
public CorsFilter corsFilter(){
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("PUT");
corsConfiguration.addAllowedMethod("GET");
corsConfiguration.addAllowedMethod("POST");
corsConfiguration.addAllowedMethod("PATCH");
corsConfiguration.addAllowedMethod("OPTIONS");
corsConfiguration.addAllowedMethod("DELETE");
corsConfiguration.setMaxAge(1728000L);
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(urlBasedCorsConfigurationSource);
}
}
Response和靜態(tài)類ResponseCode是統一封裝的result結果集
public class Response {
private boolean state;
private int code;
private String msg;
private Object data;
private long timestamp;
? ? // get set
}
完整的SpringBoot整合Shiro的代碼結構
└─boot-example-shiro-separate-2.0.5
│ pom.xml
│
├─doc
│ boot-example-shiro-separate-2.0.5-back.zip
│
└─src
├─main
│ ├─java
│ │ └─boot
│ │ └─example
│ │ └─shiro
│ │ │ ShiroApp.java
│ │ │
│ │ ├─config
│ │ │ BeanConfig.java
│ │ │ GlobalExceptionHandler.java
│ │ │ ShiroConfig.java
│ │ │ ShiroConstant.java
│ │ │ ShiroCredentialMatcher.java
│ │ │ ShiroDataMapper.java
│ │ │ ShiroFilter.java
│ │ │ ShiroRealm.java
│ │ │ ShiroSessionManager.java
│ │ │ SwaggerConfig.java
│ │ │
│ │ ├─controller
│ │ │ BootShiroIndexRedirectController.java
│ │ │ BootShiroLoginController.java
│ │ │ BootShiroLogoutController.java
│ │ │ BootShiroTestAnonController.java
│ │ │ BootShiroTestAuthcController.java
│ │ │ BootShiroTestSysUserController.java
│ │ │
│ │ └─domain
│ │ Response.java
│ │ ResponseCode.java
│ │ SysUsers.java
│ │ UserValidate.java
│ │
│ └─resources
│ application.properties
│ logback-spring.xml
│
└─test
└─java
└─boot
└─example
└─shiro
ShiroAppTest.java
啟動SpringBoot項目,訪問swagger-ui(實際前后端分離的情況,這種方式也適合將前端代碼嵌入到SpringBoot項目中)
http://localhost:20400/doc.html
瀏覽器和SwaggerUi測試
1.先不登錄,訪問匿名游客(anon)

2.先不登錄,訪問認證用戶可訪問(authc)這里有重定向
在瀏覽器直接訪問/shiro-authc/hello 因為沒有授權重定向到了 /shiro-redirect/index,但在接口上就看不到重定向操作了,直接得到數據未登錄的結果

3.登錄,使用預定的賬號訪問帶注解的接口
app_admin 12345
這個賬號登錄訪問的接口權限只有sys:user:list
也就是只能訪問@RequiresPermissions("sys:user:list")

登錄信息里除了用戶信息還返回了token和session相關的信息,這個token就是前后端交互的
將token復制輸入到token框里面,能夠訪問@RequiresPermissions("sys:user:list")

將token復制輸入到token框里面,訪問@RequiresPermissions("sys:user:add")權限不夠

直接使用瀏覽器訪問@RequiresPermissions("sys:user:list")注解的接口,發(fā)現需要token,那是因為前后端交互用的就是token機制,無狀態(tài)的

4.當瀏覽器或swagger-ui上登錄后,后端SpringBoot項目重啟,訪問

可以看到登錄后使用瀏覽器直接輸入會跳轉,得到的確實是接口類型的數據,而不是顯示的某個頁面,但是在swagger-ui里,得到了JSESSIONID后面攜帶了token(sessionId)感覺像是沒有實現前后端分離,這里之前在ShiroSessionManager類提到了的,盡量將所有接口都放在shiroFilter過濾器里,就是不使用authc這些
將這個注釋掉
// 認證用戶可訪問 authc:認證用戶可訪問
filterChainDefinitionMap.put("/shiro-authc/*", "authc");
其他使用瀏覽器和SwaggerUI測試的就不截圖了(能避免的坑幾乎都避免了,比如shiro的彈窗登錄)
前后端交互要正真測試出能不能用,關鍵還是要使用獨立的前端代碼,這樣才能測試真正的效果,SpringBoot整合Shiro和Vue實現前后端分離web項目最常見,這里使用vue+elementui搭建的測試程序進行測試
使用Vue前后端分離的前端測試Demo
使用vue和elementui來簡單測試建立一個.vue文件就可以,核心代碼首先定義request.js
import axios from 'axios'
import { getToken} from '@/utils/cookies'
import { Notification } from 'element-ui'
const service = axios.create({
baseURL: "http://127.0.0.1:20400",
timeout: 60000,
headers: {'Content-Type': 'application/json;charset=UTF-8'}
})
// 統一請求攔截器
service.interceptors.request.use(
config => {
if("/shiro-login/auth" === config.url){
return config
}
// 把token給后端
config.headers['token'] = getToken()
if(getToken()){
config.headers['token'] = getToken()
} else {
Notification({title: '消息',message: 'token失效,請重新登陸',type: 'warning',offset: 40})
return
}
return config
},
error => {
// 請求出錯
console.log(error)
return Promise.reject(error)
}
)
// 統一響應攔截器
service.interceptors.response.use (
response => {
let res
// IE9時response.data是undefined,因此需要使用response.request.responseText(Stringify后的字符串)
if (response.data == undefined) {
res = JSON.parse(response.request.responseText)
} else {
res = response.data
}
//console.log(res);
//響應的邏輯判斷
if(res){
return res
}
return Promise.reject(new Error("請求錯誤" || 'Error'))
},
error => {
//響應出錯
console.log('err' + error)
return Promise.reject(error)
}
)
export default service
定義api接口shiroVue.js
import request from '@/axios/request'
// 登錄
export const login = (data) => {
return request({
url: '/shiro-login/auth',
method: 'post',
data
})
}
// 登出
export const logout = () => {
return request({
url: '/shiro-logout/logout',
method: 'post'
})
}
// 匿名游客
export function anonHello() {
return request({
url: '/shiro-anon/hello',
method: 'get'
})
}
// 認證用戶
export function authcHello() {
return request({
url: '/shiro-authc/hello',
method: 'get'
})
}
// 查詢用戶
export function userList() {
return request({
url: '/sysUser/list',
method: 'get'
})
}
// 新增用戶
export function userInsert() {
return request({
url: '/sysUser/insert',
method: 'get'
})
}
// 更新用戶
export function userUpdate() {
return request({
url: '/sysUser/update',
method: 'get'
})
}
// 刪除用戶
export function userDelete() {
return request({
url: '/sysUser/delete',
method: 'get'
})
}
需要用到cookie cookies.js
/**
* token認證
*
*/
import Cookies from 'js-cookie'
const mywTokenKey = 'mywToken'
export function getToken() {
return Cookies.get(mywTokenKey)
}
export function setToken(token) {
return Cookies.set(mywTokenKey, token)
}
export function removeToken() {
return Cookies.remove(mywTokenKey)
}
主要的測試代碼Home.vue
<template>
<div style="border-radius:4px;padding:4px;">
<el-row style="padding-top:40px;">
<el-col :span="24">
<div>SpringBoot+Shiro框架整合實現前后端分離的權限管理基礎Demo</div>
</el-col>
<el-col :span="24" style="margin-top: 20px;">
<el-input placeholder="用戶賬號" style="width:200px;margin-right:8px;" v-model="username" clearable></el-input>
<el-input placeholder="用戶密碼" style="width:200px;margin-right:8px;" v-model="password" clearable></el-input>
</el-col>
<el-col :span="24" style="margin-top: 20px;">
<el-button type="info" @click="handleLogin()">登錄系統</el-button>
<div style="height:4px;">{{ resultLogin }}</div>
</el-col>
<el-col :span="24" style="margin-top: 20px;">
<el-button @click="handleLoGout()">登出系統</el-button>
<div style="height:4px;">{{ resultLogout }}</div>
</el-col>
<el-col :span="24" style="margin-top: 20px;">
<el-button @click="handleanonHello()">匿名游客</el-button>
<div style="height:4px;">{{ resultAnonHello }}</div>
</el-col>
<el-col :span="24" style="margin-top: 20px;">
<el-button type="info" @click="handleuserList()">查詢用戶</el-button>
<div style="height:4px;">{{ resultuserList }}</div>
</el-col>
<el-col :span="24" style="margin-top: 20px;">
<el-button type="warning" @click="handleuserInsert()">新增用戶</el-button>
<div style="height:4px;">{{ resultuserInsert }}</div>
</el-col>
<el-col :span="24" style="margin-top: 20px;">
<el-button @click="handleuserUpdate()" type="success">編輯用戶</el-button>
<div style="height:4px;">{{ resultuserUpdate }}</div>
</el-col>
<el-col :span="24" style="margin-top: 20px;">
<el-button @click="handleuserDelete()">刪除用戶</el-button>
<div style="height:4px;">{{ resultuserDelete }}</div>
</el-col>
<el-col :span="24" style="margin-top: 30px;">
<el-button type="info" @click="handleauthcHello()">認證訪問(特殊)</el-button>
<div style="height:4px;">{{ resultAuthcHello }}</div>
</el-col>
</el-row>
</div>
</template>
<script>
import { login, logout, anonHello, authcHello, userList, userInsert, userUpdate, userDelete} from '@/api/modules/shiroVue'
import {setToken, removeToken } from '../utils/cookies'
export default {
name: 'Home',
data() {
return {
username: "shiro_admin",
password: "123",
resultLogin: "",
resultLogout: "",
resultAnonHello: "",
resultAuthcHello: "",
resultuserList: "",
resultuserInsert: "",
resultuserUpdate: "",
resultuserDelete: ""
}
},
created() {
},
methods: {
init() {
},
handleLogin(){
if(this.username && this.password){
var data = {username: this.username, password: this.password}
login(data).then((response) => {
console.log(response)
if(response.state){
setToken(response.data.token)
this.resultLogin = "msg:"+response.msg+" token:"+ response.data.token
} else {
this.resultLogin = "msg:"+ response.msg
}
}).catch(response => {
console.log(response);
});
}
},
handleLoGout(){
logout().then((response) => {
console.log(response)
removeToken()
this.resultLogout = "msg:"+ response.msg
}).catch(response => {
console.log(response);
});
},
handleanonHello(){
anonHello().then((response) => {
console.log(response)
this.resultAnonHello = "msg:"+ response.msg
}).catch(response => {
console.log(response);
});
},
handleauthcHello(){
authcHello().then((response) => {
console.log(response)
this.resultAuthcHello = "msg:"+ response.msg
}).catch(response => {
console.log(response);
});
},
handleuserList(){
userList().then((response) => {
console.log(response)
this.resultuserList = "msg:"+ response.msg
}).catch(response => {
console.log(response);
});
},
handleuserInsert(){
userInsert().then((response) => {
console.log(response)
this.resultuserInsert = "msg:"+ response.msg
}).catch(response => {
console.log(response);
});
},
handleuserUpdate(){
userUpdate().then((response) => {
console.log(response)
this.resultuserUpdate = "msg:"+ response.msg
}).catch(response => {
console.log(response);
});
},
handleuserDelete(){
userDelete().then((response) => {
console.log(response)
this.resultuserDelete = "msg:"+ response.msg
}).catch(response => {
console.log(response);
});
}
},
mounted() {
this.$nextTick(function () {
this.init()
})
},
watch: {
}
}
</script>
首先將前端請求的token先注釋掉
1.不登錄的情況下訪問接口


在未登錄,也沒有前端攜帶token的情況下,可以看到匿名游客可以訪問,使用注解@RequiresPermissions("*:*:*")提示沒有token,因為在shiroFilter過濾器里過濾了的,認證用戶訪問的接口提示未登錄
2.登錄的情況下訪問接口(前端不攜帶token)

可以看到登錄后,想要登出都不可能,因為登出也是需要token認證的,加了注解的四個接口也是沒有token,只有特殊的認證訪問顯示的是未登錄,因為這種情況是這個接口是不經過shiroFilter過濾器的,但是進了自定義的session會話ShiroSessionManager,在這里他從http請求的header里沒拿到token,因此ssesionId是null,即使調用了父類方法,也是沒有的,于是重定向了接口,在這里重定向到了/shiro-redirect/index 如果后端重啟,在這里也是同樣的狀態(tài),因此不測試了。
將前端請求的token注釋取消,就是前端請求后端接口攜帶token

1.不登錄的情況下訪問接口(這是正式環(huán)境不會出現的情況)

可以看到前端報錯了,那是因為在前端緩存里token不存在,直接return了,不是return config,所以不請求后端數據。
2.登錄的情況下訪問接口(前端攜帶token,正常情況)

這種整套操作流程,登錄和登出都沒問題
3.前端登錄后,后端程序重啟后訪問認證頁面(因為后端使用的內存,內存里沒了shiro相關的會話)

可以看到報了400錯誤,那個shiro處理跳轉了登錄頁面,分離的前端無法處理的,一般我們碰不到這個錯誤的
4.使用只有部分權限的賬號登錄

可以看到加了注解的接口只有查詢用戶可以訪問,其他的訪問權限不夠。文章來源:http://www.zghlxwxcb.cn/news/detail-738872.html
這里的SpringBoot+Shiro和Vue實現前后端分離的Demo實際上使用到了Session的sessionId作為token來操作,如果要對這個token要求嚴格,那么可以使用對sessionId二次加密,例如jwt方式,這樣在session重寫的會話管理里先對token解密后在放入會話里。文章來源地址http://www.zghlxwxcb.cn/news/detail-738872.html
到了這里,關于SpringBoot+Shiro框架整合實現前后端分離的權限管理基礎Demo的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!