SpringBoot
?是為了簡(jiǎn)化?Spring
?應(yīng)用的創(chuàng)建、運(yùn)行、調(diào)試、部署等一系列問(wèn)題而誕生的產(chǎn)物,自動(dòng)裝配的特性讓我們可以更好的關(guān)注業(yè)務(wù)本身而不是外部的XML配置,我們只需遵循規(guī)范,引入相關(guān)的依賴(lài)就可以輕易的搭建出一個(gè) WEB 工程成都創(chuàng)新互聯(lián)公司主營(yíng)惠陽(yáng)網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司,主營(yíng)網(wǎng)站建設(shè)方案,app軟件開(kāi)發(fā),惠陽(yáng)h5成都微信小程序搭建,惠陽(yáng)網(wǎng)站營(yíng)銷(xiāo)推廣歡迎惠陽(yáng)等地區(qū)企業(yè)咨詢(xún)
Shiro 是 Apache 旗下開(kāi)源的一款強(qiáng)大且易用的Java安全框架,身份驗(yàn)證、授權(quán)、加密、會(huì)話管理。?相比?Spring Security
?而言?Shiro
更加輕量級(jí),且 API 更易于理解…
Shiro
?主要分為?安全認(rèn)證?和?接口授權(quán)?兩個(gè)部分,其中的核心組件為?Subject
、SecurityManager
、Realms
,公共部分?Shiro
?都已經(jīng)為我們封裝好了,我們只需要按照一定的規(guī)則去編寫(xiě)響應(yīng)的代碼即可…
利用?Spring Boot
?與?Shiro
?實(shí)現(xiàn)安全認(rèn)證和授權(quán)….
依賴(lài)?spring-boot-starter-web
…
UTF-8
UTF-8
1.8
1.4.0
org.springframework.boot
spring-boot-starter-web
org.apache.shiro
shiro-core
${shiro.version}
org.apache.shiro
shiro-spring
${shiro.version}
org.apache.shiro
shiro-ehcache
${shiro.version}
Shiro 為我們提供了?CacheManager
?即緩存管理,將用戶(hù)權(quán)限數(shù)據(jù)存儲(chǔ)在緩存,可以提高它的性能。支持?EhCache
、redis
?等常規(guī)緩存,這里為了簡(jiǎn)單起見(jiàn)就用?EhCache
?了 , 在resources
?目錄下創(chuàng)建一個(gè)?ehcache-shiro.xml
?文件
創(chuàng)建一個(gè)?User.java
?,標(biāo)記為數(shù)據(jù)庫(kù)用戶(hù)
package com.battcn.entity;
/**
* @author Levin
* @since 2018/6/28 0028
*/
public class User {
/** 自增ID */
private Long id;
/** 賬號(hào) */
private String username;
/** 密碼 */
private String password;
/** 角色名:Shiro 支持多個(gè)角色,而且接收參數(shù)也是 Set 集合,但這里為了簡(jiǎn)單起見(jiàn)定義成 String 類(lèi)型了 */
private String roleName;
/** 是否禁用 */
private boolean locked;
// 省略 GET SET 構(gòu)造函數(shù)...
}
支持?roles
、permissions
,比如你一個(gè)接口可以允許用戶(hù)擁有某一個(gè)角色,也可以是擁有某一個(gè)?permission
?…
package com.battcn.config;
import com.battcn.entity.User;
import java.util.*;
/**
* 主要不想連接數(shù)據(jù)庫(kù)..
*
* @author Levin
* @since 2018/6/28 0028
*/
public class DBCache {
/**
* K 用戶(hù)名
* V 用戶(hù)信息
*/
public static final Map USERS_CACHE = new HashMap<>();
/**
* K 角色I(xiàn)D
* V 權(quán)限編碼
*/
public static final Map> PERMISSIONS_CACHE = new HashMap<>();
static {
// TODO 假設(shè)這是數(shù)據(jù)庫(kù)記錄
USERS_CACHE.put("u1", new User(1L, "u1", "p1", "admin", true));
USERS_CACHE.put("u2", new User(2L, "u2", "p2", "admin", false));
USERS_CACHE.put("u3", new User(3L, "u3", "p3", "test", true));
PERMISSIONS_CACHE.put("admin", Arrays.asList("user:list", "user:add", "user:edit"));
PERMISSIONS_CACHE.put("test", Collections.singletonList("user:list"));
}
}
Shiro 的主要配置信息都在此文件內(nèi)實(shí)現(xiàn);
package com.battcn.config;
import org.apache.shiro.cache.ehcache.EhCacheManager;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Shiro 配置
*
* @author Levin
*/
@Configuration
public class ShiroConfiguration {
private static final Logger log = LoggerFactory.getLogger(ShiroConfiguration.class);
@Bean
public EhCacheManager getEhCacheManager() {
EhCacheManager em = new EhCacheManager();
em.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
return em;
}
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 加密器:這樣一來(lái)數(shù)據(jù)庫(kù)就可以是密文存儲(chǔ),為了演示我就不開(kāi)啟了
*
* @return HashedCredentialsMatcher
*/
// @Bean
// public HashedCredentialsMatcher hashedCredentialsMatcher() {
// HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// //散列算法:這里使用MD5算法;
// hashedCredentialsMatcher.setHashAlgorithmName("md5");
// //散列的次數(shù),比如散列兩次,相當(dāng)于 md5(md5(""));
// hashedCredentialsMatcher.setHashIterations(2);
// return hashedCredentialsMatcher;
// }
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
autoProxyCreator.setProxyTargetClass(true);
return autoProxyCreator;
}
@Bean(name = "authRealm")
public AuthRealm authRealm(EhCacheManager cacheManager) {
AuthRealm authRealm = new AuthRealm();
authRealm.setCacheManager(cacheManager);
return authRealm;
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(AuthRealm authRealm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(authRealm);
//
defaultWebSecurityManager.setCacheManager(getEhCacheManager());
return defaultWebSecurityManager;
}
@Bean
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(
DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
/**
* ShiroFilter
* 注意這里參數(shù)中的 StudentService 和 IScoreDao 只是一個(gè)例子,因?yàn)槲覀冊(cè)谶@里可以用這樣的方式獲取到相關(guān)訪問(wèn)數(shù)據(jù)庫(kù)的對(duì)象,
* 然后讀取數(shù)據(jù)庫(kù)相關(guān)配置,配置到 shiroFilterFactoryBean 的訪問(wèn)規(guī)則中。實(shí)際項(xiàng)目中,請(qǐng)使用自己的Service來(lái)處理業(yè)務(wù)邏輯。
*
* @param securityManager 安全管理器
* @return ShiroFilterFactoryBean
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必須設(shè)置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不設(shè)置默認(rèn)會(huì)自動(dòng)尋找Web工程根目錄下的"/login"頁(yè)面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登錄成功后要跳轉(zhuǎn)的連接
shiroFilterFactoryBean.setSuccessUrl("/index");
shiroFilterFactoryBean.setUnauthorizedUrl("/denied");
loadShiroFilterChain(shiroFilterFactoryBean);
return shiroFilterFactoryBean;
}
/**
* 加載shiroFilter權(quán)限控制規(guī)則(從數(shù)據(jù)庫(kù)讀取然后配置)
*/
private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean) {
/////////////////////// 下面這些規(guī)則配置最好配置到配置文件中 ///////////////////////
// TODO 重中之重啊,過(guò)濾順序一定要根據(jù)自己需要排序
Map filterChainDefinitionMap = new LinkedHashMap<>();
// 需要驗(yàn)證的寫(xiě) authc 不需要的寫(xiě) anon
filterChainDefinitionMap.put("/resource/**", "anon");
filterChainDefinitionMap.put("/install", "anon");
filterChainDefinitionMap.put("/hello", "anon");
// anon:它對(duì)應(yīng)的過(guò)濾器里面是空的,什么都沒(méi)做
log.info("##################從數(shù)據(jù)庫(kù)讀取權(quán)限規(guī)則,加載到shiroFilter中##################");
// 不用注解也可以通過(guò) API 方式加載權(quán)限規(guī)則
Map permissions = new LinkedHashMap<>();
permissions.put("/users/find", "perms[user:find]");
filterChainDefinitionMap.putAll(permissions);
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
}
}
上面介紹過(guò)?Realm
?,安全認(rèn)證和權(quán)限驗(yàn)證的核心處理就是重寫(xiě)?AuthorizingRealm
?中的?doGetAuthenticationInfo(登錄認(rèn)證)
?與?doGetAuthorizationInfo(權(quán)限驗(yàn)證)
package com.battcn.config;
import com.battcn.entity.User;
import org.apache.shiro.SecurityUtils;
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.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.context.annotation.Configuration;
import java.util.*;
/**
* 認(rèn)證領(lǐng)域
*
* @author Levin
* @version 2.5.1
* @since 2018-01-10
*/
@Configuration
public class AuthRealm extends AuthorizingRealm {
/**
* 認(rèn)證回調(diào)函數(shù),登錄時(shí)調(diào)用
* 首先根據(jù)傳入的用戶(hù)名獲取User信息;然后如果user為空,那么拋出沒(méi)找到帳號(hào)異常UnknownAccountException;
* 如果user找到但鎖定了拋出鎖定異常LockedAccountException;最后生成AuthenticationInfo信息,
* 交給間接父類(lèi)AuthenticatingRealm使用CredentialsMatcher進(jìn)行判斷密碼是否匹配,
* 如果不匹配將拋出密碼錯(cuò)誤異常IncorrectCredentialsException;
* 另外如果密碼重試此處太多將拋出超出重試次數(shù)異常ExcessiveAttemptsException;
* 在組裝SimpleAuthenticationInfo信息時(shí), 需要傳入:身份信息(用戶(hù)名)、憑據(jù)(密文密碼)、鹽(username+salt),
* CredentialsMatcher使用鹽加密傳入的明文密碼和此處的密文密碼進(jìn)行匹配。
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
String principal = (String) token.getPrincipal();
User user = Optional.ofNullable(DBCache.USERS_CACHE.get(principal)).orElseThrow(UnknownAccountException::new);
if (!user.isLocked()) {
throw new LockedAccountException();
}
// 從數(shù)據(jù)庫(kù)查詢(xún)出來(lái)的賬號(hào)名和密碼,與用戶(hù)輸入的賬號(hào)和密碼對(duì)比
// 當(dāng)用戶(hù)執(zhí)行登錄時(shí),在方法處理上要實(shí)現(xiàn) user.login(token)
// 然后會(huì)自動(dòng)進(jìn)入這個(gè)類(lèi)進(jìn)行認(rèn)證
// 交給 AuthenticatingRealm 使用 CredentialsMatcher 進(jìn)行密碼匹配,如果覺(jué)得人家的不好可以自定義實(shí)現(xiàn)
// TODO 如果使用 HashedCredentialsMatcher 這里認(rèn)證方式就要改一下 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal, "密碼", ByteSource.Util.bytes("密碼鹽"), getName());
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal, user.getPassword(), getName());
Session session = SecurityUtils.getSubject().getSession();
session.setAttribute("USER_SESSION", user);
return authenticationInfo;
}
/**
* 只有需要驗(yàn)證權(quán)限時(shí)才會(huì)調(diào)用, 授權(quán)查詢(xún)回調(diào)函數(shù), 進(jìn)行鑒權(quán)但緩存中無(wú)用戶(hù)的授權(quán)信息時(shí)調(diào)用.在配有緩存的情況下,只加載一次.
* 如果需要?jiǎng)討B(tài)權(quán)限,但是又不想每次去數(shù)據(jù)庫(kù)校驗(yàn),可以存在ehcache中.自行完善
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
Session session = SecurityUtils.getSubject().getSession();
User user = (User) session.getAttribute("USER_SESSION");
// 權(quán)限信息對(duì)象info,用來(lái)存放查出的用戶(hù)的所有的角色(role)及權(quán)限(permission)
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 用戶(hù)的角色集合
Set roles = new HashSet<>();
roles.add(user.getRoleName());
info.setRoles(roles);
// 用戶(hù)的角色對(duì)應(yīng)的所有權(quán)限,如果只使用角色定義訪問(wèn)權(quán)限,下面可以不要
// 只有角色并沒(méi)有顆粒度到每一個(gè)按鈕 或 是操作選項(xiàng) PERMISSIONS 是可選項(xiàng)
final Map> permissionsCache = DBCache.PERMISSIONS_CACHE;
final Collection permissions = permissionsCache.get(user.getRoleName());
info.addStringPermissions(permissions);
return info;
}
}
在?ShiroConfiguration
?中的?shiroFilter
?處配置了?/hello = anon
,意味著可以不需要認(rèn)證也可以訪問(wèn),那么除了這種方式外?Shiro
還為我們提供了一些注解相關(guān)的方式…
常用注解
/path = anon
AuthorizationException
AuthorizationException
package com.battcn.controller;
import com.battcn.config.ShiroConfiguration;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
* @author Levin
* @since 2018/6/28 0028
*/
@RestController
public class LoginController {
private static final Logger log = LoggerFactory.getLogger(ShiroConfiguration.class);
@GetMapping(value = "/hello")
public String hello() {
log.info("不登錄也可以訪問(wèn)...");
return "hello...";
}
@GetMapping(value = "/index")
public String index() {
log.info("登陸成功了...");
return "index";
}
@GetMapping(value = "/denied")
public String denied() {
log.info("小伙子權(quán)限不足,別無(wú)謂掙扎了...");
return "denied...";
}
@GetMapping(value = "/login")
public String login(String username, String password, RedirectAttributes model) {
// 想要得到 SecurityUtils.getSubject() 的對(duì)象..訪問(wèn)地址必須跟 shiro 的攔截地址內(nèi).不然后會(huì)報(bào)空指針
Subject sub = SecurityUtils.getSubject();
// 用戶(hù)輸入的賬號(hào)和密碼,,存到UsernamePasswordToken對(duì)象中..然后由shiro內(nèi)部認(rèn)證對(duì)比,
// 認(rèn)證執(zhí)行者交由 com.battcn.config.AuthRealm 中 doGetAuthenticationInfo 處理
// 當(dāng)以上認(rèn)證成功后會(huì)向下執(zhí)行,認(rèn)證失敗會(huì)拋出異常
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
sub.login(token);
} catch (UnknownAccountException e) {
log.error("對(duì)用戶(hù)[{}]進(jìn)行登錄驗(yàn)證,驗(yàn)證未通過(guò),用戶(hù)不存在", username);
token.clear();
return "UnknownAccountException";
} catch (LockedAccountException lae) {
log.error("對(duì)用戶(hù)[{}]進(jìn)行登錄驗(yàn)證,驗(yàn)證未通過(guò),賬戶(hù)已鎖定", username);
token.clear();
return "LockedAccountException";
} catch (ExcessiveAttemptsException e) {
log.error("對(duì)用戶(hù)[{}]進(jìn)行登錄驗(yàn)證,驗(yàn)證未通過(guò),錯(cuò)誤次數(shù)過(guò)多", username);
token.clear();
return "ExcessiveAttemptsException";
} catch (AuthenticationException e) {
log.error("對(duì)用戶(hù)[{}]進(jìn)行登錄驗(yàn)證,驗(yàn)證未通過(guò),堆棧軌跡如下", username, e);
token.clear();
return "AuthenticationException";
}
return "success";
}
}
package com.battcn.controller;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Levin
* @since 2018/6/28 0028
*/
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping
public String get() {
return "get.....";
}
/**
* RequiresRoles 是所需角色 包含 AND 和 OR 兩種
* RequiresPermissions 是所需權(quán)限 包含 AND 和 OR 兩種
*
* @return msg
*/
@RequiresRoles(value = {"admin", "test"}, logical = Logical.OR)
//@RequiresPermissions(value = {"user:list", "user:query"}, logical = Logical.OR)
@GetMapping("/query")
public String query() {
return "query.....";
}
@GetMapping("/find")
public String find() {
return "find.....";
}
}
package com.battcn;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Levin
*/
@SpringBootApplication
public class Chapter25Application {
public static void main(String[] args) {
SpringApplication.run(Chapter25Application.class, args);
}
}
啟動(dòng)?Chapter25Application.java
?中的?main
?方法,為了更好的演示效果這里打開(kāi)了?postman
?做的測(cè)試,只演示其中一個(gè)流程,剩下的可以自己復(fù)制代碼測(cè)試…
先登錄,由于?u3
?在?DBCache
?中擁有的角色是?test
,只有?user:list
?這一個(gè)權(quán)限
登陸
訪問(wèn)?/users/query
?成功,因?yàn)槲覀兎享憫?yīng)的角色/權(quán)限
訪問(wèn)Queryji
訪問(wèn)?/users/find
?失敗,并重定向到了?/denied
?接口,問(wèn)題來(lái)了為什么?/users/find
?沒(méi)有寫(xiě)注解也權(quán)限不足呢?
權(quán)限不足
細(xì)心的朋友肯定會(huì)發(fā)現(xiàn)?在 ShiroConfiguration 中寫(xiě)了一句 permissions.put(“/users/find”, “perms[user:find]”);?意味著我們不僅可以通過(guò)注解方式,同樣可以通過(guò)初始化時(shí)加載數(shù)據(jù)庫(kù)中的權(quán)限樹(shù)做控制,看各位喜好了….