- db
採用RBAC模式,其核心爲用戶-角色-權限三表。
- pom.xml
首先核心dependency如下(需要AOP依賴):
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- aop依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
可選依賴:
<!-- shiro-redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.2.2</version>
</dependency>
- ShiroConfig.java
package com.demo.shiro;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.SessionListener;
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.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
//import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
@Configuration
public class ShiroConfig {
/**
* Shiro的Web過濾器Factory 命名:shiroFilter
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 設置 securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 登錄的 url
shiroFilterFactoryBean.setLoginUrl("/login");
// 登錄成功後跳轉的 url
shiroFilterFactoryBean.setSuccessUrl("/index");
// 未授權 url
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 設置免認證 url
filterChainDefinitionMap.put("/jsonTest", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/photos/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/guest/**", "anon");
// }
// 配置退出過濾器,其中具體的退出代碼 Shiro已經替我們實現了
//filterChainDefinitionMap.put("/logout", "logout");
// 除上以外所有 url都必須認證通過纔可以訪問,未通過認證自動訪問 LoginUrl
filterChainDefinitionMap.put("/**", "user");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
// 注入自定義的realm,告訴shiro如何獲取用戶信息來做登錄或權限控制
@Bean
public CustomRealm realm() {
return new CustomRealm();
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 配置 SecurityManager,並注入 shiroRealm
securityManager.setRealm(realm());
// 配置 rememberMeCookie
securityManager.setRememberMeManager(rememberMeManager());
// 配置 緩存管理類 cacheManager
securityManager.setCacheManager(cacheManager());
securityManager.setSessionManager(sessionManager());
return securityManager;
}
@Bean(name = "lifecycleBeanPostProcessor")
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
// shiro 生命週期處理器
return new LifecycleBeanPostProcessor();
}
/**
* DefaultAdvisorAutoProxyCreator 和 AuthorizationAttributeSourceAdvisor 用於開啓
* shiro 註解的使用 如 @RequiresAuthentication, @RequiresUser, @RequiresPermissions 等
*
* @return DefaultAdvisorAutoProxyCreator
*/
@Bean
@DependsOn({ "lifecycleBeanPostProcessor" })
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 用於開啓 Thymeleaf 中的 shiro 標籤的使用
*
* @return ShiroDialect shiro 方言對象
*/
// @Bean
// public ShiroDialect shiroDialect() {
// return new ShiroDialect();
// }
/**
* shiro 中配置 redis 緩存
*
* @return RedisManager
*/
private RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
return redisManager;
}
/**
* shiro 中配置 redis cache緩存
*
* @return RedisCacheManager
*/
private RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
* rememberMe cookie 效果是重開瀏覽器後無需重新登錄
*
* @return SimpleCookie
*/
private SimpleCookie rememberMeCookie() {
// 設置 cookie 名稱,對應 login.html 頁面的 <input type="checkbox" name="rememberMe"/>
SimpleCookie cookie = new SimpleCookie("rememberMe");
// cookie.setSecure(true); // 只在 https中有效 註釋掉 正常
// 設置 cookie 的過期時間,單位爲秒,這裏爲一天
cookie.setMaxAge(2000);
return cookie;
}
/**
* cookie管理對象
*
* @return CookieRememberMeManager
*/
private CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
// rememberMe cookie 加密的密鑰
cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
return cookieRememberMeManager;
}
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
/**
* session 管理對象
*
* @return DefaultWebSessionManager
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
Collection<SessionListener> listeners = new ArrayList<>();
listeners.add(new ShiroSessionListener());
// 設置session超時時間,單位爲毫秒
sessionManager.setGlobalSessionTimeout(2000000);
sessionManager.setSessionListeners(listeners);
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
}
- CustomRealm.java
package com.demo.shiro;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.shiro.authc.AccountException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.dubbo.config.annotation.Reference;
import com.demo.entity.Permission;
import com.demo.entity.Role;
import com.demo.entity.User;
import com.demo.service.IPermissionService;
import com.demo.service.IRoleService;
import com.demo.service.IUserService;
public class CustomRealm extends AuthorizingRealm {
private static final Logger log = LoggerFactory.getLogger(CustomRealm.class);
@Reference(version = "1.0.0")
private IUserService iUserService;
@Reference(version = "1.0.0")
private IRoleService iRoleService;
@Reference(version = "1.0.0")
private IPermissionService iPermissionService;
//定義如何獲取用戶的角色和權限的邏輯,給shiro做權限判斷
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("------------權限認證---------");
User user = (User) getAvailablePrincipal(principals);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
List<Role> list = this.iRoleService.findUserRole(user.getNickname());
Set<String> set = new HashSet<String>();
for (Role r : list) {
set.add(r.getName());
}
Set<String> perms = new HashSet<String>();
List<Permission> list1 = this.iPermissionService.findRolePerm(user.getNickname());
for(Permission p : list1) {
perms.add(p.getUrl());
}
info.setRoles(set);//添加角色集合 @RequireRoles("admin")會到info中尋找 字符串 "admin"
info.setStringPermissions(perms);// 添加權限集合 @RequiresPermissions("test") 會到info中尋找字符串"test"
return info;
}
// 定義如何獲取用戶信息的業務邏輯,給shiro做登錄
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)
throws AuthenticationException {
log.info("------------身份認證方法---------");
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
String nickname = token.getUsername();
if (nickname == null) {
throw new AccountException("Null usernames are not allowed by this realm.");
}
User user = this.iUserService.findByNickName(nickname); // 從數據庫獲取對應用戶名密碼的用戶
if (user == null) {
throw new UnknownAccountException("No account found for admin [" + nickname + "]");
}
if(user.getStatus() == 0) {
throw new LockedAccountException("您的賬號被禁止登錄,請聯繫管理員");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPswd(), getName());
return info;
}
}
- ShiroSessionListener.java
package com.demo.shiro;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;
public class ShiroSessionListener implements SessionListener{
private final AtomicInteger sessionCount = new AtomicInteger(0);
@Override
public void onStart(Session session) {
sessionCount.incrementAndGet();
}
@Override
public void onStop(Session session) {
sessionCount.decrementAndGet();
}
@Override
public void onExpiration(Session session) {
sessionCount.decrementAndGet();
}
}
- GlobalExceptionHandler.java(Shiro異常捕獲)
package com.demo.handler;
import org.apache.shiro.ShiroException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import com.demo.domain.Codes;
import com.demo.domain.Json;
/**
* 統一捕捉shiro的異常,返回給前臺一個json信息,前臺根據這個信息顯示對應的提示,或者做頁面的跳轉。
*/
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
//不滿足@RequiresGuest註解時拋出的異常信息
private static final String GUEST_ONLY = "Attempting to perform a guest-only operation";
@ExceptionHandler(ShiroException.class)
@ResponseBody
public Json handleShiroException(ShiroException e) {
String eName = e.getClass().getSimpleName();
log.error("shiro執行出錯:{}",eName);
return new Json(eName, false, Codes.SHIRO_ERR, "鑑權或授權過程出錯", null);
}
@ExceptionHandler(UnauthenticatedException.class)
@ResponseBody
public Json page401(UnauthenticatedException e) {
String eMsg = e.getMessage();
if (StringUtils.startsWithIgnoreCase(eMsg,GUEST_ONLY)){
return new Json("401", false, Codes.UNAUTHEN, "只允許遊客訪問,若您已登錄,請先退出登錄", null)
.data("detail",e.getMessage());
}else{
return new Json("401", false, Codes.UNAUTHEN, "用戶未登錄", null)
.data("detail",e.getMessage());
}
}
@ExceptionHandler(UnauthorizedException.class)
@ResponseBody
public Json page403() {
return new Json("403", false, Codes.UNAUTHZ, "用戶沒有訪問權限", null);
}
}
- 測試
RequiresPermissions("/share")//代表該類下所有方法的訪問需要 登錄用戶擁有 權限 “/share”,可類比至方法註解等。
public class ShareController {
…//
}
- 登錄
@RequestMapping(value = { "/login" }, method = { RequestMethod.POST }, consumes = {
"application/json" }, produces = "application/json;charset=UTF-8")
public @ResponseBody Json login(@RequestBody User user) {//Json 爲封裝好的返回對象,可以自己設置
log.info("==========================/login==================================");
String oper = "user login : ";
JSONObject responseObj = (JSONObject) JSONObject.toJSON(user);
log.info(oper + responseObj);
String nickname = responseObj.getString("nickname");
String pswd = responseObj.getString("pswd");
if (StringUtils.isEmpty(nickname)) {
return Json.fail(oper, "用戶名不能爲空");
}
if (StringUtils.isEmpty(pswd)) {
return Json.fail(oper, "密碼不能爲空");
}
pswd = MD5Utils.encrypt(pswd);// 密碼MD5加密
UsernamePasswordToken token = new UsernamePasswordToken(nickname, pswd);
try {
// 登錄
Subject subject = SecurityUtils.getSubject();
// 從session取出用戶信息
if (subject != null) {
subject.logout();
}
subject.login(token);// shiro 認證
this.iUserService.updateLoginTime(nickname);// 更新最近一次登錄時間
User user0 = (User) SecurityUtils.getSubject().getPrincipal();
//獲取用戶角色信息
List<Role> roles = new ArrayList<Role>();
List<Role> list = this.iRoleService.findUserRole(user.getNickname());
for (Role r : list) {
roles.add(r);
}
//獲取用戶權限信息
List<Permission> perms = new ArrayList<Permission>();
List<Permission> list1 = this.iPermissionService.findRolePerm(user.getNickname());
for(Permission p : list1) {
perms.add(p);
}
user0.setUserRoles(roles);
user0.setUserPerms(perms);
return Json.succ(oper).data(user0);
} catch (UnknownAccountException uae) {
log.warn("用戶帳號不正確");
return Json.fail(oper, "用戶帳號或密碼不正確");
} catch (IncorrectCredentialsException ice) {
log.warn("用戶密碼不正確");
return Json.fail(oper, "用戶帳號或密碼不正確");
} catch (LockedAccountException lae) {
log.warn("用戶帳號被鎖定");
return Json.fail(oper, "用戶帳號被鎖定不可用");
} catch (AuthenticationException ae) {
log.warn("登錄出錯");
return Json.fail(oper, "登錄失敗:" + ae.getMessage());
}
}