概念模型
用戶與角色之間是一對一關聯,角色與資源之間是多對多關聯(關聯關係用中間表來維護)
Resource裏面存儲着系統的url路徑
模型字段都很簡單,SQL及Model類就不再貼出來了.
1.引入依賴
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
添加這一個依賴就夠用了,它會依賴core和web
我寫的shiro類有這些
CheckLoginFilter 登錄認證過濾器
CheckPermissionsFilter 權限校驗過濾器
ShiroConfig 配置類(核心)
ShiroLifecycleBeanPostProcessorConfig shiro生命週期管理
ShiroPermissionResolver 生成Permission實例(關鍵)
ShiroWildcardPermission Permission的子孫類,判斷權限通過與否(關鍵)
UserRealm 提供身份認證和授權(核心)
要使用自定義的鑑權方式,需要寫 PermissionResolver 和 WildcardPermission 的子類
注:所有的Service類.就不貼出來了,功能很簡單
1.首先看一下UserRealm
doGetAuthorizationInfo 和 doGetAuthenticationInfo方法必須要求實現
這裏爲了url權限校驗的需要,覆蓋了isPermitted方法(這不是必須的)
package com.web.shiro;
import com.web.constant.Constant;
import com.web.constant.ErrorCode;
import com.web.dto.AuthorizationRoleInfoDTO;
import com.web.entity.Role;
import com.web.entity.User;
import com.web.exception.PolarisException;
import com.web.service.RoleService;
import com.web.service.UserService;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.MemorySessionDAO;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
/**
* Realm 充當了 Shiro 與應用安全數據間的“橋樑”或者“連接器”
*
* @author
* @date 2019/07/09 10:43
*/
public class UserRealm extends AuthorizingRealm {
@Autowired
@Lazy
private UserService userService;
@Autowired
@Lazy
private RoleService roleService;
@Autowired
@Lazy
private MemorySessionDAO sessionDAO;
/**
* 授權
*
* @param principal principal
* @return 用戶權限信息集合
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
User user = (User) principal.getPrimaryPrincipal();
SimpleAuthorizationInfo info = (SimpleAuthorizationInfo) SecurityUtils.getSubject()
.getSession().getAttribute(Constant.AUTHORIZATION_INFO);
if (info != null) {
return info;
}
info = new SimpleAuthorizationInfo();
AuthorizationRoleInfoDTO roleInfo = roleService.getAuthorizationRoleInfoDTOByUsername(user.getUsername());
if (roleInfo == null) {
return info;
}
info.addRole(String.valueOf(roleInfo.getRoleId()));
// 設置權限碼
info.setStringPermissions(new HashSet<>(roleInfo.getUrls()));
SecurityUtils.getSubject().getSession().setAttribute(Constant.AUTHORIZATION_INFO, info);
return info;
}
/**
* 身份認證
*
* @param token token
* @return 用戶角色信息集合
* @throws AuthenticationException 認證異常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 當事人,類型取決於調用subject.login之前存儲的數據類型
String username = (String) token.getPrincipal();
User user = userService.getByUsername(username);
if (null == user) {
throw new PolarisException(ErrorCode.USERNAME_OR_PASSWORD_ERROR);
}
if (User.STATUS_DISABLE == user.getStatus()) {
throw new PolarisException(ErrorCode.USER_IS_DISABLED);
}
if (User.STATUS_DELETED == user.getStatus()) {
throw new PolarisException(ErrorCode.USERNAME_OR_PASSWORD_ERROR);
}
String password = user.getPassword();
user.setPassword(null);
user.setRole(roleService.getById(user.getRoleId()));
// 第一個參數隨便放,可以是user,在系統中任意位置可以獲取改對象;
// 第二個參數必須是密碼
// 第三個參數當前Realm的名稱,因爲可能存在多個Realm
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
stopPreviousSession(user.getId());
return info;
}
/**
* 超級管理員自動獲取所有權限
*/
@Override
public boolean isPermitted(PrincipalCollection principals, String permission) {
User user = ((User) principals.getPrimaryPrincipal());
if (Role.ADMIN_FLAG_SUPER_ADMIN == user.getRole().getAdminFlag()) {
return true;
}
return isPermitted(principals, getPermissionResolver().resolvePermission(permission));
}
@Override
public boolean isPermitted(PrincipalCollection principals, Permission permission) {
AuthorizationInfo info = getAuthorizationInfo(principals);
Collection<Permission> perms = getPermissions(info);
if (CollectionUtils.isEmpty(perms)) {
return false;
}
for (Permission perm : perms) {
if (perm.implies(permission)) {
return true;
}
}
return false;
}
/**
* 踢掉上一個登錄的同名用戶
*
* @param id 主鍵
*/
private void stopPreviousSession(Integer id) {
Collection<Session> sessions = sessionDAO.getActiveSessions();
Session currSession = SecurityUtils.getSubject().getSession();
Serializable sId = currSession.getId();
for (Session session : sessions) {
SimplePrincipalCollection collection = (SimplePrincipalCollection) session
.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (collection == null) {
continue;
}
User u = (User) collection.getPrimaryPrincipal();
if (id.equals(u.getId())) {
if (sId.equals(session.getId())) {
continue;
}
session.stop();
break;
}
}
}
}
2. ShiroWildcardPermission 類
implies方法就是用來校驗權限通過與否的.寫的比較簡單粗淺,考慮的情況很簡單....
假若用戶擁有 /user/page 路徑的訪問權限,那麼 /user/page/** 下的所有路徑都會判定通過,
而 /user 路徑判定不通過,/role 判定也不通過
package com.web.shiro;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.permission.WildcardPermission;
import java.util.List;
import java.util.Set;
/**
* @author
* @date 2019/07/25 15:10
*/
public class ShiroWildcardPermission extends WildcardPermission {
ShiroWildcardPermission(String wildcardString) {
super(wildcardString, DEFAULT_CASE_SENSITIVE);
}
@Override
public boolean implies(Permission p) {
if (!(p instanceof ShiroWildcardPermission)) {
return false;
}
List<Set<String>> targetParts = ((ShiroWildcardPermission) p).getParts();
String targetUrl = targetParts.get(0).iterator().next();
String url = getParts().get(0).iterator().next();
if (targetUrl.startsWith(url)) {
if (targetUrl.equals(url)) {
return true;
}
return targetUrl.startsWith(url.concat("/"));
}
return false;
}
}
3. ShiroPermissionResolver 類
很簡單,就是生成 ShiroWildcardPermission 類對象
package com.web.shiro;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.permission.PermissionResolver;
/**
* @author
* @date 2019/07/25 15:05
*/
public class ShiroPermissionResolver implements PermissionResolver {
@Override
public Permission resolvePermission(String permissionString) {
return new ShiroWildcardPermission(permissionString);
}
}
4.CheckLoginFilter
onAccessDenied 方法 當用戶沒有登錄的處理策略,這裏是直接返回json
ShiroUtil類放到最後面貼出
package com.web.shiro;
import com.web.base.RestResponse;
import com.web.constant.ErrorCode;
import com.web.utils.ShiroUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
* 登錄認證過濾器
*
* @author
*/
public class CheckLoginFilter extends FormAuthenticationFilter {
/**
* 表示訪問拒絕時是否自己處理,如果返回true表示自己不處理且繼續攔截器鏈執行,返回false表示自己已經處理了(比如重定向到另一個頁面)
*
* @param request 對象
* @param servletResponse 對象
* @return true-繼續往下執行,false-該filter過濾器已經處理,不繼續執行其他過濾器
* @throws Exception 異常
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse servletResponse) throws Exception {
HttpServletResponse response = (HttpServletResponse) servletResponse;
Object user = SecurityUtils.getSubject().getPrincipal();
if (null != user) {
return true;
}
Map<String, String> data = new HashMap<>(1);
data.put("toLoginUrl", "/login");
RestResponse<Map> rsp = new RestResponse<>(ErrorCode.SESSION_TIMEOUT_ERROR);
rsp.setMsg("你還沒有登錄或者會話過期!!!!!!!!!");
rsp.setData(data);
ShiroUtil.errorResponse(response, rsp);
return false;
}
}
5.CheckPermissionsFilter
第一個方法判定權限的是否通過,getPathWithinApplication(request) 取得當前請求的url路徑
方法會進入到UserRealm的 doGetAuthorizationInfo 方法進行授權,之後進入isPermitted方法進行初步處理,最終會進入ShiroWildcardPermission的 implies 進行鑑權.
第二個方法表示判定失敗時的處理措施
package com.web.shiro;
import com.web.base.RestResponse;
import com.web.constant.ErrorCode;
import com.web.utils.ShiroUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
/**
* 檢驗權限過濾器
*
* @author
* @date 2019/07/24 14:34
*/
public class CheckPermissionsFilter extends PermissionsAuthorizationFilter {
@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return SecurityUtils.getSubject().isPermitted(getPathWithinApplication(request));
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse r) {
if (null == SecurityUtils.getSubject().getPrincipals()) {
RestResponse rest = new RestResponse(ErrorCode.SESSION_TIMEOUT_ERROR);
ShiroUtil.errorResponse((HttpServletResponse) r, rest);
return false;
}
RestResponse rest = new RestResponse(ErrorCode.PERMIT_INSUFFICIENT);
rest.setMsg("你沒有訪問權限,請聯繫管理員");
ShiroUtil.errorResponse((HttpServletResponse) r, rest);
return false;
}
}
6.ShiroLifecycleBeanPostProcessorConfig 類
裏面只有一個bean
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author
* @date 2019/07/12 15:33
*/
@Configuration
public class ShiroLifecycleBeanPostProcessorConfig {
/**
* Shiro生命週期處理器
*
* @return LifecycleBeanPostProcessor
*/
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
7.ShiroConfig 類
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.eis.MemorySessionDAO;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Shiro 配置類
*
* @author
* @date 2019/07/09 10:13
*/
@Configuration
@AutoConfigureAfter(ShiroLifecycleBeanPostProcessorConfig.class)
public class ShiroConfig {
@Value("${shiro.session.global-session-timeout}")
String sessionTimeOut;
@Bean("userRealm")
public UserRealm userRealm() {
UserRealm realm = new UserRealm();
realm.setPermissionResolver(new ShiroPermissionResolver());
return realm;
}
/**
* 配置保存sessionId的cookie
* 注意:這裏的cookie 不是上面的記住我 cookie 記住我需要一個cookie session管理 也需要自己的cookie
* 默認爲: JSESSIONID 問題: 與SERVLET容器名衝突,重新定義爲sid
*/
@Bean("sessionIdCookie")
public SimpleCookie sessionIdCookie() {
//這個參數是cookie的名稱
SimpleCookie simpleCookie = new SimpleCookie("sid");
//setcookie的httponly屬性如果設爲true的話,會增加對xss防護的安全係數。它有以下特點:
//setcookie()的第七個參數
//設爲true後,只能通過http訪問,javascript無法訪問
//防止xss讀取cookie
simpleCookie.setHttpOnly(true);
simpleCookie.setPath("/");
//maxAge=-1表示瀏覽器關閉時失效此Cookie
simpleCookie.setMaxAge(1800);
return simpleCookie;
}
@Bean(name = "sessionDAO")
public MemorySessionDAO getMemorySessionDAO() {
return new MemorySessionDAO();
}
@Bean("sessionManager")
public DefaultWebSessionManager defaultWebSessionManager(
@Qualifier("sessionIdCookie") SimpleCookie simpleCookie,
@Qualifier("sessionDAO") MemorySessionDAO sessionDAO) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// 全局會話超時時間,毫秒,目前是二十分鐘
sessionManager.setGlobalSessionTimeout(Integer.parseInt(sessionTimeOut));
sessionManager.setSessionIdCookie(simpleCookie);
sessionManager.setSessionDAO(sessionDAO);
// 刪除無效的session
sessionManager.setDeleteInvalidSessions(true);
// 是否開啓定時調度器進行檢測過期session 默認爲true
sessionManager.setSessionValidationSchedulerEnabled(true);
//設置session失效的掃描時間, 清理用戶直接關閉瀏覽器造成的孤立會話 默認爲 1個小時
//設置該屬性 就不需要設置 ExecutorServiceSessionValidationScheduler 底層也是默認自動調用ExecutorServiceSessionValidationScheduler
// 毫秒 十分鐘
sessionManager.setSessionValidationInterval(600000);
//去掉URL中的JSESSIONID
sessionManager.setSessionIdUrlRewritingEnabled(false);
return sessionManager;
}
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(UserRealm userRealm, DefaultWebSessionManager sessionManager) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setSessionManager(sessionManager);
manager.setRealm(userRealm);
return manager;
}
/**
* 開啓shiro aop註解支持.
* 使用代理方式;所以需要開啓代碼支持;
* 可以在controller中的方法前加上註解
* 如 @RequiresPermissions("userInfo:add")
*
* @param securityManager 管理器
* @return AuthorizationAttributeSourceAdvisor
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
bean.setLoginUrl("/toLogin");
bean.setUnauthorizedUrl("/403");
// 定義過濾器
Map<String, Filter> filters = bean.getFilters();
filters.put("authc", new CheckLoginFilter());
filters.put("perms", new CheckPermissionsFilter());
bean.setFilters(filters);
// 配置訪問權限
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/*.ico", "anon");
filterChainDefinitionMap.put("/**/*.js", "anon");
filterChainDefinitionMap.put("/**/*.css", "anon");
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/user/login", "anon");
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/interface/**", "anon");
filterChainDefinitionMap.put("/**", "authc,perms");
bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return bean;
}
}
其中 shiroFilter 就配置了訪問策略
可以插入如下代碼,
(因爲可以我只是根據請求url判定,就沒有寫,當然即使加上了,在我這個代碼裏面也沒有效果,
這和CheckPermissionsFilter 類的 isAccessAllowed,UserRealm類的 doGetAuthorizationInfo 授權方法,以及
ShiroWildcardPermission類的鑑權方法implies有關)
filterChainDefinitionMap.put("/user/**","perms[123]"); // /user 下的所有路徑必須具備123權限纔可以訪問
filterChainDefinitionMap.put("/role/**","role[admin]"); // /role下的路徑必須具備admin角色纔可以訪問
8.ShiroUtil 類
就是輸出json提示信息
import com.fasterxml.jackson.databind.ObjectMapper;
import com.polaris.web.base.RestResponse;
import com.polaris.web.constant.ErrorCode;
import com.polaris.web.exception.PolarisException;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author
* @date 2019/07/24 15:15
*/
public class ShiroUtil {
public static void errorResponse(HttpServletResponse response, RestResponse rest) {
response.setStatus(200);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
try (PrintWriter writer = response.getWriter()) {
writer.write(new ObjectMapper().writeValueAsString(rest));
writer.flush();
} catch (IOException e) {
throw new PolarisException(ErrorCode.ServerError);
}
}
}
好,至此結束!
可以通過斷點了解執行流程,這樣可以方便理解
水平有限,錯誤之處敬請指出...謝謝!!!